Workerman是一款純PHP開發(fā)的開源高性能的異步PHP socket框架。支持TCP長連接,支持websocket、MQTT等諸多協(xié)議。今天我們來介紹一下Workerman中的reusePort屬性,有需要的可以參考參考。
Workerman是一個高性能的PHP Socket服務器框架??梢杂?Workerman 直接在 TCP 層編程,基本的編程套路是:
$w = new WorkermanWorker('tcp://0.0.0.0:80'); $w->count = 4; $w->onMessage = function(WorkermanCOnnectionTcpConnection $connection, array $data) { $connection->send('Hello World'); }; Worker::runAll();
在使用的過程中,不知道你是否留意過 reusePort 這個參數(shù),他默認被設置為 false。這個參數(shù)有什么用?什么情況下我們需要把他設置為 true,從而提高性能呢?
1. reuseport 的作用
關于 reusePort 參數(shù),Workerman官方的文檔是這么解釋的:
開啟監(jiān)聽端口復用后允許多個無親緣關系的進程監(jiān)聽相同的端口,并且由系統(tǒng)內(nèi)核做負載均衡,決定將socket連接交給哪個進程處理,避免了驚群效應,可以提升多進程短連接應用的性能。
如果沒有深入研究過 Linux 網(wǎng)絡編程,很難理解這句話。在此簡單解釋一下:
服務端程序通常通過監(jiān)聽服務器上的某個端口號,來接收客戶端的請求。在Linux中,服務器網(wǎng)卡 + 端口號被抽象成了一個 Socket 。
為了提升性能,一般的服務端程序在運行時都有多個進程(俗稱 Worker)監(jiān)聽同一個 Socket,在沒有客戶端連接到來的時候,這些Worker是處于掛起狀態(tài)的,不消耗CPU資源。
如果某一刻有一個客戶端連接到來,Linux 內(nèi)核就會同時喚醒這些 Worker,讓他們競爭去處理這個連接,
結果只有一個 Worker 可以獲得處理這個連接的機會,其他Worker在競爭失敗后繼續(xù)回到掛起狀態(tài)。喚醒 Worker 的過程是要消耗CPU資源的,Worker 數(shù)量越多,消耗的 CPU 資源就越多,造成了資源的浪費。這就是常說的 驚群效應。
你也許會問:為什么不每次只喚醒一個Worker呢?很遺憾,Linux內(nèi)核并沒有這樣的功能。
幸好,在 Linux 3.9 及以后的版本,加入 reuseport 特性。這個特性有什么用呢?
在有 reuseport 之前,一個端口號只能被一個 Socket 監(jiān)聽,有了 reuseport 之后,這個限制就被打破了:一個端口號可以被多個 Socket 同時監(jiān)聽。
前面說到,Linux 內(nèi)核沒法做到一次只喚醒一個 Worker,但是,內(nèi)核可以做到將客戶端連接均勻地發(fā)送到監(jiān)聽統(tǒng)一端口的一群 Socket 上。
如圖所示,每個 Worker 都有自己的 Socket,都監(jiān)聽同一個端口。當有客戶端連接到來時,內(nèi)核轉(zhuǎn)發(fā)連接到一個 Socket 上,而這個 Socket 只會喚醒自己隸屬的那個 Worker。這樣就很巧妙地解決了 驚群效應,提高了整體的性能。
由此,我們可以得出結論:如果你的 Linux 內(nèi)核版本是 3.9 及以上的話,那么在使用 Workerman 時,可以將 reusePort 設置為 true 提升程序運行效率。
2. Workerman 如何利用 reuseport
雖然你只要在 Workerman 中把 reusePort 設置為 true,就能享受到 Linux 的這個高級特性。但 Workerman 的源碼中,并不只是開啟一個內(nèi)核參數(shù)那么簡單。Workerman 為你隱藏了許多的設計細節(jié),我們來研究下。
Worker
類是 Workerman 里最主要的類,其中有個 listen()
函數(shù):
protected function listen() { ... if (!$this->_mainSocket) { ... $this->_mainSocket = stream_socket_server(...); ... } ... }
listen()
函數(shù)的作用就是在當前進程創(chuàng)建一個 Socket 并開始監(jiān)聽請求。
當 reusePort 為 false 時,主進程在創(chuàng)建 Worker 之前就調(diào)用了 listen()
函數(shù):
protected function initWorkers() { .... if (!$worker->reusePort) { $worker->listen(); } .... }
隨后主進程通過 pcntl_fork() 創(chuàng)建 Worker。pcntl_fork() 有個特性:創(chuàng)建出來的子進程(Worker)中的變量都是父進程復制而來的,包括父進程創(chuàng)建的 mainSocket。所以,當reusePort為??false??時,所有的Worker都復制父進程的mainSocket。所以,當reusePort為??false??時,所有的Worker都復制父進程的_mainSocket,也即共用一個 Socket。
而當 reusePort 為 true 時,情況就不同了。主進程在創(chuàng)建 Worker 前不會調(diào)用 listen()
,而是在創(chuàng)建完 Worker 后由每個 Worker 自行發(fā)起 listen()
調(diào)用:
protected static function forkOneWorkerForLinux($worker) { ... $pid = pcntl_fork(); if ($pid === 0) { if ($worker->reusePort) { $worker->listen(); } ... } ... }
這樣的結果就是,每個子進程(Worker)都創(chuàng)建了自己的 Socket。
最后還有一點,如果想要內(nèi)核開啟 reuseport 功能,需要手動設置 Socket 的 context:
if ($this->reusePort) { $context = stream_context_create(); stream_context_set_option($context, 'socket', 'so_reuseport', 1); }
推薦學習:php視頻教程