天天看點

淺談 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)

作者:邦彥