你可能有充分的理由去搭建一个私有的 Python 包索引,主要原因可以归结为以下三点:
- 官方的 Python 包索引(PyPI)并不提供任何服务可用性保障。它由 Python 软件基金会维护,主要依赖于社区捐赠来维持运行。这意味着在极端情况下,服务有可能中断甚至关闭。如果你的部署流程高度依赖 PyPI,一旦其不可用,整个发布过程可能会被迫中止。
- 即使你的代码是闭源且不对外发布的,将 Python 编写的可复用模块进行标准化打包依然具有显著优势。通过集中管理这些组件,不同项目之间无需重复引入(vendoring),可以直接从内部索引安装。这不仅减少了代码冗余,也极大简化了跨团队协作时的维护成本,有助于提升整体开发效率。
- 采用 setuptools 对整个应用进行打包是一种被广泛认可的最佳实践。完成打包后,新版本的部署变得极为简便——只需执行一条命令:
pip install --upgrade my-application即可完成更新。
使用 PyPI 镜像缓解依赖风险
为了降低因 PyPI 故障带来的影响,可以让包管理工具从某个镜像源下载依赖。事实上,官方索引已经通过内容分发网络(CDN)进行加速,具备一定的镜像能力。然而,在某些时段仍可能出现无法下载包的情况。此时切换到非官方镜像看似可行,但存在潜在的安全隐患,比如包被篡改或注入恶意代码。
更安全、可控的解决方案是建立属于自己的 PyPI 镜像服务,仅同步你需要的公共包。由于该服务完全由你掌控,因此可用性和响应速度都能得到更好保障。当外部服务宕机时,你也无需等待第三方恢复。目前由 PyPA 推荐并维护的镜像工具是 bandersnatch,它可以完整复制整个 PyPI 的内容。你可以在 .pypirc 文件的 repository 段落中配置 index-url 来指向这个本地镜像(如前文所述)。需要注意的是,bandersnatch 仅支持下载同步,不支持上传功能,也没有网页界面。
此外还需注意:完整的 PyPI 镜像可能占用数百 GB 甚至更多的磁盘空间,并且数据量会随时间持续增长,资源开销较大。
超越基础镜像:选择更灵活的方案
不过,是否真的需要全量镜像所有公开包?大多数项目即便拥有上百个依赖,也只是整个生态的一小部分。更重要的是,bandersnatch 不支持上传私有包,这一限制使其在企业环境中实用性受限。考虑到高昂的存储成本和有限的功能扩展,它的投入产出比往往不高。
对于大多数实际场景而言,更好的选择是使用 devpi。这是一个与 PyPI 兼容的包索引实现,具备以下核心功能:
- 支持创建私有索引,允许上传内部使用的非公开包;
- 提供智能镜像机制,按需缓存远程包。
与 bandersnatch 不同,devpi 并不会一开始就拉取全部包数据。它的镜像策略是“按需获取”:当 pip、setuptools 或 easy_install 请求某个包时,若本地不存在,devpi 会自动从上游索引(通常是 PyPI)下载并缓存,随后再提供给客户端。此后,系统还会定期检查已缓存包的更新情况,确保本地镜像保持最新状态。
这种机制虽然存在极小概率在首次请求新包而上游又恰好不可用时失败,但在实际部署中影响微乎其微——因为生产环境通常只会使用那些已经被缓存过的稳定依赖。而对于已缓存的包,其版本状态始终与 PyPI 保持一致,新版本也会自动同步。这种设计在可靠性与资源消耗之间取得了良好平衡。
以包形式进行应用部署的优势
现代 Web 应用通常依赖大量第三方库,将其正确部署到远程主机涉及多个复杂步骤。典型流程包括:
- 创建独立的虚拟运行环境;
- 将项目代码传输至目标主机;
- 根据 requirements.txt 安装最新的依赖项;
- 执行数据库结构的同步或迁移操作;
- 收集静态资源文件(如图片、CSS、JS)并放置到指定目录;
- 编译多语言环境下的本地化文件。
对于结构更复杂的前端项目,还可能涉及以下额外处理:
- 利用 SASS 或 LESS 预处理器生成 CSS 样式表;
- 对 JavaScript 和 CSS 文件进行压缩、混淆或合并以优化性能;
- 将 CoffeeScript、TypeScript 等高级语言编译为标准 JavaScript;
- 预处理模板文件,例如进行 HTML 压缩或内联样式处理。
这些流程可以通过 Bash 脚本、Fabric 或 Ansible 等自动化工具实现编排。然而,在远程生产服务器上实时执行这些构建任务并非理想做法,原因如下:
- 一些常见的静态资源处理工具(如 Webpack、Sass 编译器)在运行时会消耗大量 CPU 和内存资源,容易对正在运行的服务造成干扰;
- 构建过程中的错误可能导致部署中断,增加运维风险;
- 每次部署都重新构建相同资产会造成资源浪费,降低效率。
因此,推荐的做法是在 CI/CD 流水线中提前完成所有构建工作,并将最终产物打包为一个自包含的 Python 包。这样,部署就简化为一个可靠、快速且可重复的安装过程,极大提升了系统的稳定性与可维护性。
这些工具可能会对应用运行的稳定性造成破坏。 在大多数情况下,这类工具依赖额外的系统组件,而这些组件并非项目正常运作所必需。它们往往引入了诸如 JVM、Node 或 Ruby 等附加运行环境,不仅增加了配置管理的复杂度,也显著提升了整体维护成本。 当需要将应用部署到大量服务器(如几十、数百甚至上千台)时,重复性工作会急剧增加。虽然在自有基础设施上部署可能不会带来明显的成本上升,尤其是在低流量时段进行操作的情况下,但如果使用的是按需计费的云服务,特别是那些根据负载峰值或执行时间收费的平台,那么由此产生的额外开销可能相当可观。 此外,许多部署步骤本身耗时较长。例如,在远程服务器上传代码过程中,最令人担忧的情况莫过于因网络波动导致连接中断。因此,尽可能缩短部署流程的时间,有助于降低此类失败发生的概率。 出于显而易见的原因,上述这些部署过程中的中间产物不应被纳入应用的代码仓库中。每个版本发布都包含一些必须执行的操作,这一点无法避免。尽管如此,这些任务非常适合自动化处理——关键在于选择合适的时机和位置来实施。 像静态资源收集、代码预处理或资产编译等工作,通常可以在本地开发环境或专用构建环境中完成。这样一来,实际部署到远程服务器的代码包几乎无需再进行现场处理,极大提高了效率与可靠性。 在构建发行版本或安装包的过程中,以下几个步骤尤为关键: - 安装 Python 所需依赖,并将静态文件(如 CSS 和 JavaScript)移动至指定目录。这两项操作均可集成到 `setup.py` 脚本的 `install` 命令中统一处理。 - 预处理前端代码,包括对 JavaScript 超集的转换、资源的压缩/混淆/合并,以及 SASS 或 LESS 文件的编译;同时,还包括文本本地化的编译工作(例如 Django 中的 `compilemessages` 命令)。这些都可以作为 `setup.py` 脚本中 `sdist` 或 `bdist` 命令的一部分自动执行。 [此处为图片1] 通过一个合理的 `MANIFEST.in` 文件,可以轻松管理除 Python 代码外的其他预处理资源。建议始终在 `setuptools` 包中 `setup()` 函数调用的 `install_requires` 参数里明确列出所有依赖项。 当然,实现完整的应用打包需要投入一定的额外工作量,比如定义自定义的 setuptools 命令或重写现有命令。但这种做法带来的优势是显著的:它能够使项目的部署过程更加快速、稳定且可重复。 接下来我们以一个基于 Django 框架的项目为例(使用 Django 1.9 版本),说明如何实践这一思路。选择该框架是因为它在同类产品中具有较高的流行度,读者很可能已经对其有所了解。 在此类项目中,典型的目录结构如下所示:$ tree . -I __pycache__ --dirsfirst . ├── webxample │ ├── conf │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── locale │ │ ├── de │ │ │ └── LC_MESSAGES │ │ │ └── django.po │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ └── django.po │ │ └── pl │ │ └── LC_MESSAGES │ │ └── django.po │ ├── myapp │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── static │ │ │ ├── js │ │ │ │ └── myapp.js │ │ │ └── sass │ │ │ └── myapp.scss │ │ ├── templates │ │ │ ├── index.html │ │ │ └── some_view.html │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── __init__.py │ └── manage.py ├── MANIFEST.in ├── README.md └── setup.py 15 directories, 23 files需要注意的是,此结构与标准 Django 项目模板略有不同。默认情况下,存放 WSGI 应用、配置模块和 URL 路由的包名通常与项目名称一致。但由于我们采用打包部署策略,故将其命名为 `webxample`。这容易引起混淆,因此更佳的做法是将其重命名为 `conf`。 本文不深入探讨具体实现细节,仅做若干基本假设。 本示例应用包含若干外部依赖,主要包括两个常用的 Django 扩展包:`djangorestframework` 和 `django-allauth`,以及一个非 Django 相关的运行依赖:`gunicorn`。
项目中使用了 djangorestframework 和 django-allauth,这两个组件通过 webexample.webexample.settings 模块中的 INSTALLED_APPS 进行注册和启用。
应用支持多语言环境,已实现德语、英语与波兰语的本地化。为避免版本库臃肿,我们选择不将编译后的 gettext 消息文件(即 .mo 文件)提交至代码仓库。
[此处为图片1]
在样式处理方面,由于团队对传统 CSS 语法存在偏好上的抵触,因此采用了功能更加强大的 SCSS 作为开发语言,并借助 SASS 工具将其编译为标准 CSS 输出。
了解上述项目结构后,我们可以着手编写 setup.py 脚本,利用 setuptools 实现以下自动化任务:
- 将位于
webxample/myapp/static/scss目录下的 SCSS 源文件进行编译输出为 CSS。 - 在
webexample/locale目录下,自动将 .po 格式的翻译文件编译成 Django 可读的 .mo 格式。
为了提升部署效率,我们需要为 Python 包配置一个入口脚本,从而能够通过自定义命令完成操作,而非依赖传统的 manage.py 方式执行管理任务。
幸运的是,Python 社区提供了 libsass 的绑定——这是 SASS 引擎的一个 C/C++ 实现版本,且已集成支持 setuptools 与 distutils。仅需少量配置即可启用 setup.py 中的自定义命令来运行 SCSS 编译流程。示例如下:
from setuptools import setup
setup(
name='webxample',
setup_requires=['libsass >= 0.6.0'],
sass_manifests={
'webxample.myapp': ('static/sass', 'static/css')
},
)
通过该配置,开发者只需执行 python setup.py build_scss 命令,即可完成 SCSS 到 CSS 的转换,无需手动调用 sass 命令或在脚本中启动子进程处理。
然而这仍不足以满足我们的目标。虽然它简化了部分工作,但我们期望整个发布流程可以完全自动化,实现一键构建新版本。为此,必须对 setuptools 默认的分发命令进行扩展与重写。
以下是一个增强版的 setup.py 示例,用于整合多个构建步骤:
import os
from setuptools import setup
from setuptools import find_packages
from distutils.cmd import Command
from distutils.command.build import build as _build
try:
from django.core.management.commands.compilemessages import Command as CompileCommand
except ImportError:
# 注意:在安装过程中 Django 可能尚未可用
CompileCommand = None
# 设置必要的环境变量
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webxample.conf.settings")
class build_messages(Command):
"""自定义命令,用于编译 Django 项目中的 gettext 国际化消息"""
description = "compile gettext messages"
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
if CompileCommand:
CompileCommand().handle(verbosity=2, locales=[], exclude=[])
else:
raise RuntimeError("could not build translations")
class build(_build):
"""覆写默认的 build 命令,添加额外的构建步骤"""
sub_commands = [
('build_messages', None),
('build_sass', None),
] + _build.sub_commands
setup(
name='webxample',
setup_requires=[
'libsass >= 0.6.0',
'django >= 1.9.2',
],
install_requires=[
'django >= 1.9.2',
'gunicorn == 19.4.5',
'djangorestframework == 3.3.2',
'django-allauth == 0.24.1',
],
packages=find_packages('.'),
)
此方案实现了构建过程的高度集成:在执行标准构建时,会自动触发消息编译和样式转换,确保最终打包内容完整且一致。
sass_manifests = {
'webxample.myapp': ('static/sass', 'static/css')
},
cmdclass = {
'build_messages': build_messages,
'build': build,
},
entry_points = {
'console_scripts': [
'webxample = webxample.manage:main'
]
}
通过上述配置,仅需一条终端命令即可完成所有静态资源的构建,并生成 webxample 项目的源代码发行包。执行命令如下:
python setup.py build sdist
若组织内部已搭建私有包索引(例如使用 devpi),可进一步结合 install 子命令进行本地安装,或借助 twine 工具上传,使团队成员能通过 pip 直接安装该包。
查看由 setup.py 构建出的源码发行版目录结构,可以发现其中已包含编译后的 gettext 多语言消息文件以及由 SCSS 编译生成的 CSS 样式表。解压并浏览内容示例如下:
tar -xvzf dist/webxample-0.0.0.tar.gz 2> /dev/null
tree webxample-0.0.0/ -I __pycache__ --dirsfirst
输出结构为:
webxample-0.0.0/
├── webxample
│ ├── conf
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── locale
│ │ ├── de
│ │ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ │ └── pl
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
│ ├── myapp
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── static
│ │ │ ├── css
│ │ │ │ └── myapp.scss.css
│ │ │ └── js
│ │ │ └── myapp.js
│ │ ├── templates
│ │ │ ├── index.html
│ │ │ └── some_view.html
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── __init__.py
│ └── manage.py
├── webxample.egg-info
│ ├── PKG-INFO
│ ├── SOURCES.txt
│ ├── dependency_links.txt
│ ├── requires.txt
│ └── top_level.txt
├── MANIFEST.in
├── PKG-INFO
├── README.md
├── setup.cfg
└── setup.py
16 directories, 33 files
[此处为图片1]
此方法的一个显著优势在于,能够为项目自定义命令入口点,替代 Django 默认的 manage.py 调用方式。配置完成后,可以直接使用 entry_points 中定义的命令来运行各类 Django 管理操作,例如:
webxample migrate
webxample collectstatic
webxample runserver
为实现这一功能,需对原有的 manage.py 脚本稍作调整,确保其与 setup() 中 entry_points 的调用机制兼容。具体做法是将原脚本的核心逻辑封装进 main() 函数中,修改后的代码结构如下:
#!/usr/bin/env python3
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webxample.conf.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
需要注意的是,目前许多 Web 框架(包括 Django)在设计之初并未充分考虑以标准 Python 包形式进行完整项目打包的需求。因此,这种集成化构建与分发的方式虽然强大,但仍需开发者手动补充部分配置和脚本支持。将正在进行的项目重构为一个可发布的包,往往需要进行大量的调整。特别是在使用 Django 框架时,通常涉及重新组织大量隐式导入语句,并对配置文件中的多项设置进行修改。
[此处为图片1]
另一个值得关注的问题是通过 Python 打包机制生成版本时的一致性问题。当多个团队成员都有权限构建应用的发布版本时,确保构建过程在统一且可复现的环境中执行变得尤为关键,尤其是在项目包含较多静态资源预处理步骤的情况下。即便基于相同的源码,若在不同的系统环境中打包,最终产物仍可能存在差异——这通常是由于构建工具的版本不一致所导致。为解决这一问题,推荐的做法是将打包与发布流程交由持续集成/持续交付(CI/CD)系统来完成,例如 Jenkins 或 Buildbot。这样不仅能保证环境一致性,还能确保每次发布前都完整运行了必要的测试套件。此外,自动化部署也可以被整合进该流程中,进一步提升效率和可靠性。
尽管利用 setuptools 将代码封装成 Python 包并非完全零门槛,过程可能涉及一定复杂度,但其带来的部署简化效果显著,因此非常值得投入实践。值得注意的是,这种做法也契合“十二要素应用”中第六条原则的核心建议:以一个或多个无状态进程来运行应用。


雷达卡


京公网安备 11010802022788号







