改变 PHP 未来的 RFC Polling API

0 阅读14分钟

改变 PHP 未来的 RFC Polling API

当所有人都在争论泛型时,PHP 悄然释放了它极致扩展的全部潜力

一场无人关注的技术辩论

现在打开任何一个 PHP 开发者社区,都能看到关于 Bound-Erased Generics RFC 的激烈争论。更直接地说,打开 PHP internals 邮件列表——语言决策实际发生的地方——关注度最高、回复最多、讨论最活跃的帖子正是 Bound-Erased Generics 的讨论。Turbofish 语法、类型擦除与具体化之争、"使用泛型"和"运行静态分析"的开发者群体是否完全重叠、擦除式泛型是否会永久锁死具体化的可能性。这是一场喧闹、热烈且真正有趣的语言设计辩论,已经消耗了 PHP 社区数周的注意力。

与此同时,另一项 RFC 在撰写本文时以 19 票赞成、0 票反对的结果通过,距离投票截止还有 12 天。邮件列表上没有激烈的讨论,Twitter 上没有争吵,也没有人写博客分析其影响。PHP 生态系统中一些最受尊敬的名字——Composer 作者、FrankenPHP 创建者、Symfony 核心贡献者、AMPHP 维护者——全部投了赞成票,没有任何异议。

这项 RFC 就是 Polling API(Io\Poll)。可以这样认为,它是自 PHP 7 引入类型系统以来,对 PHP 影响最为深远的变化。

要理解为什么,需要先谈谈 stream_select

如果从未写过异步 PHP,大概率没听说过 stream_select。这其实是一种幸运。

stream_select 是 PHP 用于监控多个 I/O 流的原生机制:套接字、文件句柄、连接,等待其中任何一个变为就绪状态。它封装了古老的 Unix select() 系统调用,这个调用自 1983 年 BSD 4.2 起就已存在。PHP 的异步 I/O 原语构建在一个设计于 1983 年的系统调用之上,这一点值得深思。

这对实际开发意味着什么:

它有硬性的文件描述符限制。 在大多数系统上,select() 最多只能同时监控 1024 个文件描述符。这 1024 个不仅仅是并发用户连接——每一个打开的套接字都计入其中。一个出站 HTTP 客户端请求、一个数据库连接、一个 WebSocket 连接、一个 Redis 连接、一个文件句柄,它们都从同一个共享池中消耗文件描述符。具体来看:现代典型异步 PHP 应用中的单个用户请求可能消耗一个文件描述符用于入站连接,一个用于 Redis 缓存检查,一个用于数据库查询,一个用于对外部 API 的 HTTP 调用。单个用户就已经消耗了 4 个文件描述符。将 1024 除以 4,理论上 1024 连接上限在实际应用中实际上接近于 250 个并发用户。在面对真实流量的现代服务器上,这个天花板会以惊人的速度被触及,一旦到达,任何优化都无济于事。

它每次都扫描全部。 事件循环的每一次迭代,stream_select 都会将整个被监控文件描述符集合从用户空间复制到内核空间,扫描所有描述符以找到就绪的,然后将结果复制回来。这是 O(n) 的复杂度。连接越多,每次轮询就越慢。

它频繁唤醒。 即使没有任何就绪事件,stream_select 也会在超时时唤醒进行检查。这完全是无效工作。

结果是一条在负载下灾难性退化的性能曲线。100 个连接时表现尚可,500 个开始显露压力,1024 个时撞到了硬性上限。超过这个数字,就必须求助于 PHP 核心之外的工具。

变通方案的版图

面对这一限制,PHP 生态系统并非无动于衷。优秀的开发者构建了优秀的变通方案。

Swoole 本质上从零重写了 PHP 的网络栈。它功能强大,工程实现令人印象深刻,但它几乎是一个并行的 PHP 运行时。与许多标准扩展不兼容,超全局变量的行为不同,完全独立的思维模型。使用 Swoole 不是在 PHP 应用中添加功能,而是为 Swoole 重写整个应用。

ext-uv、ext-event、ext-ev 是分别暴露 libuv、libevent 和 libev 的 PECL 扩展,它们都原生支持 epoll 和 kqueue。ReactPHP 和 AMPHP 将它们用作可选的高性能后端。在可用的情况下,它们运作良好。

但"在可用的情况下"这句话承载了太多。PECL 扩展不随 PHP 一起发布,需要手动编译或使用特殊的软件包仓库。共享主机几乎从不提供,许多 VPS 提供商也不在默认 PHP 构建中包含它们。Windows 支持参差不齐。版本兼容性在 PHP 版本之间断裂。即使成功安装了它们,异步库也必须为每个扩展维护完全独立的驱动程序实现——一个兼容性矩阵,代表了数千小时的工程投入,仅仅是为了填补 PHP 核心中的一个空白。

