Swoole+Lumen:同步編程風格調用MySQL異步查詢

網絡編程一直是PHP的短板,儘管 Swoole擴展彌補了這個缺陷,可是其編程風格偏向了NodeJS或GoLang,與本來的同步編程風格迥然相異。目前PHP的大部分主流應用框架依然是同步編程風格,因此一直在探索Swoole與同步編程結合的途徑。
lumen-swoole-http正是鏈接同步編程Lumen和異步編程Swoole的一座橋樑,有興趣能夠關注一下。

LNMP的不足

LNMP是經典的Web應用架構組合,雖然(Linux、NginX、MySQL和PHP-FPM)四者各類是優秀的系統或軟件,可是組合到一塊兒的整體性能並不盡人意,明顯的不是1+1+1+1>4,而是4+3+2+1<1。Linux系統無可厚非,主要問題出如今:php

從NginX到PHP-FPM

NginX利用IO多路複用機制epoll,極大地減小了IO阻塞等待,能夠輕鬆應對C10K。但是每次NginX將用戶請求傳遞給PHP-FPM時,PHP-FPM老是須要重新加載PHP項目代碼:建立執行環境,讀取PHP文件和代碼解析、編譯等操做一次又一次的重複執行,形成不小的消耗。html

從PHP-FPM到MySQL

因爲PHP代碼自己是同步執行,PHP-FPM鏈接MySQL查詢數據時,只能空閒等待MySQL返回查詢結果。一個查詢語句執行時間可能會須要幾秒鐘,期間PHP-FPM如果能暫時放下當前用戶慢查詢請求,而去處理其餘用戶請求,效率必然有所提升。laravel

<!--more-->git

Swoole HTTP服務器

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

  • 2000客戶端併發500000請求,不開啓HTTP Keepalive,平均QPS:
NginX + HTML               QPS:25883.44
NginX + PHP-FPM + Lumen    QPS:828.36
Swoole + Lumen             QPS:13647.75
  • 2000客戶端併發500000請求,開啓HTTP Keepalive,平均QPS:
NginX + HTML               QPS:86843.11
NginX + PHP-FPM + Lumen    QPS:894.06
Swoole + Lumen             QPS:18183.43

能夠看出,Swoole + Lumen組合的執行效率遠高於NginX + PHP-FPM + Lumen組合。數據庫

異步MySQL客戶端

以上都是鋪墊,如下才是整篇文章的重點😂😂😂

一個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

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服務器與數據庫讀寫:

通常的快速查詢和快速寫入測試:

  • 200併發50000請求讀,利用HTTP Keepalive,平均QPS:
NginX + PHP-FPM + Lumen + MySQL    QPS:521.56
Swoole + Lumen + MySQL             QPS:7509.99
  • 200併發50000請求寫,利用HTTP Keepalive,平均QPS:
NginX + PHP-FPM + Lumen + MySQL    QPS:449.44
Swoole + Lumen + MySQL             QPS:1253.93

慢查詢協程測試:

  • 16worker的Swoole HTTP服務器,併發執行select sleep(1);請求的最大效率是15.72rps;
  • 16worker x 10coroutine的Swoole HTTP服務器,併發執行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服務器上;固然也能夠快速遷移回去😂。
有興趣的同窗能夠嘗試使用:

相關文章
相關標籤/搜索