天天看点

无停机部署一个 Django 应用

当 healthchecks.io 的流量超过每秒一次访问之后,我就意识到不能随意在部署代码后重启服务了。作为一个监控服务,即使丢掉几个 http 请求也是不应该的。而且,如果服务器变得更加繁忙的话,这个问题只会更加严重。

先简单介绍一下我们所做的工作,这是一个相对简单的 django 实现的 app,由 gunicorn 来运行,前端是 nginx。数据保存在 postgresql 数据库里。gunicorn和另一个额外的后台进程由 supervisor 负责管理。整个服务在单个$20级别的digitalocean实例上运行。

此外,我的技术选型的指导方针是整个架构尽可能的简单,能够使用尽可能长的时间。需要添加的东西,例如负载均衡、数据库容灾、k-v存储、消息队列等等,都要是必须的。另一方面,还需要考虑更多的事情,包括监控、备份等等。同时,对于刚接触这个项目的人来说,需要花更多时间来了解整个系统的“输入和输出”,并且从头开始搭建系统。既需要保持简单、实用,还要保证性能和功能符合预期,这是个不错的挑战。

目前的部署方式是使用 fabric 脚本,以及用于 supervisor 和 nginx 的配置模板。在我的工作机上运行“fab deploy”,fabric 本就会在远端机上完成下面的事情:

fabric 脚本的相关部分参考下面的代码,其中的 virtualenv 上下文管理部分源自优秀的 fabtools 库。

现在,如何消除掉部署的最后一步停止服务的时间呢?我们来加一些前提条件:没有负载均衡(目前)。所有的功能都需要集中在一台机器,而且不能有非 200 的响应码。不过我们可以有一些小小的让步:可以考虑一个稍微简单(一般)的情形,不需要做数据库合并,或者数据库合并是向后兼容的,应用的老版本在数据库合并之后也能工作。

经过观察,我发现应用的某些部分的可用性比其他部分的更重要。特别是被监控的客户端系统需要访问的 api,其重要程度要高于用户需要访问的前端页面。虽然向用户显示错误页面肯定是很糟糕的,但是不丢掉客户端的请求更加重要。丢失的请求可能会导致后续发送不该发送的报警,这显然更加糟糕。

我考虑过使用 amazon api gateway 来处理客户端的 ping 请求,也实现了原型。这需要把 ping 消息放到 amazon sqs 队列里,django 在空闲的时候去消费。这是相对简单的增强可用性和扩展性的方式,不过代价比较大,也带来新的外部依赖。将来需要再考虑一下有没有更好的办法。

另一种方式:把监听客户端的 ping 请求这个功能与 django 应用的其他部分分离开。ping 的监听逻辑非常简单,最终只涉及到两个 sql 操作:一个更新操作和一个插入操作。重写这部分代码应该比较简单,也许可以使用 python的microframeworks,或者也可以不用 python 去实现,甚至还可以在 nginx 里去实现(使用 ngx_postgres 模块)。有意思的是,这里有一段 nginx 的配置,做的就是类似的事情(忽略其中可笑的正则表达式):

简单的说明一下这段配置:当客户端请求并且 url 满足一定规则的时候,服务端会执行 postgresql 查询,返回 http 的 200 或者4 00。这样做性能上也占优,因为请求没有走到 gunicorn、django 和 psycopg2。只要数据库可用,nginx 就可以处理 ping 请求,即使是 django 由于某种原因挂掉了。

不过,这种方式用了一点小伎俩,而且还引入了一些细节,开发者和系统管理员需要了解这些细节。例如,当数据库的 schema 更改时,前面提到的 sql 查询语句也需要更新并测试。另外,ngx_postgres扩展也不是简单的通过“apt-get install”就能安装成功的。

让我们再想一下,也许通过仔细规划进程的重加载,就能实现零宕机时间的目标。

我的脚本里之前使用的是“/etc/init.d/nginx restart”,这是因为我不知道更好的办法。不过现在我知道可以改成 “/etc/init.d/nginx reload”,这样会更优雅一些:

类似的,我的脚本使用“supervisorctl reload”来停止服务、重新加载配置、然后再启动所有的服务。实际上,应该使用“supervisorctl update”来在配置有更新的时候启动、停止和重启服务。

现在,“fab deploy”的工作流程如下:

下面是 fabric 脚本的改进部分,与 supervisor 任务处理相关:

通过这种方式,nginx 可以一直提供服务,总可以与在线的 gunicorn 进程交互。为了验证这点,我写了一个脚本无限循环的请求特定的 url。当遇到非 200 的响应结果时,会打印出相应的错误信息。用这个脚本对测试虚拟机进行压测,期间部署了多次,没有发现有请求被丢掉。成功!

代码部署时保证零宕机有很多种方式,每一种都有其优缺点。例如,把关键部分从一个大的系统里区分出来,这是一个合理的策略。这样每个部分就可以独立进行更新。之后每个部分也可以独立的进行扩展。这种方式的不足之处是需要维护更多的代码和配置。

最终的结果是:

无停机部署一个 Django 应用

强烈推荐:healthchecks.io,它是一个免费的开源的基于 cron 的监控服务。在 cron 里配置好监控只需要几分钟时间,却能让你晚上睡得更好!