淺談Node.js和PHP程式管理
所周知,PHP 佔據了服務端程式語言的半壁江山,正如汪峰在音樂圈的地位一般。隨著 Node.js 逐漸走上服務端程式設計的舞臺,關於 PHP 和 Node.js 孰優孰劣的爭論也不曾間斷。
壟斷性的市場份額足以佐證 PHP 的優秀。並且 HHVM 虛擬機器、PHP 7 的革新,也給 PHP 帶來了跨越式的效能突破。然而,當我們為語言層面的效能差異喋喋不休時,卻往往忽略了 Web 模型在效能表現中的權重。
從 CGI 到 FastCGI
早期的 Web 服務,是基於傳統的 CGI 協議實現的。每個傳送到伺服器的請求,都需要經過啟動程式、處理請求、結束程式三個步驟,以至於訪問量增大時,系統資源(如記憶體、CPU 等)開銷也巨大,導致伺服器效能下降甚至服務中斷。
圖 1:簡單的 CGI 流程示意
在 CGI 協議下,解析器的反覆載入是效能低下的主要原因。如果讓解析器程式長駐記憶體,那麼它只需啟動一次,就可以一直執行著,不必每次都重新 fork 程式,這就有了後來的 FastCGI 協議。
如果 FastCGI 僅僅做到這樣,那麼和 Node.js 單程式單執行緒的模型是基本一致的:Node.js 程式啟動後保持持續執行,所有的請求都由這個程式接收和處理,當某個請求引起未知錯誤時,才可能致使程式退出。
事實上 FastCGI 並沒有那麼簡單,為了保證服務的穩定性,他被設計成了多程式排程的模式:
圖 2:Nginx + FastCGI 執行過程
這個過程同樣可以描述為三個步驟:
- 首先,初始化 FastCGI 程式管理器,並啟動多個 CGI 直譯器子程式;
- 接著,當請求到達 Web 伺服器時,程式管理器選擇並連線一個子程式,將環境變數和標準輸入傳送給它,處理完成後將標準輸出和錯誤資訊返還給 Web 伺服器;
- 最終,子程式關閉連線,繼續等待下一個請求的到來;
從 child_process 到 cluster
我們回過頭來看看 Node.js 的程式管理方式。
原生 Node.js 的單程式單執行緒模型是一個極易被噴的槽點。這種機制也決定了 Node.js 天生只支援單核 CPU,無法有效地利用多核資源,一旦程式崩潰,還會導致整個 Web 服務的土崩瓦解。
圖 3:簡單的 Node.js 的請求模型
和 CGI 一樣,單一程式始終面臨著可靠性低、穩定性差的問題,當真正服務於生產環境時,這樣的弱點相當致命。如果程式碼本身足夠健壯,倒可以在一定程度上避免出錯,但同時也對測試工作提出了更高要求。現實中我們無法避免程式碼 100% 不出紕漏,有些東西容易編寫測試用例,有些東西卻只能依靠人肉目測。
所幸 Node.js 提供了 child_process
模組,通過簡單 fork 即可隨意建立出子程式。如果為每個 CPU 分別指派一個子程式,多核利用就完美實現了。於此同時,由於 child_process
模組本身繼承自 EventEmitter
這個基礎類,事件驅動使得程式間的通訊非常高效。
圖 4:簡單的 Node.js master-worker 模型(扒的淘傑老溼的圖)
為了簡化龐雜的父子程式模型實現,Node.js 緊接著又封裝了 cluster
模組,不論是負載均衡、資源回收,還是程式守護,它都會像保姆一樣幫你默默地搞定一切。具體技術細節可以參考淘傑老溼的《當我們談論 cluster 時我們在談論什麼(上)》和《當我們談論 cluster 時我們在談論什麼(下)》,裡面有所有關於 cluster
方案的推演和實現,這裡不再贅述。
在 Node.js 裡,要讓應用跑在多核叢集上,只需寥寥幾行程式碼就萬事大吉了:
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 {
// 啟動應用...
}
那麼反觀 FastCGI 協議,它又是如何處理這種模型的呢?
PHP-FPM 的天生缺陷
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 配置:
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 服務的負載排程能力。
ReactPHP,事件驅動,非同步執行,非阻塞 IO
對 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。
相關資料
- FastCGI Process Manager (FPM)
- ReactPHP – Event-driven, non-blocking I/O with PHP.
- ReactPHP – PHP 版的 Node.js
- 當我們談論 cluster 時我們在談論什麼(上)
- 當我們談論 cluster 時我們在談論什麼(下)
該文章來自於阿里巴巴技術協會(ATA)
作者:邦彥
相關文章
- 淺談PHP fastcgi和php-fpmPHPAST
- 淺談PHP物件導向程式設計PHP物件程式設計
- 淺談PHP程式碼設計結構PHP
- 淺談php 取餘PHP
- 淺談品牌管理
- 淺談node.js中的stream(流)Node.js
- 淺談 PHP 生成器PHP
- 淺談php web安全 【轉】PHPWeb
- 淺談程式同步和互斥的概念
- 淺談php變數的實現-PHPPHP變數
- 淺談Node.js的事件環(event loop)Node.js事件OOP
- 淺談 Node.js 單執行緒模型Node.js執行緒模型
- 淺談PHP自動化程式碼審計技術PHP
- 淺談PHP弱型別安全PHP型別
- 淺談PHP-FPM引數PHP
- 淺談php count()函式方法PHP函式
- Node.js 和 PHP 包管理工具使用總結Node.jsPHP
- 淺談SAP專案管理專案管理
- 淺談前端狀態管理前端
- 淺談Oracle的undo管理Oracle
- 淺談Java、PHP、C++程式設計的優缺點JavaPHPC++程式設計
- 淺談程式設計程式設計
- 淺談程式優化優化
- 淺談BSGS和EXBSGS
- 淺談ERP財務管理
- 淺談Unix系統管理(轉)
- 淺談 PHP 中的多種加密技術及程式碼示例PHP加密
- Node.js的程式管理Node.js
- 淺談 PHP 中異常類的使用PHP
- PHP 和 Node.js 的角摔PHPNode.js
- 淺談使用node.js怎麼搭建本地伺服器Node.js伺服器
- 程式碼規範淺談
- 淺談程式設計思想程式設計
- 淺談Swift程式語言Swift
- 睿象雲高科|淺談事件管理事件
- 淺談班級高效管理方法
- 淺談專案管理軟體(轉)專案管理
- 淺談專案管理創新(轉)專案管理