PEP 381 – PyPI 的镜像基础设施
- 作者:
- Tarek Ziadé <tarek at ziade.org>, Martin von Löwis <martin at v.loewis.de>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2009 年 3 月 21 日
- 发布历史:
摘要
本 PEP 描述了 PyPI 的镜像基础设施。
PEP 撤回
主要的 PyPI Web 服务于 2013 年 5 月迁移到了 Fastly 缓存 CDN 后面:https://mail.python.org/pipermail/distutils-sig/2013-May/020848.html
随后,这种安排以实物捐赠的形式与 PSF 正式化,并且 PSF 还承担了在该捐赠安排可能停止时管理风险的任务。
之前直接在 PyPI 上提供的下载统计数据,现在通过 Google Big Query 间接发布:https://packaging.pythonlang.cn/guides/analyzing-pypi-package-downloads/
因此,本 PEP 中描述的镜像提案不再需要,并被标记为“已撤回”。
基本原理
PyPI 托管着超过 6000 个项目,并且人们每天都在使用它来构建应用程序。特别是像 easy_install 和 zc.buildout 这样的系统,它们大量使用 PyPI。
对于大量使用 PyPI 的用户来说,它可能成为一个单点故障。人们已经开始设置一些镜像,包括私有和公共镜像。这些镜像都是主动镜像,这意味着它们会抓取 PyPI 以进行同步。
为了使系统更加可靠,本 PEP 描述了
- 在 PyPI 上进行镜像列表和注册
- 公共镜像应维护的页面。PyPI 将使用这些页面来获取点击次数和最后修改日期。
- 镜像应如何与 PyPI 同步
- 客户端如何实现故障转移机制
镜像列表和注册
想要镜像 PyPI 的人可以在 catalog-SIG 上提出建议。当在邮件列表中提出镜像建议时,经过检查确认符合镜像规则后,会自动添加到 PyPI 应用程序的镜像列表中。
镜像列表以主机名列表的形式提供,格式如下:
X.pypi.python.org
X 的值是序列 a,b,c,…,aa,ab,… a.pypi.python.org 是主服务器;镜像从 b 开始。CNAME 记录 last.pypi.python.org 指向最后一个主机名。镜像操作员应使用静态地址,并提前将计划的地址变更通知 distutils-sig。
新的镜像也会出现在 http://pypi.python.org/mirrors,这是一个人类可读的页面,提供了镜像列表。该页面还说明了如何注册新镜像。
统计页面
PyPI 在 /stats 处提供下载统计数据。此页面由 PyPI 每天计算,读取所有镜像的本地统计数据并求和。
统计数据以每日或每月文件的形式呈现,位于 /stats/days 和 /stats/months 下。每个文件都是 bzip2 文件,格式如下:
- YYYY-MM-DD.bz2 用于每日文件
- YYYY-MM.bz2 用于每月文件
示例
- /stats/days/2008-11-06.bz2
- /stats/days/2008-11-07.bz2
- /stats/days/2008-11-08.bz2
- /stats/months/2008-11.bz2
- /stats/months/2008-10.bz2
镜像真实性
对于分布式镜像系统,客户端可能希望验证镜像的真实性。有多种威胁需要考虑:
- 中心索引可能被攻破
- 中心索引被认为是可信的,但镜像可能被篡改。
- 中心索引与最终用户之间,或镜像与最终用户之间存在中间人攻击,可能篡改数据报。
本规范仅处理第二种威胁。已做出一些规定来检测中间人攻击。为了检测第一种攻击,包作者需要使用 PGP 密钥对他们的包进行签名,以便用户验证包是否来自他们信任的作者。
中心索引在 URL /serverkey 处提供了一个 DSA 密钥,格式为 PEM,由“openssl dsa -pubout”生成(即 RFC 3280 SubjectPublicKeyInfo,算法为 1.3.14.3.2.12)。此 URL **不得**被镜像,客户端必须直接从 PyPI 获取官方的 serverkey,或者使用 PyPI 客户端软件随附的副本。镜像仍应下载密钥,以检测密钥轮换。
对于每个包,在 /serversig/<package> 处提供了一个镜像签名。这是 /simple/<package> 的并行 URL 的 DSA 签名,采用 DER 格式,使用 SHA-1 和 DSA(即根据 RFC 3279 Dsa-Sig-Value,由算法 1.2.840.10040.4.3 创建)
使用镜像的客户端需要执行以下步骤来验证包:
- 下载 /simple 页面,并计算其 SHA-1 哈希值
- 计算该哈希值的 DSA 签名
- 下载相应的 /serversig,并将其(逐字节)与步骤 2 中计算的值进行比较。
- 计算并验证(相对于 /simple 页面)从镜像下载的所有文件的 MD-5 哈希值。
验证算法的实现可从 https://svn.python.org/packages/trunk/pypi/tools/verify.py 获取
从中心索引下载时不需要验证,为减少计算开销应避免。
大约每年,密钥将被替换为一个新密钥。镜像将不得不重新获取所有 /serversig 页面。使用镜像的客户端需要找到一个受信任的密钥副本。一种获取方式是从 https://pypi.python.org/serverkey 下载。为了检测中间人攻击,客户端需要验证 SSL 服务器证书,该证书将由 CACert 机构签名。
镜像需要提供的特殊页面
镜像 PyPI 的一个子集副本,因此它通过复制 PyPI 的结构来提供相同的结构。
- simple: 包索引的 REST 版本
- packages: 包,按 Python 版本和字母存储
- serversig: simple 页面的签名
它还需要提供两个特定的元素:
- last-modified
- local-stats
最后修改日期
CPAN 使用一个新鲜度日期系统,该系统可提供镜像的最后同步日期。
对于 PyPI,每个镜像需要维护一个 URL,其中包含简单的文本内容,表示镜像维护的最后同步日期。
日期以 GMT 时间提供,使用 ISO 8601 格式 [2]。每个镜像将负责维护其最后修改日期。
此页面必须位于:/last-modified,并且必须是 text/plain 页面。
本地统计
每个镜像负责统计通过它完成的所有下载。PyPI 使用此信息来汇总所有下载,以便显示总数。
这些统计数据采用类 CSV 格式,第一行包含标题。它需要遵守 PEP 305。基本上,它应该可以被 Python 的 csv 模块读取。
此文件中的字段是:
- package: 包的 distutils id。
- filename: 已下载的文件名。
- useragent: 下载包的客户端的 User-Agent。
- count: 下载次数。
内容将如下所示:
# package,filename,useragent,count
zc.buildout,zc.buildout-1.6.0.tgz,MyAgent,142
...
计数从镜像启动那天开始,每天有一个文件,使用 bzip2 格式压缩。每个文件的命名方式与日期相同。例如,2008-11-06.bz2 是 2008 年 11 月 6 日的文件。
然后,它们在名为 days 的文件夹中提供。例如:
- /local-stats/days/2008-11-06.bz2
- /local-stats/days/2008-11-07.bz2
- /local-stats/days/2008-11-08.bz2
此页面必须位于 /local-stats。
镜像应如何与 PyPI 同步
Martin v. Loewis 和 Jim Fulton 基于 easy_install 的工作方式,描述并实现了一个名为 Simple Index 的镜像协议。本节对其进行总结,并提供了一些相关链接,以及关于 User-Agent 的小部分内容。
镜像协议
镜像必须减少中央服务器和镜像之间传输的数据量。为实现这一点,它们 **必须** 使用 changelog() PyPI XML-RPC 调用,并且仅重新获取自上次以来已更改的包。对于每个包 P,它们 **必须** 复制文档 /simple/P/ 和 /serversig/P。如果一个包在中央服务器上被删除,它们 **必须** 删除该包及其所有相关文件。为检测包文件的修改,它们 **可以** 缓存文件的 ETag,并 **可以** 使用 If-none-match 头请求跳过它。
每个镜像工具 **必须** 使用描述性的 User-agent 头来识别自身。
pep381client 包 [1] 提供了一个遵循此协议浏览 PyPI 的应用程序。
User-agent 请求头
为了区分客户端在 PyPI 上执行的操作,所有镜像软件都应提供一个特定的用户代理名称。
所有客户端也是如此,例如:
XXX 在 PyPI 上注册用户代理的机制?
客户端如何使用 PyPI 及其镜像
浏览 PyPI 的客户端应该能够使用替代镜像,方法是使用 last.pypi.python.org 获取镜像列表。
代码示例
>>> import socket
>>> socket.gethostbyname_ex('last.pypi.python.org')[0]
'h.pypi.python.org'
到目前为止能够使用此机制的客户端:
- setuptools
- zc.buildout(通过 setuptools)
- pip
故障转移机制
浏览 PyPI 的客户端应该能够在 PyPI 或使用的镜像无响应时使用故障转移机制。
客户端自行决定应使用哪个镜像,可能需要考虑其地理位置和响应能力。
本 PEP 不描述此故障转移机制应如何工作,但强烈建议客户端尝试使用最近的镜像。
到目前为止能够使用此机制的客户端:
- setuptools
- zc.buildout(通过 setuptools)
- pip
额外的包索引
显而易见,有些包不会上传到 PyPI,原因可能是它们是私有的,或者项目维护者运行自己的服务器,用户可以从那里获取项目包。然而,强烈建议公共包索引遵循 PyPI 和 Distutils 协议。
换句话说,register 和 upload 命令应该与任何现有的包索引服务器兼容。
目前兼容 PyPI 和 Distutils 的软件:
额外的包索引不是 PyPI 的镜像,但它本身可以有自己的镜像。
合并多个索引
当客户端需要从多个不同的索引获取一些包时,它应该能够将它们中的每一个作为潜在的包来源。不同的索引应该定义为一个排序列表,供客户端查找包。
每个独立的索引当然可以提供其镜像列表。
XXX 定义如何获取任意索引的镜像主机名。
这使得客户端可以进行所有组合,从而实现一个具有所有隐私级别的可靠打包系统。
合并工作由客户端负责。
参考资料
致谢
Georg Brandl。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0381.rst