在Nylas,我们喜欢使用Python进行开发。它的语法简单并富有表现力,拥有大量可用的开源模块和框架,而且这个社区既受欢迎又有多样性。我们的后台是纯用 Python 写的,团队也经常在 PyCon 和 meetups 上演讲。你可以认为我们是 Python 的超级粉。

然而,Python 的一个大缺陷是没有一个明确的工具来部署 Python 服务端应用。工作的情况就像是“执行 git 的 pull 命令后剩下的就只有祈祷了”,但这并不是一个好的方式,尤其当用户依赖于我们的应用。当你的应用引用了很多仍在变化的依赖时,这会让 Python 的部署工作变得更加复杂。下面 HN 上的评论概括了 Python 部署的糟糕情况。

 

为什么这么多年了,仍没有一个有效的办法帮我将 Python 编写的软件转换成 deb 格式?

—— 来自一个受挫的 HN 用户

在 Nylas,我们开发了一种更好的方法可以将 Python 代码连同其依赖一起部署,使得我们能够轻松地对轻量级包进行安装、升级或者删除。该方式的实现并不需要将我们的整个栈迁移到像 Docker、 CoreOS 或者AMIs 这样的系统上。

新手才用 GIT 和 PIP 进行 Python 部署

Python 提供了丰富的模块。不管你搭建的是一个web 服务器还是机器学习分类器,总会有一个合适的模块帮你启动项目。现在获取这些模块的标准方法是通过 pip 从 Python 包索引 (亦称 PyPI)中下载和安装。这就跟 apt,yum,rubgem 等命令操作是一样。

大多数人搭建开发环境的第一步就是用 git 克隆份代码,然后通过 pip 安装其依赖。这也是大多数人第一次尝试部署代码的做法。部署脚本大致如下:

1234 git clone https://github.com/company/somerepo.gitcd /opt/myprojectpip install r requirements.txtpython start_server.py

但是当部署大量生产服务时,这种策略有下面几个原因可能导致失败:

PIP 并没有提供“部署回滚”策略

pip unistall 并不是每次都能正常工作,也没有办法“回滚”到上一个状态。虽然可以用Virtualenv 实现,但它并不是用来管理历史环境的。

用 pip 安装依赖会让部署变得极其慢

使用pip install 来安装一个由C语言编写的模块经常需要从源码进行编译,对于一个新建立的virtualenv环境,这将花费几分钟的时间。但部署应用应该是一个以秒计算的快速而轻量的过程。

在每台主机上分别构建代码会有一致性问题

当你使用 pip 部署时,无法保证不同服务器上运行的应用版本是一致的。构建过程或现有依赖中的错误导致的不一致,这是很难去调试的。

如果 PyPI 或者你的 git 服务器挂掉会导致部署失败

pip installgit pull 通常依赖于外部服务器。你可以选择使用第三方平台 (如 Github,PyPI)或者自己搭建服务器,重要的是确保部署过程满足正常运行时间和规模的预期。当扩展自己的基础设施,尤其是在大型部署时,外部服务往往是第一个挂掉的。

如果你在运营一个人们依赖的应用系统,而且它又是部署在多个服务器上,那么采用 git + pip 部署策略只会让你更加头痛。我们需要的部署策略应该是快速的、一致而且可靠的。更具体地说:

  1. 能够构建代码成单一、有版本控制的工件
  2. 受版本控制的工件可以进行单元测试和系统测试
  3. 一种简单的机制可以从远程主机完全的安装或卸载工件

有了这三样东西可以让我们将更多的时间花在功能的构建上,以更少的时间进行代码一致性迁移。

“干脆用DOCKER吧”

初看下,这似乎最适合用 Docker 了,Docker 是当前盛行的容器管理工具。在一个 Dockerfile里,只要简单地添加代码仓库的引用,安装必要的库和依赖。那么我们就建好 Docker 镜像,将它作为受版本控制的工件发往到远程主机。

然而,当我们尝试这样操作时遇到了几个问题:

  • 我们的内核版本 (3.2)并不完美支持 Docker,如果为了更快的迁移代码而升级内核,我们觉得这有点像在用牛刀杀鸡。
  • 在专用网络里分发 Docker 镜像要有一个独立服务,这样我们又需要对服务进行配置,测试和维护。
  • 将 ansible 自动化安装转换成 Dockerfile 的过程是痛苦的,需要对日志配置、用户权限和秘钥管理等进行大量繁琐的编辑。

即便我们顺利的解决了这些问题,为了调试生产上的问题,我们的工程师团队又不得不学习如何跟 Docker 连接交互。我们认为更快地迁移代码并不应该重新执行整个基础架构自动化和编排层。所以我们继续调查其他方案。

PEX

PEX 是 Twitter 开发的一个智能工具,它允许 Python 代码以可执行压缩文件进行传送。这是一个很酷的想法,关于这个主题我们建议去看Brian Wickman 在推特大学的演讲。

