软件工程实践系列文章,
会着重讲述实际的工程项目中是如何协作开发软件的。
本文主要介绍了 django/python 系列的工具链。

outline

本文包括以下内容:

  • outline
  • django: 一个搭建后端服务的工具箱。
    • framework: django vs flask/tornado/spring/laravel
    • restful: django/restframework/swagger
    • worker: django/uwsgi/gevent/celery/channels
    • database: django/mysql/sqlite/migrations
  • python: 一门依赖开发者的语言。
    • developing: gitlab/pipenv/docker
    • quality: unittest/pytest/flake8/pylint/yapf
    • deploy: fabric/aws/nginx
  • conclusion


django

django 是一个大名鼎鼎的后端开发框架,
它自己的口号是 the web framework for perfectionists with deadlines.

在我用 django 开发的这几年来,
我觉得它是一个逻辑上自洽,
并且为了逻辑自洽甚至舍弃了一部分功能的框架。

framework

django-vs
> search google for django vs

讲框架避免不了的是同行竞争,
比如到网上搜一下 django vs ... 就有一大堆搜索结果。
其实框架之间的比较是很难的,
每种框架都有自己适合的业务场景。

xkcd-927
> xkcd-927: standards

django 最大的特点就是 Model 是一等公民
在 django 中的所有的操作都会跟 Model 相关,
比如它提供了自带的强大 ORM,
也有一系列挂载在 Model 上的校验等。

个人感觉在项目的业务需求达到了某种程度的多样化以后,
基础框架用什么并不重要,
适合开发团队才是最重要的。

鉴于本文的标题是 django,
所以我们只讲 django。

restful

我参与的项目基本都是前后端分离的项目,
后端提供的接口都是用 djangorestframework 写的。
虽然像 HATEOAS 这样的高级属性还没用到,
但接口是遵循 restful 风格的,
比如像用 http method+status 表达语义,
对资源的定义等。

接口文档我们选用了 drf-yasg 来生成符合 swagger 规范的文档。
曾经我们也试过 django-rest-swagger 这个库,不过……

django-rest-swagger-readme
> another help-wanted project…

使用现成框架的好处是语言表达力极强,
最终我们实现一个“解密微信提供的手机号”接口的业务代码大概如下:

class WeChatVS(BaseVS):

    @with_response(empty=True)
    @with_request(DecryptionSiri)
    @action(methods=['post'], detail=True, url_path='decryption/phone')
    def decrypt_wechat_phone(self, request, uid):
        """ 解密并修改用户的手机号
        - https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
        """
        self.check_account_request(request, uid)
        openid, encrypted_data, initial_vector = self.request_data
        phone = WeChatManager(openid).decrypt_safely(encrypted_data, initial_vector)
        # TODO(ldsink): 找产品问一下外国手机号怎么处理
        hutils.check_error(not hutils.is_chinese_phone(phone), 'o(╥﹏╥)o 目前只支持国内的手机号')
        self.account.modify(phone=phone)
        return self.empty_response()

这样的十行代码包含了文档、外链、错误检查、写库,
让写业务代码本身也有种施法的快感。

worker

最开始服务器上我们跑的是 django+uwsgi 的普通模式,
wrk 去压一个小接口,
测试环境 4G 内存的机器 QPS 只有 40 左右。
后来加上了 gevent, monkey patch 一下,改到了协程模式
同样的接口同样的机器 QPS 上升到了大概 600。
调优一下效果会更好。
(需要更高性能的业务可能就根本不用 python 了 Orz)

celery 充当了我们的定时任务+异步任务框架,
我们也拆分了 读写密集型/计算密集型 的两类队列以处理不同的事情。
对于业务中的即时通知部分,
我们用了 channels 库来实现 web socket 的功能。

对于这些大型的框架,其实我们选择余地并不大。
比如虽然 django 开发者有说在 3.0 会考虑大幅度重写异步调用,
channels 项目会逐渐弃坑……
但毕竟 perfectionists with deadlines.
不能说人家功能不完美,我们就不干活了嘛…

database

我们用到的数据库也是 mysql/mongo/redis 这御三家,
所以就是每个选取对应的连接库就是了。

值得一提的是在单元测试里,
我们用 sqlite(in-memory) 替代了 mysql 数据库。
sqlite 里缺失了 mysql 的函数的问题,
也可以用 connection.create_function 的方法来规避掉。

在上线时,还有一个很好玩的东西是 database migration
这个基本上跟“给行驶中的火车换轮子”一样刺激。
详细的细节以后会专门开篇文章讲一下(挖坑预警),
从结果上来说我们做到的是利用 Django Migration 做到数据库结构变更全兼容