HiblaPHP(一个 PHP 异步库)的设计历程可以说明这一困境的严重程度。该库的事件循环附带了两套完全独立的驱动程序实现——StreamSelect 驱动和 Uv 驱动——因为 PHP 核心中不存在单一原生高性能选项。这不是理论推演,而是在生产环境中直接验证的结果。用户态 PHP 异步库的开发者全都做过同样的妥协,Polling API RFC 对这一群体而言意义大到难以言说。这正是异步 PHP 社区一直在默默等待的时刻。

Epoll 和 Kqueue 究竟是什么

Linux 的 epoll 和 BSD/macOS 的 kqueue 是对 select() 在 1983 年试图解决的问题的现代答案,而且它们以完全不同的方式解决了问题。

epoll 和 kqueue 不是"给我一个文件描述符列表,我来全部扫描",而是反过来工作:"告诉我你关心哪些文件描述符,我只有在其中一个真正就绪时才通知你。"

其意义深远。

O(1) 性能。 无论是监控 100 个还是 100,000 个连接,等待 I/O 事件的成本基本持平。内核维护一个内部数据结构,只返回实际就绪的文件描述符的事件。

没有文件描述符限制。 epoll 可以同时监控数百万个连接。1024 的天花板根本不存在。

真正的事件驱动唤醒。 进程休眠直到真正有事情需要处理。没有轮询,没有扫描,没有浪费的 CPU 周期。

这就是为什么每一个严肃的高性能网络运行时都在底层使用 epoll 或 kqueue:Node.js、Nginx、Go 的 net 包、Rust 的 Tokio。对于严肃的规模化场景,这不是可选项,而是基础所在。

PHP 是最后一个没有原生访问这一机制的主流运行时。直到现在。

Polling API RFC

这项 RFC 由 Jakub Zelenka 撰写,目前处于投票阶段,提出了一个新的 Io\Poll 命名空间,提供简洁、最小化的平台原生轮询 API。

该实现自动为当前平台选择最佳可用后端:

  • Linux 上使用 epoll
  • BSD 和 macOS 上使用 kqueue
  • Solaris/illumos 上使用 Event Ports
  • Windows 上使用 WSAPoll
  • 其他 POSIX 系统上使用 poll() 回退

核心 API 有意保持最小化。Context 管理一组监控器。StreamPollHandle 包装一个流资源。Context::wait() 阻塞直到有事件就绪,可选择性设置超时。基本就这些。

没有捆绑事件循环,没有定时器,没有信号处理(目前还没有)。只提供一个真正缺失的原语:一种利用底层操作系统最佳能力来高效监控文件描述符的方法。

这种克制实际上是出色的政治和技术设计。之前试图做得太多的异步 RFC——捆绑完整的事件循环并一次性解决所有问题——遇到了与扼杀 True Async RFC 相同的政治阻力。通过严格聚焦于那一个缺失的原语,这项 RFC 完全避开了那些争议。于是 19-0,零反对。

Fibers 与 Polling API:天作之合

PHP 8.1 引入了 Fibers,一种协作式多任务原语,可以用看起来同步的代码编写异步逻辑。它确实令人兴奋。但回顾来看,它也是不完整的。

Fibers 赋予了 PHP 并发模型,却没有提供让这种并发模型真正可扩展的 I/O 原语。开发者可以写出漂亮的基于 Fiber 的异步代码,然后看着它在 1024 个连接处卡住,因为底层仍然在调用 stream_select

就像造了一辆跑车却保留了限速器。

Polling API 移除了这个限速器。Fibers 和 Polling API 一起构成了 PHP 异步规模化场景的完整答案:并发模型加上让它真正生效的 I/O 原语。二者缺一不可。PHP 8.1 铺设了轨道,PHP 8.6 提供了引擎。

这对生态系统意味着什么

直接影响首先落在异步 PHP 库上:AMPHP、ReactPHP、Revolt 和 HiblaPHP。

目前这些库维护着多个后端实现来处理扩展依赖问题。ReactPHP 有 StreamSelectLoop、ExtUvLoop、ExtEventLoop、ExtEvLoop——四个独立的实现来填补 PHP 核心的空白。AMPHP 也是如此。每一个这样的实现都代表了为解决本不应存在的问题而投入的工程精力。

有了 Polling API,所有这些都合并为一个。单一的高性能原生后端。无需扩展,无需兼容性矩阵,无需在 README 中写 PECL 安装说明。只需 PHP 8.6,就能原生、规模化地在任何服务器上运行。

stream_select 实现中的 Windows $except 黑科技——消失了,WSAPoll 原生处理了。EINTR 信号中断错误抑制——消失了,内部处理了。文件描述符上限——消失了。O(n) 扫描——消失了。

HiblaPHP 计划在该功能落地当天重写事件循环核心。StreamSelect/Managers/StreamManager,目前包含 Windows 变通方案、EINTR 处理和手动文件描述符集管理的数百行代码,将变为 Io\Poll\Context 的薄封装。整个 Uv 驱动之所以存在,正是因为原生选项不够好。一旦 Polling API 发布,该驱动就变成可选的遗留方案,不再是想获得真正性能的用户的推荐路径。

6 美元 VPS 上的 C10K