设置 PEX 比 Docker 还简单,因为他只需要运行生成好的可执行压缩文件,但是构建 PEX 文件却是一项巨大的工程。我们在构建第三方库需求时遇到问题,特别是当中包含了静态文件。我们也遭遇到 PEX 源代码产生的混乱的堆栈跟踪,使得调试工作变得更加困难。这是个异份子,因为我们主要目标是改善工程效率,让事情更加易懂。

使用 Docker 会增加运行时的复杂度。而 PEX 会增加构建时的复杂度。我们需要一个方案可以最小化整体复杂度,同时提供可靠的部署,所以我们继续调查其他方案。

包:原始的“容器”

几年前,Spotify 悄悄发布了一个工具叫 dh-virtualenv,你可以用来构建内含 virtualenv 的 debian 包。我们觉得这很有意思,也已经有了很多 Debian 相关经验并在生产环境上运行。(Christine,我们的联合创始人之一,就是一个 Debian 的开发者。)

dh-virtualenv可以很简单的创建一个 debian 包,它包含了 virtualenv 和罗列在requirement.txt 文件里的所有依赖。当在主机上安装这个 debian 包时, 它会将 virtualenv 放置在/usr/share/pyton/路径下。就是它了。

这就是我们在 Nylas 上部署代码的关键。我们的持续集成服务器 (Jenkins) 运行 dh-virtualenv 来构建包, 用 Python 的wheel 缓存来避免对依赖重新构建。这就创建一个捆绑式工件(debian 包),然后对其进行大量的单元测试和系统测试。如果通过测试,则可认为生产上是安全的,可以上传到 s3.

这个过程的关键部分是通过均衡 Debian 的内置包管理器 dpkg,我们可以最小化部署脚本的复杂度。部署脚本大致如下:

1234 temp=$(mktemp /tmp/deploy.deb.XXXXX)curl https://artifacts.nylas.net/syncengine3k48dls.deb o $tempdpkg i $tempsv reload syncengine

要回滚的话,我们只需要部署上一版本的工件。dpkg 工具可以免费帮你清除旧代码。

这个策略最重要的一方面是它实现了一致性和可靠性,同时匹配了我们的开发环境。我们的工程师已经在用virtualenv,而dh-virtualenv 只是一个将其迁往远程主机的方式。如果我们选择 Docker 或者 PEX,明显需要改变我们本地开发的方式又增加了复杂度。我们同样不希望给使用我们开源代码的开发者带来复杂度负担。

现在,我们用 Debian 包来迁移我们所有的 Python 代码。完整构建我们代码库(包含数十个依赖)只要不到2分钟时间,部署更是数秒便完成。

DH-VIRTUALENV 入门

如果你受够了 Python 部署的折磨,那就试试 dh-virtualenv吧。它会是你不错的选择!

配置 Debian 包对于初学者而言是棘手的,所以我们构建了 make-deb 工具来帮你入手。它会基于你 Python 项目里的 setup.py 文件生成 Debian 配置。

首先安装make-deb工具,然后在你项目的根目录下运行它:

123 cd /my/projectpip install makedebmakedeb

如果你的 setup.py 文件有缺失信息的话,make-deb会要求你添加。一旦它收齐所需的信息,make-deb会在你项目的根目录下创建一个 debian 目录,包含了 dh-virtualenv 需要的所有配置。

构建 Debian 包需要你在装有 dh-virtualenv 的 Debian 系统上进行。如果你没有 Debian 环境,我们建议你在 Mac 或者 Windows 上用 Vagrant 和 Virtualbox 安装一个 Debian 的虚拟机。你也可以参考我们放在 Git 仓库下 sync-engine 项目里的Vagrantfile 来作配置。

最后,运行dpkg-builpackage -us -uc来创建 Debian 包。你不需要直接调用 dh-virtualenv,因为它已经在之前make-deb创建好的配置规则里了。当这条命令执行好后,你就构建好一个可部署的漂亮的工件。

一段简单的部署脚本如下:

12345678 scp mypackage.deb remotehost.example.org:ssh remotehost.example.org # Run the next commands on remote-host.example.orgdpkg i mypackage.deb /usr/share/python/myproject/bin/python>>> import myproject # it works!

部署时,你需要将这个工件上传到生产服务器上。运行dpkg -i my-package.deb命令来安装。virtualenv 会被放在/usr/share/python/目录下,所有预设在 setup.py 文件里的脚本文件则会在bin目录里。就是它了!你这就走上了简易部署的光明大道。

总结

在构建大型系统时,项目难点往往是在寻求合适的工具,而非从头开始重构一个新的系统。我们认为使用 Debian 基于包的部署是部署 Python 应用的一个极佳方案,最重要的是它可以帮我们平稳而快速的迁移代码。

请联系我们,如果你有任何的意见或建议,又或者是觉得这篇文章很有意思。感谢你阅读这篇文章!