python

上面一小节中,我们基本上是走马观花地过完了 django 相关的三方库。
到了真正用 python 开发的时候,
我们遇到的更多的是框架之外的奇遇。

每门语言都有自己的味道。
我很喜欢 python 的一点是:
这门语言有着非常强的表达张力。
就像上面举的那段典型业务代码一样,
在实际的开发中,
python 是能完美表达开发者心中所想的。

但假如开发者自己都没想清楚自己要写啥,
这就有点不妙了。

所以我们有一系列的开发工具来保持清醒。

stay-awake
> 强制清醒.jpg

quality

hax-principles
> 贺师俊在《如何引导程序员新人按正确的流程开发?》下面一段我很欣赏的回答

除了开发流程上的类似要求,
我们对代码本身也执行了类似的严格要求:

  • 单元测试覆盖率必须得在 96% 以上 (unittest/pytest/coverage)
  • 代码的逗号、换行、引号的使用都必须符合规范 (flake8)
  • 代码强制经过 linter 检验,禁止多种黑魔法 (pylint)
    • 代码的各个模块之间必须符合特定的拓扑顺序 (pylint-topology)
  • 代码风格(如字典的复制、长列表、换行与空行)强制统一 (yapf)

其中单元测试覆盖率必须得在 96% 以上值得被单独拎出来表扬一下。

业务代码的 96% 的覆盖率是什么概念呢?
这意味着代码里只有那种真正的边缘情况是没被测到的。
(比如为了兼容微信 SYSTEM ERROR -1000 写的代码)

为了达到了这么高的覆盖率,
我们也专门强化过单元测试的表达力,
比如一段测试创建用户接口的代码可能如下:

def test_create_account(self):
    """ 测试创建用户 """
    with self.assert_model_increase(Account, delta=1):
        response = self.client.post(self.account_url(), {'username': 'hulucc'})
        self.ok(response, username='hulucc', tags__length=0)
    with self.assert_model_increase(Account, delta=0):
        response = self.client.post(self.account_url(), {'username': 'hulucc'})
        self.bad_request(response, message=AccountErrors.DUPLICATE.value)

gitlab-ci-sample
> 所有这些限制都在 CI 中检查了,不通过的话是不让 merge into master 的

developing

我们的合作方式是用 gitlab 作为代码托管平台。
为了团队的开发效率,
我们还自己写了个小机器人来处理各种如分支合并、有效性检查、贴标签之类的杂活。

gitlab ci 不仅被用来做开发阶段的质量保证,
最终我们的构建上线也走的是 gitlab ci (以前我们用的是 jenkins)

gitlab-pr-sample

对于 python 的依赖管理,
我们用的是 pipenv,
pip list 一下大概有了 181 个库。
(关于 pipenv 的介绍可以参见《Python 依赖管理的未来 - ldsink》)

也因为我们线上用的是 docker,
所以不想装依赖的也可以直接用 docker 的环境开发。

deploy

部署这一块我们暂时还没上 k8s,
目前走的是 gitlab ci 中调用 fabric + aws(boto3) 直接操作裸 docker 的方式。
aws 的负载均衡器提供了基础的流量切换服务,
我们也是借用了现成的服务达到灰度发布、无缝发布的效果。

gitlab-ci-deploy
> 用 GitLab CI 部署的步骤图

conclusion

至此,本文介绍了一遍我们在 Python 业务后端的实践。

对于高可用、容器化、数据库等屠龙技,
业界其实有非常多的探讨,
大家也很容易找到现成的文章。

但具体到业务后端的工程化实践,
能借鉴的大型项目并不多。
我读过的也只有两年前的 reddit 代码sentry 这个 django 项目符合要求了。

总的来说,我们用 django 在开发中遵循的约定跟共识有这些:

  • 做正确的事情。
    • 比如我们在讨论过后,一致觉得“线性的 Commit 历史是最干净的”,从当天开始我们的 Commit 历史就是干净的线性历史了。
  • 自动化一切能自动化的工作。
    • 用 swagger 自动化生成文档,用 gitlab ci 自动化质量保证。
  • 尽可能使用最新的特性,让代码时刻保持崭新。
    • 我们每隔一阵就会把所有依赖升到最新的稳定版。
    • 不过因为这个我们也踩了不少坑。
    • 不少时候三方库会引入全新的用法,动辄改动 100+ 的文件数。
    • 这个时候就到了 Vim Macro 展现魔法的时候了。

假如你也在用 Django 作为后端框架的话,
不防尝试一下上面提到的各类工具,
绝对物超所值噢 :)