周知のとおり、音楽業界における Wang Feng の地位と同様に、PHP はサーバーサイド プログラミング言語の半分を占めています。 Node.js が徐々にサーバーサイド プログラミングの段階に入るにつれて、PHP と Node.js の長所と短所についての議論が止まらなくなりました。
独占的な市場シェアは、PHP の優秀性を証明するのに十分です。また、HHVM 仮想マシンと PHP 7 の革新により、PHP のパフォーマンスに飛躍的な進歩がもたらされました。ただし、言語レベルでのパフォーマンスの違いについて口うるさく言うと、パフォーマンスにおける Web モデルの重要性を見落とすことがよくあります。
初期の Web サービスは、従来の CGI プロトコルに基づいて実装されました。サーバーに送信されるすべてのリクエストは、プロセスの開始、リクエストの処理、プロセスの終了という 3 つのステップを経る必要があります。その結果、アクセス量が増加すると、システム リソース (メモリ、CPU など) のオーバーヘッドが増加します。 .) も大きく、サーバーのパフォーマンスが低下し、サービスが中断されることもあります。
図 1: 単純な CGI プロセス図
CGI プロトコルでは、パーサーの繰り返しロードがパフォーマンス低下の主な原因です。パーサー プロセスがメモリ内に常駐できる場合は、一度起動するだけでよく、毎回プロセスをフォークし直す必要がなく、常に実行できます。これが FastCGI プロトコルの誕生です。
FastCGI がこれのみを実行する場合、基本的に Node.js の単一プロセス、単一スレッド モデルと一致します。Node.js プロセスは開始後も実行され続け、すべてのリクエストは によって受信され、処理されます。リクエストによって不明なエラーが発生すると、プロセスが終了する可能性があります。
実際、FastCGI はそれほど単純ではありません。サービスの安定性を確保するために、マルチプロセス スケジューリング モードに設計されています。
図 2 : Nginx + FastCGI 実行プロセス
このプロセスは 3 つのステップとしても説明できます。
ネイティブ Node.js の単一プロセス、単一スレッド モデルは批判されやすい点です。また、このメカニズムにより、Node.js は本質的にシングルコア CPU のみをサポートし、マルチコア リソースを効果的に利用できないことが決まり、プロセスがクラッシュすると Web サービス全体も崩壊します。
図 3: 単純な Node.js リクエスト モデル
CGI と同様、単一プロセスは、実際のサービスでは常に信頼性の低さと安定性の低さの問題に直面します。本番環境では、この弱点は非常に致命的です。コード自体が十分に堅牢であれば、エラーはある程度回避できますが、テスト作業にはより高い要件が課せられます。実際には、コードを 100% 完璧にすることを避けることはできません。テスト ケースを作成するのは簡単なこともありますが、人間の目視検査に頼るしかないものもあります。
幸いなことに、Node.js には child_process モジュールが用意されており、フォークするだけで自由に子プロセスを作成できます。各 CPU に子プロセスが割り当てられている場合、マルチコアの使用率は完全に達成されます。同時に、child_process モジュール自体は基本クラス EventEmitter を継承しているため、プロセス間のイベント駆動型通信は非常に効率的です。
図 4: シンプルな Node.js マスター-ワーカー モデル (Taojie Laoshi の写真)
複雑な父-子プロセス モデルの実装を簡素化するために、次に、Node .js はクラスター モジュールをカプセル化し、負荷分散、リソースのリサイクル、プロセス デーモンなど、すべてを乳母のように静かに処理するのに役立ちます。特定の技術的な詳細については、Taojie Laoshi の「クラスターについて話すとき、私たちは何について話しているのか (パート 1)」および「クラスターについて話しているとき、私たちは何について話しているのか (パート 2)」を参照してください。これらには、すべての推論と説明が含まれています。クラスター ソリューションの実装については、ここでは詳しく説明しません。
Node.js では、マルチコア クラスター上でアプリケーションを実行するために必要なコードは数行だけで、すべて問題なく実行できます。
FastCGI プロトコルはどのように処理しますか? このモデルはどうですか?
var cluster = require('cluster');var os = require('os');if (cluster.isMaster) { for (var i = 0, n = os.cpus().length; i < n; i ++) { cluster.fork(); }} else { // 启动应用...}
PHP-FPM の固有の欠陥
PHP-FPM このモデルは非常に典型的なマルチプロセス同期モデルです。つまり、1 つのリクエストが 1 つのプロセス スレッドに対応し、IO が同期的にブロックされます。したがって、PHP-FPM は独立した CGI プロセス プールを維持し、プロセスのライフ サイクルを容易に管理できますが、Node.js のような巨大なリクエスト プレッシャーに耐えることはできない運命にあります。
サーバーのハードウェア機能に応じて、PHP-FPM は適切な php-fpm.conf 構成を指定する必要があります。
pm.max_children # 子进程最大数pm.start_servers # 启动时的子进程数pm.min_spare_servers # 最小空闲进程数,空闲进程不够时自动补充pm.max_spare_servers # 最大空闲进程数,空闲进程超过时自动清理pm.max_requests = 1000 # 子进程请求数阈值,超过后自动回收
和 JS 不一样的是,PHP 进程本身并不存在内存泄露的问题,每个进程完成请求处理后会回收内存,但是并不会释放给操作系统,这就导致大量内存被 PHP-FPM 占用而无法释放,请求量升高时性能骤降。
所以 PHP-FPM 需要控制单个子进程请求次数的阈值。很多人会误以为 max_requests 控制了进程的并发连接数,实际上 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 模型,甚至连副标题,都起得一毛一样:
Event-driven, non-blocking I/O with PHP.
鉴于大家都比较了解 Node.js,对 ReactPHP 的原理就不再赘述了,我们可以认为它就是个 PHP 版的 Node.js。拿它和传统架构(Nginx + PHP-FPM,公平起见,PHP-FPM 只开一个进程)去做对比,结果是这样的:
图 5:输出“Hello World”时的 QPS 曲线
图 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。