C10K 问题——处理 10,000 个并发连接——是一个里程碑式的基准测试,定义了整整一代服务器基础设施决策。Node.js 因此而存在,Nginx 因此而取代 Apache 成为默认 Web 服务器,Go 因此而成为高性能服务的首选语言。

PHP 从未参与过这场对话。不是因为 PHP 开发者能力不足,而是因为实现这一目标所需的工具需要如此多的变通方案,以至于直接换一种语言反而更容易。

PHP 8.6 改变了这一点。一台 5 美元的 DigitalOcean 云主机,运行原生 PHP 8.6,之上使用 AMPHP、ReactPHP 或 HiblaPHP,通过 Polling API 使用原生 epoll,可以现实地处理 C10K。瓶颈不再是轮询机制,而是应用逻辑、每连接内存消耗和 CPU 数量——这才是瓶颈本应在的位置,也是 Node.js 和 Go 多年来所处的位置。

这对 PHP 的用户群体意义重大。PHP 社区中有很大一部分是独立开发者、小型代理商、自筹资金的创始人——那些深切关心从廉价基础设施中榨取性能的人。他们正是那些无法为 Swoole 或 ext-uv 的运维开销提供充分理由,但会立即从原生 epoll 中受益的人。现在,他们用 PHP 8.6 免费获得了这一切。

Swoole 与更广泛的异步生态

Swoole 的成就值得极大的肯定。当 PHP 没有通向真正异步性能的可行路径时,Swoole 从头构建了一条。这是真正令人印象深刻的工程,并且证明了一件至关重要的事情:PHP 开发者渴望高并发异步能力,并愿意为此付出巨大努力。

ext-uv、ext-event 以及 ReactPHP 和 AMPHP 背后的团队同样值得称赞,他们构建并维护了多个驱动后端,只为给 PHP 开发者一个可行的异步方案。所有这些工作都是必要的、有价值的,并推动了生态系统向前发展。

Polling API 并没有让这些工作变得无关紧要,而是验证了它们。它证明了需求一直是真实的,使用场景一直是合理的,唯一缺少的只是 PHP 核心中的一个原生原语作为基础。

PHP 8.6 带来的变化是,生态系统不再需要绕过 PHP 的限制来达成目标。基础现在已经成为语言本身的一部分。这一上升浪潮托起了每一个多年来在不完美基础上努力构建的异步 PHP 库和框架。

讽刺之处

Bound-Erased Generics RFC 正在产生数百条邮件列表消息、Twitter 线程和博客文章。关于 turbofish 语法以及类型擦除是否在哲学上与 PHP 的运行时类型模型一致的激烈辩论。

Polling API 以 19-0 通过。没有辩论,没有反对。只有那些理解它实际作用的人默默达成的一致同意。

而它实际做到的,比 PHP 近期历史上几乎任何事情都更具分量。不是因为它添加了一个会在应用代码中直接使用的功能——大多数 PHP 开发者永远不会写 new Io\Poll\Context()——而是因为它移除了一个限制整个 PHP 异步生态系统长达二十年的根本性天花板。

最好的基础设施变更是不可见的。注意不到 epoll,只会注意到 Nginx 毫无压力地处理一万个连接。也不会注意到 Polling API,只会在某天发现异步 PHP 应用扩展得比预期的更远,运行在比计划更便宜的硬件上,配置比担心的更少。

这就是 PHP 在达到它的全部潜力。安静地,一致地,在投票截止前还剩 12 天。

接下来会发生什么

该 RFC 将于 2026 年 6 月 3 日截止,并有望通过进入 PHP 8.6。一旦落地:

  • AMPHP、ReactPHP 和 Revolt 将更新它们的事件循环后端
  • HiblaPHP 将围绕 Io\Poll\Context 重写其核心
  • "安装 ext-uv 以获得性能"的脚注将从各处的异步 PHP README 中消失
  • 基准测试将开始出现,展示 PHP 8.6 在普通硬件上处理 C10K
  • "PHP 无法规模化"的论调将失去最后的技术依据

而在 PHP internals 档案中的某个角落,一封有 19 票赞成、0 票反对的安静邮件列表帖子,将成为历史学家指向"一切从这里改变"的脚注。

延伸阅读

如果想深入了解本文涵盖的任何内容,以下资源值得收藏:

RFC 提案本身——在 PHP wiki 上阅读完整的 Polling API RFC 提案并关注实时投票结果。魔法正在这里发生。 wiki.php.net/rfc/poll_ap…

理解 epoll——深入技术解析 epoll 在底层的工作原理,以及为什么它在 Linux 内核中出现时是对 select() 的里程碑式改进。 man7.org/linux/man-p…

理解 kqueue——BSD 和 macOS 的 epoll 等价物,驱动 Apple 服务器和 FreeBSD 系统高性能网络的机制。 man.freebsd.org/cgi/man.cgi…

开始使用异步 PHP——如果对编写异步 PHP 产生兴趣但不确定从何开始,这是使用 ReactPHP 的事件循环模型最友好的入门介绍。 www.honeybadger.io/blog/gettin…

改变 PHP 未来的 RFC Polling API