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