天天看点

浅谈 Node.js 和 PHP 进程管理

所周知,php 占据了服务端编程语言的半壁江山,正如汪峰在音乐圈的地位一般。随着 node.js 逐渐走上服务端编程的舞台,关于 php 和 node.js 孰优孰劣的争论也不曾间断。

垄断性的市场份额足以佐证 php 的优秀。并且 hhvm 虚拟机、php 7 的革新,也给 php 带来了跨越式的性能突破。然而,当我们为语言层面的性能差异喋喋不休时,却往往忽略了 web 模型在性能表现中的权重。

早期的 web 服务,是基于传统的 cgi 协议实现的。每个发送到服务器的请求,都需要经过启动进程、处理请求、结束进程三个步骤,以至于访问量增大时,系统资源(如内存、cpu 等)开销也巨大,导致服务器性能下降甚至服务中断。

浅谈 Node.js 和 PHP 进程管理

图 1:简单的 cgi 流程示意

在 cgi 协议下,解析器的反复加载是性能低下的主要原因。如果让解析器进程长驻内存,那么它只需启动一次,就可以一直执行着,不必每次都重新 fork 进程,这就有了后来的 fastcgi 协议。

如果 fastcgi 仅仅做到这样,那么和 node.js 单进程单线程的模型是基本一致的:node.js 进程启动后保持持续运行,所有的请求都由这个进程接收和处理,当某个请求引起未知错误时,才可能致使进程退出。

事实上 fastcgi 并没有那么简单,为了保证服务的稳定性,他被设计成了多进程调度的模式:

浅谈 Node.js 和 PHP 进程管理

图 2:nginx + fastcgi 执行过程

这个过程同样可以描述为三个步骤:

首先,初始化 fastcgi 进程管理器,并启动多个 cgi 解释器子进程;

接着,当请求到达 web 服务器时,进程管理器选择并连接一个子进程,将环境变量和标准输入发送给它,处理完成后将标准输出和错误信息返还给 web 服务器;

最终,子进程关闭连接,继续等待下一个请求的到来;

我们回过头来看看 node.js 的进程管理方式。

原生 node.js 的单进程单线程模型是一个极易被喷的槽点。这种机制也决定了 node.js 天生只支持单核 cpu,无法有效地利用多核资源,一旦进程崩溃,还会导致整个 web 服务的土崩瓦解。

浅谈 Node.js 和 PHP 进程管理

图 3:简单的 node.js 的请求模型

和 cgi 一样,单一进程始终面临着可靠性低、稳定性差的问题,当真正服务于生产环境时,这样的弱点相当致命。如果代码本身足够健壮,倒可以在一定程度上避免出错,但同时也对测试工作提出了更高要求。现实中我们无法避免代码 100% 不出纰漏,有些东西容易编写测试用例,有些东西却只能依靠人肉目测。

所幸 node.js 提供了 <code>child_process</code> 模块,通过简单 fork 即可随意创建出子进程。如果为每个 cpu 分别指派一个子进程,多核利用就完美实现了。于此同时,由于 <code>child_process</code> 模块本身继承自 <code>eventemitter</code> 这个基础类,事件驱动使得进程间的通信非常高效。

浅谈 Node.js 和 PHP 进程管理

图 4:简单的 node.js master-worker 模型(扒的淘杰老湿的图)

为了简化庞杂的父子进程模型实现,node.js 紧接着又封装了 <code>cluster</code> 模块,不论是负载均衡、资源回收,还是进程守护,它都会像保姆一样帮你默默地搞定一切。具体技术细节可以参考淘杰老湿的《当我们谈论 cluster 时我们在谈论什么(上)》和《当我们谈论 cluster 时我们在谈论什么(下)》,里面有所有关于 <code>cluster</code> 方案的推演和实现,这里不再赘述。

在 node.js 里,要让应用跑在多核集群上,只需寥寥几行代码就万事大吉了:

那么反观 fastcgi 协议,它又是如何处理这种模型的呢?

php-fpm 是 php 针对 fastcgi 协议的具体实现,也是 php 在多种服务器端应用编程端口(sapi:cgi、fast-cgi、cli、isapi、apache)里使用最普遍、性能最佳的一款进程管理器。它同样实现了类似 node.js 的父子进程管理模型,确保了 web 服务的可靠性和高性能。

