The Pitchfork Story

两年多前,作者在 Shopify 的 Ruby 和 Rails 基础设施团队工作时,发布了名为Pitchfork的新 Ruby HTTP 服务器。

  • Unicorn 的设计不错:Shopify 十多年来一直使用 Unicorn 作为生产环境中的应用服务器,作者认为 Unicorn 并非 legacy 软件。反对 Unicorn 的一个主要观点是 Rails 应用大多是 IO 绑定的,可使用线程服务器提高吞吐量,但作者认为 Shopify 的单体应用并非如此,使用线程服务器不可行。此外,作者在 Resiliency 团队工作时目睹了一些灾难性的故障,深知不能完全避免所有 bug,因此采用了纵深防御策略,而 Unicorn 的基于进程的执行模型在系统的弹性方面发挥了重要作用。
  • 并非全是美好:Unicorn 也有缺点,如不防止 slowloris 等常见攻击,需要在缓冲反向代理(如 NGINX)后使用;多进程设计无法进行高效的连接池;无法在内存中缓存数据;内存使用量因进程而增加等。
  • 堆清洁工:作者日常工作中会处理 Shopify 单体应用的内存增长问题,通过分析 Ruby 应用的堆、开发专用工具等方式来减少内存使用,如 deduplicating 模式信息等。
  • 神奇的 Copy-on-Write(CoW):通过 CoW 可在进程间共享更多内存,减少单个工作进程的内存使用,但 Ruby 代码中的懒加载模式会导致内存不能共享,通过使用常量等方式可改善。通过测试应用验证了 inline caches 对 CoW 有效性的影响,若能在 fork 前填充 inline caches 可节省大量内存,但实现起来并不容易。
  • Puma 的 fork 工作进程:Puma 的fork_worker实验功能可让工作进程通过特殊方式启动,避免自动 fork 导致的问题,但作者对其进程父级关系存在保留意见,在测试环境中发现新 spawn 的工作进程会从grpc gem 引发错误,且使grpc gem fork 安全几乎不可能。
  • 子 subreaper:在prctl(2)手册中发现PR_SET_CHILD_SUBREAPER常量,可使 Puma 的实验功能更健壮,允许 JIT 代码共享,作者花费数周进行原型设计。
  • 第一个原型:作者尝试在 Unicorn 中实现基于PR_SET_CHILD_SUBREAPER的功能,最初的补丁不使用该特性以支持所有 POSIX 系统,而是基于 Unicorn 的零停机重启功能,但该方法很脆弱且需要在守护进程模式下启动 Unicorn,还依赖文件系统中的命名管道进行进程间通信,后来改进为使用socketpair(2)UNIXSocket#send_io来使设计更健壮。
  • 进程间通信:Unicorn 中 master 进程与 worker 进程通过创建管道进行通信,作者为使 master 能知道未自己 spawn 的 worker 进程,最初使用命名管道,后改进为使用socketpair(2)UNIXSocket#send_io,使设计更简洁。
  • 决定 fork:由于 Unicorn 的一些特性难以与 reforking 配合,且 Unicorn 项目贡献难度较大,作者决定 fork 该项目并命名为 Pitchfork,移除 Unicorn 中不实用的功能,选择更适合现代容器环境的特性。
  • 模具进程:将 Pitchfork 中 spawning 新 worker 的责任从 master 进程转移到“mold”进程,初始时 mold 进程加载应用并 spawn 新 worker,当 mold 进程退出后,worker 进程会自动重新父级化为 monitor 进程,通过这种方式可逐步更新 worker 进程。
  • 基准测试:将常量缓存演示转化为 Rack 服务器的内存使用基准测试,早期版本的 Pitchfork 表现良好,与 Puma 相比使用一半的内存,但这只是极端微基准测试,不代表实际生产应用的效果。
  • 通往生产的坎坷之路:编写新服务器并进行基准测试是有趣的部分,但在将 Pitchfork 投入生产时会遇到很多问题,其中最大的问题是grpc gem 的 fork 安全问题,作者尝试消除该依赖但未成功,后来同事帮助使grpc gem 获得 fork 支持,之后又花费一个月时间解决其他 fork 安全问题,如在 CI 中模拟 reforking 以发现其他 gem 的问题,并开发close_all_ios!辅助工具来确保 no file descriptors 泄漏。
  • 第一次生产 reforking:在单个金丝雀容器中手动触发 reforking 未发现问题,逐渐在更多服务器上启用自动 reforking,但由于 Shopify 单体应用部署频繁,仍可能在周末发现问题。在生产中发现 mold 进程存在文件描述符泄漏问题,通过提交补丁解决了该问题。
  • 调整 reforking 频率:reforking 和 Copy-on-Write 并非免费,需要找到合适的 reforking 频率以平衡内存使用和延迟影响,作者通过部署多种配置并绘制结果图来找到最佳点,最终确定了较线性增长的设置,使内存使用减少 30%,延迟减少 9%。
  • Unicorn 偏差:Unicorn 和 Pitchfork 在 Linux 上使用epoll系统调用等待传入请求,2016 年epoll增加了EPOLLEXCLUSIVE标志,可避免“thundering herd problem”,但会导致 worker 之间的请求处理不均衡,Ruby 中的这种不平衡会使 VM 中的 inline caches、应用中的懒初始化代码以及 YJIT 在不同 worker 中预热程度不同。
  • reforking 如何降低延迟:由于缓存、JIT 等原因,“冷”worker 比 warmed-up worker 慢,而 reforking 使 worker 更均匀地预热,平均运行速度更快,通过展示 reforking 启用前后容器中 JITed 代码的差异说明了这一点,更多的 JITed 代码意味着更快的执行和更少的热方法编译时间。
  • 真正的杀手功能:最初开发 Pitchfork 的动机是减少内存使用,后来发现 reforking 的真正好处是在流量小高峰时,原本大多空闲的 worker 响应更快,且 reforking 使 killed worker 被更 warmed-up 的 worker 替换,减少了性能影响。
  • 进一步优化:Ruby 3.4.0-preview1 中的 YJIT 编译时间大幅增加,导致服务器 CPU 利用率升高,作者想到只在worker 0中启用 YJIT,利用EPOLLEXCLUSIVE导致的平衡偏差,减少 YJIT 开销,通过图形展示了配置启用前后系统时间的变化。
  • 超越 Shopify:作者原本认为 Pitchfork 只是针对特定需求的 fork,但有一些公司在迁移到或考虑迁移到 Pitchfork,尽管只有一篇关于此类迁移的公开文章且是日语,但这可能需要团队具备处理 fork-safety 问题的资源和专业知识,作者也注意到一些人只是对现代化的 Unicorn 感兴趣。
  • Pitchfork 的未来:Pitchfork 受到作者团队的欢迎,但上级管理的反馈并不积极,作者认为 Pitchfork 的设计在当前能满足大型 Rails 单体应用的需求,提供了真正的并行性、更快的 JIT 预热、低内存使用和一定的弹性,但希望未来 Ruby 能实现真正的单进程并行性,使 Pitchfork 的设计过时。目前 reforking 能解决一些实际问题,在短期内仍将继续发挥作用,虽然存在一些未解决的问题,但在当前情况下尝试不同的服务器设计并不合理。
阅读 8
0 条评论