Python Workers Redux:快速冷启动、包管理以及以 uv 为核心的开发流程(二)
类型提示
您可以使用 pywrangler types,针对 Wrangler 配置中定义的所有绑定项来生成类型提示。这些类型提示将适用于 Pylance 或最新版本的 mypy。
为了生成类型,我们使用 wrangler types 来创建 TypeScript 类型提示,然后使用 TypeScript 编译器为这些类型生成抽象语法树。最后,我们使用 TypeScript 提示(例如 JS 对象是否有迭代器字段),来生成与 Pyodide 外部函数接口协同工作的 mypy 类型提示。
使用快照来缩短冷启动时间
Python 启动通常非常缓慢,导入 Python 模块会触发大量工作。我们使用内存快照,避免在冷启动期间运行 Python 启动程序。
部署 Worker 时,我们会执行 Worker 的顶层作用域,然后拍摄内存快照,并将其与您的 Worker 一起存储。每当我们为 Worker 启动新的隔离时,我们会恢复内存快照,Worker 可随时处理请求,而无需预先执行任何 Python 代码。这显著改善了冷启动时间。例如,启动一个不使用快照的 Worker 且导入 fastapi、httpx 和 pydantic 等库大约需要 10 秒。使用快照则只需 1 秒。
Pyodide 基于 WebAssembly 构建,正是这一点使其成为可能。我们可轻松捕获运行时的完整线性内存并进行恢复。
内存快照和熵
WebAssembly 运行时不需要像地址空间布局随机化这类安全功能,因此在现代操作系统上,与内存快照相关的大部分难题都不会出现。就像使用原生内存快照一样,我们仍然需要在启动时小心处理熵,以避免使用 XKCD 随机数生成器(我们非常注重实际的随机性)。
图1
通过创建内存快照,我们可能会无意中锁定一个随机种子值。在这种情况下,未来对“随机”数的调用将在多个请求中始终返回相同的值序列。
由于 Python 在启动时会使用大量的熵,因此避免这种情况尤其具有挑战性。其中包括 libc 函数 getentropy() 和 getrandom(),以及从 /dev/random 和 /dev/urandom 中读取。所有这些函数与 JavaScript crypto.getRandomValues() 函数的实施相同。
在 Cloudflare Workers 中,crypto.getRandomValues()启动时始终处于禁用状态,以便将来我们可以转换为使用内存快照。遗憾的是,如果不调用此函数,Python 解释器将无法启动。而且许多软件包在启动时也需要熵。这种熵主要有两个用途:
·用于哈希随机化的哈希种子
·用于伪随机数生成器的种子
在启动时进行哈希随机化,并且我们接受每个特定 Worker 具有固定哈希种子的成本。Python 没有在启动后允许替换哈希种子的机制。
对于伪随机数生成器 (PRNG),我们采取以下方法:
在部署时:
1.使用固定的“毒种子”为 PRNG 设定种子,然后记录 PRNG 的状态。
2.将所有调用 PRNG 的 API 替换为一项覆盖,该覆盖会因用户错误而导致部署失败。
3.执行用户代码的顶层作用域。
4.捕获快照。
运行时:
1.确认 PRNG 状态未更改。如果发生更改,说明我们忘记了某种方法的覆盖。部署因内部错误而失败。
2.恢复快照后,在执行任何处理程序之前,重新设定随机数生成器的种子。
这样,我们可以确保 Worker 在运行的同时能够使用 PRNG,但在初始化和预快照期间会阻止 Workers 使用。
内存快照与 WebAssembly 状态
在 WebAssembly 上创建内存快照时,还会遇到一个额外的难题:我们保存的内存快照仅包含 WebAssembly 的线性内存;但是,Pyodide WebAssembly 实例的完整状态并未包含在线性内存中。
在此内存之外有两张表。
一张表用于保存函数指针的值。传统计算机使用“冯·诺伊曼”架构,这意味着代码与数据存在于相同的内存空间中,因此调用函数指针相当于跳转到某个内存地址。WebAssembly 具有“哈佛架构”,代码存在于单独的地址空间中。这是实现 WebAssembly 大部分安全保障的关键,特别是 WebAssembly 为何不需要地址空间布局随机化。在 WebAssembly 中,函数指针是指向函数指针表的索引。
第二张表保存了所有从 Python 引用的 JavaScript 对象。JavaScript 对象不能直接存储到内存中,因为 JavaScript 虚拟机禁止直接获取指向 JavaScript 对象的指针。而这些对象被存储在一个表中,并在 WebAssembly 中表示为该表的索引。
在将快照恢复为捕获快照时,我们需要确保这两个表的状态完全相同。
WebAssembly 实例初始化时,函数指针表始终处于相同状态。加载动态库时(例如 numpy 等原生 Python 包),动态加载器会更新该表。
处理动态加载:
1.在拍摄快照时,我们会为加载器打补丁,以记录动态库的加载顺序、每个库的元数据在内存中分配的地址,以及用于调整的函数指针表基址。
2.恢复快照时,我们以相同的顺序重新载入动态链接库,并使用经过打补丁的内存分配器将元数据放置在相同的位置。我们确认,函数指针表的当前大小与我们为动态库记录的动态库的函数指针表基址相匹配。
所有这些确保在我们恢复快照后,每个函数指针都具有与拍摄快照时相同的含义。
为了处理 JavaScript 引用,我们实施了一个相当有限的系统。如果一个 JavaScript 对象可以通过一系列属性访问项,从 globalThis 进行访问,我们会记录这些属性访问项,并在恢复快照时回放。如果存在任何无法通过此方式访问的 JavaScript 对象,我们将无法部署 Worker。这已经足以处理所有支持 Pyodide 的现有 Python 软件包,这些软件包会执行顶级导入,例如:
图2
使用分片来降低冷启动频率
Python Workers 性能策略的另一项重要特征是分片。此外非常详细地介绍了其实施。简而言之,我们现在将请求路由到现有的 Worker 实例,而以前我们可能会选择启动一个新实例。
实际上分片是首先在 Python Workers 中启用的,并且后续也证明了其是一个很好的测试平台。在 Python 中,冷启动的成本远高于 JavaScript。因此,务必确保将请求路由到已在运行的隔离区,这一点至关重要。
下一步
以上这些仅仅是开始。我们接下来会有很多计划来改进 Python Workers:
·为开发人员提供更友好、更高效的开发工具和环境
·利用我们的隔离架构,实现更快的冷启动
·支持更多软件包。
·支持原生 TCP 套接字、原生 WebSocket 及更多绑定项。
要了解有关 Python Workers 的更多信息,请查看此处提供的文档。如需获得帮助,请务必加入我们的 Discord。
Cloudflare 保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序,抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的部署与实施。
从任何设备访问 1.1.1.1,使用我们的免费应用加速和保护您的互联网。
立即联系我们,获取更多相关信息http://t.cn/A6Tx4yTT
#Cloudflare##Python Workers#