php-fpm 这种模型是非常典型的多进程同步模型,意味着一个请求对应一个进程线程,并且 io 是同步阻塞的。所以尽管 php-fpm 维护着独立的 cgi 进程池、系统也可以很轻松的管理进程的生命周期,但注定无法像 node.js 那样,一个进程就可以承担巨大的请求压力。

受制于服务器的硬件设施,php-fpm 需要指定合理的 php-fpm.conf 配置:

和 js 不一样的是,php 进程本身并不存在内存泄露的问题,每个进程完成请求处理后会回收内存,但是并不会释放给操作系统,这就导致大量内存被 php-fpm 占用而无法释放,请求量升高时性能骤降。

所以 php-fpm 需要控制单个子进程请求次数的阈值。很多人会误以为 <code>max_requests</code> 控制了进程的并发连接数,实际上 php-fpm 模式下的进程是单一线程的,请求无法并发。这个参数的真正意义是提供请求计数器的功能,超过阈值数目后自动回收,缓解内存压力。

或许你已经发现了问题的关键:尽管 php-fpm 架构卓越,但还是卡在单一进程的性能上了。

node.js 天生没有这个问题,而 php-fpm 却无法保证,它的稳定性受制于硬件设施和配置文件的契合度,以及 web 服务器(通常是 nginx)对 php-fpm 服务的负载调度能力。

对 php 7 的狂热掩盖了 node.js 带来的猛烈冲击。当大家还沉醉在如何选择 hhvm 还是 php 7 的时候,reactphp 也在茁壮成长,它彻彻底底抛弃了 nginx + php-fpm 的传统架构,转而模仿并接纳了 node.js 的事件驱动和非阻塞 io 模型,甚至连副标题,都起得一毛一样:

鉴于大家都比较了解 node.js,对 reactphp 的原理就不再赘述了,我们可以认为它就是个 php 版的 node.js。拿它和传统架构(nginx + php-fpm,公平起见,php-fpm 只开一个进程)去做对比,结果是这样的:

浅谈 Node.js 和 PHP 进程管理

图 5:输出“hello world”时的 qps 曲线

浅谈 Node.js 和 PHP 进程管理

图 6:查询 sql 时的 qps 曲线

我们可以看到,当事件驱动、异步执行、非阻塞 io 被移植嫁接到 php 上后,即便没了 php-fpm 支撑,qps 曲线依然不错,在 io 密集型的场景下,性能甚至得到了成倍成倍的提升。

事件和异步回调机制真是太赞了,它巧妙地将大规模并发、大吞吐量时的拥堵化解为一个异步事件队列,然后挨个解决阻塞(如文件读取,数据库查询等)。

针对单进程模型的吐槽,或许有些偏激。不过显而易见的事实是,单进程模型的可靠性,在 web 服务器和进程管理器层面是有很大的优化空间的,而高并发的处理能力取决于语言特性,说白了就是事件和异步的支持。

这两点想必是让 node.js 天生骄傲的事情,但在 php 里没有得到原生支持,只能通过模拟步进操作的方式来支持类似 node.js 的事件机制,所以 reactphp 其实也并没有想象中那么完美。

大部分时候,当我们比较语言优劣,容易局限在语言本身,而忽视了配套的一些关键因素。

就拿 php 来说,这两年听到了太多关于即时编译器(jit)、opcode 缓存、抽象语法树(ast)、hhvm 等等之类的话题。当这些优化逐步完备,语言层面的问题,早已不再是 web 性能的短板了。如果实在不行,我们还可以把复杂任务交给 c 和 c++,以 node.js addon 或者 php 扩展的形式,轻轻松松就搞定了。

都说 php 是“世界上最好的语言”,既然如此,也是时候学习下 node.js 事件驱动和异步回调,考虑考虑如何对 php-fpm 进行大刀阔斧的革新。毕竟不管是 node.js 还是 php,我们所擅长的地方,终将还是 web,高性能的 web。

fastcgi process manager (fpm)

reactphp - event-driven, non-blocking i/o with php.

reactphp - php 版的 node.js

当我们谈论 cluster 时我们在谈论什么(上)

当我们谈论 cluster 时我们在谈论什么(下)

该文章来自于阿里巴巴技术协会(ata)

作者:邦彦