运行 Home Assistant 时提升 Python 速度 40%
我们的大多数 Containers 使用 Alpine。它是容器的完美发行版,因为它体积小(基于 BusyBox)、支持许多 CPU 架构,并且包系统精简。Alpine 使用 musl 作为其 C 库,而不是更常用的 glibc。
Alpine 与 musl 相比其同类产品相对年轻(分别有 15 年和 9 年的历史),但发展速度显著。由于进展如此之快,基于已不再真实的情况,对两者存在许多误解。本文的目标是解决其中几个问题,以及我们如何解决它们。
本博文无意成为 musl vs. glibc 的争吵。每种用例都不同,有自己的权衡。例如,我们在操作系统中使用 glibc。
对于测试,我使用了 Docker Python library 的镜像,结果发布到我们的 base images。我使用 pyperformance 进行实验室测试,并使用 Home Assistant 内部 benchmark 工具进行更真实的对比。测试环境运行在与 Docker host 相同的容器内。
C/POSIX 标准库
我经常读到:Python 在使用 musl 作为默认 C 库时速度较慢。这个事实并不 100% 正确。如果 Python 运行时使用相同的 GCC 和 -O3 编译,glibc 版本在实验室基准测试中稍快,但在实际世界中,差异微不足道。Alpine 使用 -Os 编译,而大多数其他发行版使用 -O2 编译。这导致了 Python 运行时解释器之间常被提及的差异。但使用相同的编译器优化时,基于 musl 的 Python 运行时没有负面副作用。
但有一个变革者,使得基于 musl 的运行时相比基于 glibc 的运行时更实用。它是内存分配器 jemalloc,一个注重减少碎片和可扩展并发支持的通用 malloc 实现。我在一些关于 Rust 的博文上发现了一个有趣的效果。一些开发者发现 musl 在使用 jemalloc 时比 glibc 快得多,而 glibc 在使用 jemalloc 时反而变慢。可以肯定的是,glibc 和 jemalloc 的好处不在于速度(因为它们优化的是内存管理),但 musl 同时获得两者的好处。虽然纯 musl 和 glibc 之间的差异可以忽略不计,但 musl + jemalloc 与 glibc 之间的差异是实质性的(禁用 GCC 内置内存分配器优化时)。是的,现在的 jemalloc 与 musl 兼容(曾经有一段时间不兼容)。
编译器
如何编译 Python 也很重要。Fedora 或 Redhat 曾有关于禁用 semantic-interposition 以获得高性能提升的声明。我在 GCC 9.3.0 上无法重现这一点,但也未看到任何不良副作用。我建议禁用语义(如内置分配器优化)并在构建时链接 jemalloc。我也建议使用 -O3 优化。我们从未在目标平台上看到这些激进优化引发任何问题。需要说明的是,与发行版的 Python 运行时解释器不同,我们不需要在所有地方运行。因此,我们可以不加任何覆盖地使用 --enable-optimizations 并添加更多 flags。我可以肯定地说,PGO/LTO/O3 使 Python 更快,并且在我们的目标 CPU 上有效。
Python 包
Alpine 使用 musl 确实没有 manylinux 兼容性。如果你不缓存构建,安装需要 C extensions 的包时,需要编译它们。这个过程需要时间,就像使用 Qemu 为不同 CPU 架构 cross-build 一样。你无法从 PyPi 获取预编译的二进制文件。这对我们不是问题,因为 PyPI 上提供的二进制文件大多不为我们的目标系统优化。
为了解决 Python 包的安装时间问题,我们创建了自己的 wheel index 和 backend,用于编译所有需要的 wheels,并通过 CI agents 保持最新。我们为每种 CPU 架构预构建了超过 1k 个包,Docker 文件的构建时间已不再重要。
Alpine Linux
Alpine 是容器的优秀基础系统,使我们能够为用户提供最佳体验。非常感谢 Alpine Linux、musl 和 jemalloc,使这一切成为可能。
下表显示了 Alpine Linux 的 Python 运行时与我们优化(GCC 9.3.0/musl)的结果对比。所有测试均使用 Python 3.8.3 进行。

