網絡編程一直是PHP的短板,儘管 Swoole擴展彌補了這個缺陷,可是其編程風格偏向了NodeJS或GoLang,與本來的同步編程風格迥然相異。目前PHP的大部分主流應用框架依然是同步編程風格,因此一直在探索Swoole與同步編程結合的途徑。
lumen-swoole-http正是鏈接同步編程Lumen和異步編程Swoole的一座橋樑,有興趣能夠關注一下。
LNMP是經典的Web應用架構組合,雖然(Linux、NginX、MySQL和PHP-FPM)四者各類是優秀的系統或軟件,可是組合到一塊兒的整體性能並不盡人意,明顯的不是1+1+1+1>4
,而是4+3+2+1<1
。Linux系統無可厚非,主要問題出如今:php
NginX利用IO多路複用機制epoll,極大地減小了IO阻塞等待,能夠輕鬆應對C10K。但是每次NginX將用戶請求傳遞給PHP-FPM時,PHP-FPM老是須要重新加載PHP項目代碼:建立執行環境,讀取PHP文件和代碼解析、編譯等操做一次又一次的重複執行,形成不小的消耗。html
因爲PHP代碼自己是同步執行,PHP-FPM鏈接MySQL查詢數據時,只能空閒等待MySQL返回查詢結果。一個查詢語句執行時間可能會須要幾秒鐘,期間PHP-FPM如果能暫時放下當前用戶慢查詢請求,而去處理其餘用戶請求,效率必然有所提升。laravel
<!--more-->git
Swoole HTTP服務器也採用了epoll機制,運行性能與NginX相比,雖不及,猶未遠。不過Swoole HTTP服務器嵌入PHP中做爲其一部分,能夠直接運行PHP,徹底能夠取代NginX + PHP-FPM組合。程序員
以目前流行的爲框架Lumen(Laravel的子框架)爲例,用Swoole HTTP服務器運行Lumen項目十分簡單,只須要在$worker->onRequest($request, $response)
(收到用戶請求)時將$request
傳給Lumen處理,$response
再將Lumen的處理結果返回給用戶,並且$worker
的整個生命週期裏只會加載一次Lumen項目代碼,沒有多餘的磁盤IO和PHP代碼編譯的開銷。github
在4GB+4Core的虛擬機下,測試HTTP服務器的靜態輸出:sql
NginX + HTML QPS:25883.44 NginX + PHP-FPM + Lumen QPS:828.36 Swoole + Lumen QPS:13647.75
NginX + HTML QPS:86843.11 NginX + PHP-FPM + Lumen QPS:894.06 Swoole + Lumen QPS:18183.43
能夠看出,Swoole + Lumen
組合的執行效率遠高於NginX + PHP-FPM + Lumen
組合。數據庫
以上都是鋪墊,如下才是整篇文章的重點😂😂😂
一個PHP應用要作的事不會是單純的數據計算和數據輸出,更多的是與數據庫數據交互。以MySQL數據庫爲例,在只有一個PHP進程的狀況,有10個用戶同時請求執行select sleep(1);
(耗時1秒)查詢語句,如果使用MySQL同步查詢,那麼總耗時至少是10秒;如果使用MySQL異步查詢,那麼總耗時可能壓縮到1到2秒內。編程
在PHP應用中可以實現數據庫異步查詢,才能更大的突破性能瓶頸。json
雖然Swoole提供了異步MySQL客戶端,可是其異步編程風格與Lumen這種同步編程風格的項目框架衝突,那麼有沒有可能在同步編程風格代碼中調用異步MySQL客戶端呢?
一開始我以爲這是不可能的,直到我看到了這片文章: Cooperative multitasking using coroutines (in PHP!)。固然,我看的是中文版: 在PHP中使用協程實現多任務調度,文中提到了PHP5.5加入的一個新功能: yield。
yield
是個動詞,意思是「生成」,PHP中yield
生出的東西叫Generator
,意思是「生成器」😂😂😂。
我的理解是:yield將當前執行的上下文做爲當前函數的結果返回(yield必須在函數中使用)。
在系統層面,各個進程的運行秩序由CPU調度;而有了yield,在PHP進程內,程序員能夠自由調度各個代碼塊的執行順序。好比,當「發現」當前用戶請求的MySQL查詢將會花費較多的時間,那麼能夠將當前執行上下文記錄起來,交給異步MySQL客戶端處理(與用戶請求相關的$request
和$response
也傳遞過去),而主進程繼續處理下一個用戶請求。
前面用了「發現」這個詞,固然程序不可能智能地發現還沒執行的查詢語句將會是個慢查詢,咱們須要一些約定和聲明。
Lumen框架是經典的MVC模式,咱們約定C即Controller是處理用戶請求的最後一步——Controller接受用戶請求$request
並返回響應$response
。同時咱們聲明一個類,叫SlowQuery
,這個類十分簡單(具體請參見SlowQuery.php):
<?php namespace BL\SwooleHttp\Database; class SlowQuery { public $sql = ''; public function __construct($sql) { $this->sql = $sql; } }
好比,Lumen項目中有這麼一個Controller:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use DB; class TestController extends Controller { public function test() { $a = DB::select('select sleep(1);'); response()->json($a); } }
上面的DB::select
使用的同步MySQL客戶端查詢,咱們用SlowQuery
對象替換它:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use BL\SwooleHttp\Database\SlowQuery; class TestController extends Controller { public function test() { $a = yield new SlowQuery('select sleep(1);'); response()->json($a); } }
以Swoole HTTP服務器運行Lumen項目時,咱們必定會獲取Controller的返回結果。Controller的返回結果通常能夠直接包裝成Lumen響應返回給用戶的,但返回結果如果一個生成器Generator對象,並且其當前值是一個慢查詢SlowQuery對象的話,那麼咱們能夠取出SlowQuery對象的sql屬性,交由異步MySQL客戶端執行;在異步查詢的回調函數中將查詢結果放回Generator對象存儲的上下文中運行,獲得最後結果才返回給用戶;而主進程沒有阻塞,能夠繼續處理其餘用戶請求。
固然,若是想用Eloquent ORM,那也很簡單:咱們先繼承Lumen的Model,封裝成一個新的Model類(具體參見Model.php),應用中的數據模型都繼承於新的Model,Controller就能夠這樣寫:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\User; use DB; class TestController extends Controller { public function test() { $a = yield User::select(DB::raw('sleep(1)'))->yieldGet(); // 注意User須繼承自\BL\SwooleHttp\Database\Model response()->json($a); } }
以上三個Controller最終產出的用戶響應都是同樣的,不事後二者使用的是異步MySQL客戶端,效率更高。
固然,咱們還須要一個任務調度器來執行這些生成器,任務調度器的實現方法 在PHP中使用協程實現多任務調度文中「多任務協做」章節裏有介紹,這裏不展開。
Lumen框架中的代碼保持了同步編程風格,而任務調度器中使用了異步編程風格來調用異步MySQL客戶端。任務調度器是在Swoole HTTP服務器層面使用的,具體參見Service.php。
其實,每開啓一個Swoole異步MySQL客戶端,主進程就會新建一個線程鏈接MySQL,如果創建太多鏈接(線程),會增長自身服務器的壓力,也會增長MySQL數據庫服務器的壓力。
這種利用yield來調用異步MySQL客戶端處理慢查詢而產生的線程,暫且稱它爲「慢查詢協程」。
爲了限制數據庫鏈接數量,咱們能夠設置一個全局變量記錄可新建慢查詢協程的數量MAX_COROUTINE
,開啓一個異步MySQL客戶端時讓其減一,關閉一個異步MySQL客戶端時讓其加一;當用戶請求慢查詢時,MAX_COROUTINE
大於0則由異步MySQL客戶端處理,MAX_COROUTINE
等於0時則由主進程「硬着頭皮」本身處理。
在4GB+4Core的虛擬機下,測試HTTP服務器與數據庫讀寫:
NginX + PHP-FPM + Lumen + MySQL QPS:521.56 Swoole + Lumen + MySQL QPS:7509.99
NginX + PHP-FPM + Lumen + MySQL QPS:449.44 Swoole + Lumen + MySQL QPS:1253.93
select sleep(1);
請求的最大效率是15.72rps;select sleep(1);
請求的最大效率是151.93rps。這裏爲何說最大效率呢?由於當併發量遠大於worker數目 x coroutine數目時,可開啓慢查詢協程的Swoole HTTP服務器的效率會逐漸跌向普通Swoole HTTP服務器。
select sleep(1);
查詢語句耗時1秒,每一個用戶請求都須要1秒時間來處理;不過,16進程的、每一個進程可開啓10個慢查詢協程的Swoole HTTP服務器的每秒最多能夠處理160個用戶請求,而16進程的普通Swoole HTTP服務器每秒最多隻能處理16個用戶請求。
其實利用yield,咱們還能夠實現各類各樣的「協程」。好比,Swoole2.1版本已經開始支持go函數與通道,後續咱們可能還能夠將Lumen Controller中一些IO阻塞的操做的上下文移至go函數裏執行,這樣既保留了同步編程的風格,由達到異步執行的性能。
以上理論,已經在lumen-swoole-http項目中實現。lumen-swoole-http
是鏈接同步編程Lumen和異步編程Swoole的一座橋樑,能夠幫助原生PHP的Lumen應用項目快速遷移到Swoole HTTP服務器上;固然也能夠快速遷移回去😂。
有興趣的同窗能夠嘗試使用: