Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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_installzc.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

镜像真实性

对于分布式镜像系统,客户端可能希望验证镜像的真实性。有多种威胁需要考虑:

  1. 中心索引可能被攻破
  2. 中心索引被认为是可信的,但镜像可能被篡改。
  3. 中心索引与最终用户之间,或镜像与最终用户之间存在中间人攻击,可能篡改数据报。

本规范仅处理第二种威胁。已做出一些规定来检测中间人攻击。为了检测第一种攻击,包作者需要使用 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 创建)

使用镜像的客户端需要执行以下步骤来验证包:

  1. 下载 /simple 页面,并计算其 SHA-1 哈希值
  2. 计算该哈希值的 DSA 签名
  3. 下载相应的 /serversig,并将其(逐字节)与步骤 2 中计算的值进行比较。
  4. 计算并验证(相对于 /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 上执行的操作,所有镜像软件都应提供一个特定的用户代理名称。

所有客户端也是如此,例如:

  • zc.buildout [3]
  • setuptools [4]
  • pip [5]

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 协议。

换句话说,registerupload 命令应该与任何现有的包索引服务器兼容。

目前兼容 PyPI 和 Distutils 的软件:

  • PloneSoftwareCenter [6],用于运行 plone.org 产品部分。
  • EggBasket [7]

额外的包索引不是 PyPI 的镜像,但它本身可以有自己的镜像。

合并多个索引

当客户端需要从多个不同的索引获取一些包时,它应该能够将它们中的每一个作为潜在的包来源。不同的索引应该定义为一个排序列表,供客户端查找包。

每个独立的索引当然可以提供其镜像列表。

XXX 定义如何获取任意索引的镜像主机名。

这使得客户端可以进行所有组合,从而实现一个具有所有隐私级别的可靠打包系统。

合并工作由客户端负责。

参考资料

致谢

Georg Brandl。


来源:https://github.com/python/peps/blob/main/peps/pep-0381.rst

最后修改:2025-02-01 08:55:40 GMT