如果允许安装工具从 PyPI 的一个镜像下载包,那么就可以缓解 PyPI 故障带来的问题。 事实上,官方的 Python 包索引已经通过内容分发网络(Content Delivery Network,CDN) 提供服务,因此它是自带镜像的。但这无法改变下列事实:似乎不时会有一些糟糕的日子, 下载一个包的任何尝试都会失败。对于这种情况,使用非官方的镜像也不是解决办法,因 为可能会引起一些安全问题。

最好的解决方案是使用你自己的 PyPI 镜像,里面包含所有你需要的包。只有你会使 用这个镜像,才更容易确保其可用性。另一个优点是,每当这项服务停机时,你不需要 依靠其他人来启动它。由 PyPA 维护并推荐的镜像工具是 bandersnatch。它允许你制作 Python 包索引全部内容的镜像,你可以在.pypirc 文件中 repository 区段的 index-url 选项中进行设置(正如上一章所述)。这个镜像不接受上传,并且没有 PyPI 的 web 部分。 无论怎样一定要小心!完整的镜像可能需要数百 GB 的存储,其大小将随着时间的推移而 持续增长。

 但如果我们有更好的选择,为什么要止步于简单的镜像呢?你几乎不可能需要整个包

160 第6章 部署代码

索引的镜像。即使对于一个有上百个依赖的项目,也只是所有可用包的一小部分。此外, 无法上传自己的私有包,也是这种简单镜像的一大限制。使用 bandersnatch 的代价这么大, 而附加的价值似乎却非常小。而且对大多数情况来说都是这样。如果只需要为几个项目中 的一个维护包镜像,那么更好的方法是使用 devpi。它是与 PyPI 兼容的包索引实现,可以 提供:

● 上传非公开包的私有索引;

● 索引镜像。

与 bandersnatch 相比,devpi 的主要优点在于它处理镜像的方式。它当然可以对其他索

引制作一般完整的镜像,就像 bandersnatch 所做的那样,但这并不是它的默认做法。它维 护客户端已经请求的包组成的镜像,而不是对整个仓库进行代价高昂的备份。因此,每当 安装工具(pip、setuptools 和 easyinstall)请求一个包时,如果它不在本地镜像 中,那么 devpi 服务器将会尝试从镜像索引(通常是 PyPI)中下载并提供。一旦包下载完 成之后,devpi 将定期检查其更新,以保持镜像的最新状态。

如果你请求一个尚未制作镜像的新包,且上游包索引出现了故障,那么镜像方法有很 小的可能性会失败。不管怎样,由于在大多数部署中,你将会仅依赖在索引中已经存在镜 像的包,因此这一可能性很小。已经请求过的包的镜像状态与 PyPI 保持完全一致,新版本 将会自动下载。这似乎是一个非常合理的权衡。

6.3.2 使用包进行部署

现代 Web 应用都有大量依赖,通常需要很多步骤才能在远程主机上正确安装。例如, 对于远程主机上的新版本应用来说,典型的引导过程包括以下步骤。

● 创建新的虚拟隔离环境。

● 将项目代码移动到执行环境。

● 安装最新的项目需求(通常来自于 requirements.txt 文件)。

● 同步或迁移数据库模式。

● 从项目源代码和外部包中收集静态文件并放在所需位置。

● 为不同语言的应用编译本地化文件。 对于更复杂的网站,可能还有许多额外的任务,主要和前端代码有关:

● 使用 SASS 或 LESS 等预处理器生成 CSS 文件。

● 执行静态文件(JavaScript 和 CSS 文件)的压缩、混淆和/或合并。

● 将 JavaScript 超集语言(CoffeeScript、TypeScript 等)编写的代码编译为原生 JS。

● 预处理响应模板文件(压缩、样式内联等)。

利用 Bash、Fabric 或 Ansible 等工具可以将所有这些步骤轻松自动化,但在应用的安

6.3 你自己的包索引或索引镜像 161

装过程中,在远程主机上完成所有这些步骤并不是一个好主意,其原因如下。

● 有些处理静态资产的常用工具可能会占用大量的 CPU 或内存。在生产环境中运行

   这些工具可能会破坏应用运行的稳定性。

● 这些工具通常需要额外的系统依赖,而项目的正常运行可能并不需要它们。它们大

多是额外的运行环境,例如 JVM、Node 或 Ruby。这增加了配置管理的复杂性,

也增加了总的维护成本。

● 如果你要将应用部署到多个服务器(几十、成百、上千),那么你就是在重复大量

工作,而这些工作本来只需要做一次就可以。如果你有自己的基础设施,那么你可 能不会遇到成本的巨大增长,特别是如果你在低流量时段进行部署。但如果你运行 定价模式的云计算服务,并且它对负载峰值或一般的执行时间额外收费的话,那么 额外成本可能会相当高。

● 大多数步骤只是需要大量时间。你在远程服务器上安装代码时,你最不希望的事情就 是由于网络问题导致连接中断。保持部署过程快速完成,可以降低部署中断的概率。 由于显而易见的原因,上述部署步骤的结果不能包含在你的应用代码仓库中。简单来 说,每个版本都有一些必须要做的事情,你不能改变这一点。这显然适合使用自动化,但

应该在正确的地方和正确的时间进行。 类似静态收集和代码/资产预处理之类的大多数事情都可以在本地或专用环境中完成,

所以部署到远程服务器的实际代码只需要最少的现场处理。在构建一个发行版或安装一个 包的过程中,最值得注意的部署步骤如下。

● 安装 Python 依赖,并将静态资产(CSS 文件和 JavaScript)移动到所需位置,这两 个步骤都可以作为 setup.py 脚本 install 命令的一部分来处理。

● 预处理代码(预处理 JavaScript 超集、资产的压缩/混淆/合并、运行 SASS 或 LESS) 与诸如将文本编译本地化(例如 Django 中的 compilemessages)之类的操作, 都可以作为 setup.py 脚本 sdist/bdist 命令的一部分。

利用正确的 MANIFEST.in 文件,可以轻松处理除 Python 之外的预处理代码。当然, 最好在 setuptools 包中 setup()函数调用的 install_requires 参数中给出依赖。 当然,将整个应用打包需要一些额外的工作,例如提供你自己的自定义 setuptools 命令或者覆写现有的命令,但它有许多优点,可以让项目部署变得更加快速、更加可靠。

我们用一个基于 Django 的项目(Django 1.9 版)作为例子。我之所以选择这个框架, 是因为它似乎是同一类型中最流行的 Python 框架,所以你很可能对它已经有所了解。在这 样的项目中,典型的文件结构可能如下所示: