Swoole+Lumen:同步編程風(fēng)格調(diào)用MySQL異步查詢

網(wǎng)絡(luò)編程一直是PHP的短板,盡管Swoole擴(kuò)展彌補(bǔ)了這個缺陷,但是其編程風(fēng)格偏向了NodeJS或GoLang,與原本的同步編程風(fēng)格迥然相異。目前PHP的大部分主流應(yīng)用框架依然是同步編程風(fēng)格,所以一直在探索Swoole與同步編程結(jié)合的途徑。
lumen-swoole-http正是連接同步編程Lumen和異步編程Swoole的一座橋梁,有興趣可以關(guān)注一下。

LNMP的不足

LNMP是經(jīng)典的Web應(yīng)用架構(gòu)組合,雖然(Linux、NginX、MySQL和PHP-FPM)四者各種是優(yōu)秀的系統(tǒng)或軟件,但是組合到一起的總體性能并不盡人意,明顯的不是1+1+1+1>4,而是4+3+2+1<1。Linux系統(tǒng)無可厚非,主要問題出現(xiàn)在:

從NginX到PHP-FPM

NginX利用IO多路復(fù)用機(jī)制epoll,極大地減少了IO阻塞等待,可以輕松應(yīng)對C10K。可是每次NginX將用戶請求傳遞給PHP-FPM時,PHP-FPM總是需要從新加載PHP項目代碼:創(chuàng)建執(zhí)行環(huán)境,讀取PHP文件和代碼解析、編譯等操作一次又一次的重復(fù)執(zhí)行,造成不小的消耗。

從PHP-FPM到MySQL

由于PHP代碼本身是同步執(zhí)行,PHP-FPM連接MySQL查詢數(shù)據(jù)時,只能空閑等待MySQL返回查詢結(jié)果。一個查詢語句執(zhí)行時間可能會需要幾秒鐘,期間PHP-FPM若是能暫時放下當(dāng)前用戶慢查詢請求,而去處理其他用戶請求,效率必然有所提高。

Swoole HTTP服務(wù)器

Swoole HTTP服務(wù)器也采用了epoll機(jī)制,運(yùn)行性能與NginX相比,雖不及,猶未遠(yuǎn)。不過Swoole HTTP服務(wù)器嵌入PHP中作為其一部分,可以直接運(yùn)行PHP,完全可以取代NginX + PHP-FPM組合。

以目前流行的為框架Lumen(Laravel的子框架)為例,用Swoole HTTP服務(wù)器運(yùn)行Lumen項目十分簡單,只需要在$worker->onRequest($request, $response)(收到用戶請求)時將$request傳給Lumen處理,$response再將Lumen的處理結(jié)果返回給用戶,而且$worker的整個生命周期里只會加載一次Lumen項目代碼,沒有多余的磁盤IO和PHP代碼編譯的開銷。

壓力測試

在4GB+4Core的虛擬機(jī)下,測試HTTP服務(wù)器的靜態(tài)輸出:

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

可以看出,Swoole + Lumen組合的執(zhí)行效率遠(yuǎn)高于NginX + PHP-FPM + Lumen組合。

異步MySQL客戶端

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

一個PHP應(yīng)用要做的事不會是單純的數(shù)據(jù)計算和數(shù)據(jù)輸出,更多的是與數(shù)據(jù)庫數(shù)據(jù)交互。以MySQL數(shù)據(jù)庫為例,在只有一個PHP進(jìn)程的情況,有10個用戶同時請求執(zhí)行select sleep(1);(耗時1秒)查詢語句,若是使用MySQL同步查詢,那么總耗時至少是10秒;若是使用MySQL異步查詢,那么總耗時可能壓縮到1到2秒內(nèi)。

在PHP應(yīng)用中能夠?qū)崿F(xiàn)數(shù)據(jù)庫異步查詢,才能更大的突破性能瓶頸。

雖然Swoole提供了異步MySQL客戶端,但是其異步編程風(fēng)格與Lumen這種同步編程風(fēng)格的項目框架沖突,那么有沒有可能在同步編程風(fēng)格代碼中調(diào)用異步MySQL客戶端呢?

一開始我覺得這是不可能的,直到我看到了這片文章:Cooperative multitasking using coroutines (in PHP!)。當(dāng)然,我看的是中文版: 在PHP中使用協(xié)程實現(xiàn)多任務(wù)調(diào)度,文中提到了PHP5.5加入的一個新功能:yield

Yield

yield是個動詞,意思是“生成”,PHP中yield生出的東西叫Generator,意思是“生成器”??????。

個人理解是:yield將當(dāng)前執(zhí)行的上下文作為當(dāng)前函數(shù)的結(jié)果返回(yield必須在函數(shù)中使用)。

在系統(tǒng)層面,各個進(jìn)程的運(yùn)行秩序由CPU調(diào)度;而有了yield,在PHP進(jìn)程內(nèi),程序員可以自由調(diào)度各個代碼塊的執(zhí)行順序。比如,當(dāng)“發(fā)現(xiàn)”當(dāng)前用戶請求的MySQL查詢將會花費較多的時間,那么可以將當(dāng)前執(zhí)行上下文記錄起來,交給異步MySQL客戶端處理(與用戶請求相關(guān)的$request$response也傳遞過去),而主進(jìn)程繼續(xù)處理下一個用戶請求。

約定聲明

前面用了“發(fā)現(xiàn)”這個詞,當(dāng)然程序不可能智能地發(fā)現(xiàn)還沒執(zhí)行的查詢語句將會是個慢查詢,我們需要一些約定和聲明。
Lumen框架是經(jīng)典的MVC模式,我們約定C即Controller是處理用戶請求的最后一步——Controller接受用戶請求$request并返回響應(yīng)$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服務(wù)器運(yùn)行Lumen項目時,我們一定會獲取Controller的返回結(jié)果。Controller的返回結(jié)果一般可以直接包裝成Lumen響應(yīng)返回給用戶的,但返回結(jié)果若是一個生成器Generator對象,而且其當(dāng)前值是一個慢查詢SlowQuery對象的話,那么我們可以取出SlowQuery對象的sql屬性,交由異步MySQL客戶端執(zhí)行;在異步查詢的回調(diào)函數(shù)中將查詢結(jié)果放回Generator對象存儲的上下文中運(yùn)行,得到最后結(jié)果才返回給用戶;而主進(jìn)程沒有阻塞,可以繼續(xù)處理其他用戶請求。

當(dāng)然,如果想用Eloquent ORM,那也很簡單:我們先繼承Lumen的Model,封裝成一個新的Model類(具體參見Model.php),應(yīng)用中的數(shù)據(jù)模型都繼承于新的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最終產(chǎn)出的用戶響應(yīng)都是一樣的,不過后兩者使用的是異步MySQL客戶端,效率更高。

任務(wù)調(diào)度器

當(dāng)然,我們還需要一個任務(wù)調(diào)度器來執(zhí)行這些生成器,任務(wù)調(diào)度器的實現(xiàn)方法 在PHP中使用協(xié)程實現(xiàn)多任務(wù)調(diào)度文中“多任務(wù)協(xié)作”章節(jié)里有介紹,這里不展開。
Lumen框架中的代碼保持了同步編程風(fēng)格,而任務(wù)調(diào)度器中使用了異步編程風(fēng)格來調(diào)用異步MySQL客戶端。任務(wù)調(diào)度器是在Swoole HTTP服務(wù)器層面使用的,具體參見Service.php

連接限制

其實,每開啟一個Swoole異步MySQL客戶端,主進(jìn)程就會新建一個線程連接MySQL,若是建立太多連接(線程),會增加自身服務(wù)器的壓力,也會增加MySQL數(shù)據(jù)庫服務(wù)器的壓力。
這種利用yield來調(diào)用異步MySQL客戶端處理慢查詢而產(chǎn)生的線程,暫且稱它為“慢查詢協(xié)程”。
為了限制數(shù)據(jù)庫連接數(shù)量,我們可以設(shè)置一個全局變量記錄可新建慢查詢協(xié)程的數(shù)量MAX_COROUTINE,開啟一個異步MySQL客戶端時讓其減一,關(guān)閉一個異步MySQL客戶端時讓其加一;當(dāng)用戶請求慢查詢時,MAX_COROUTINE大于0則由異步MySQL客戶端處理,MAX_COROUTINE等于0時則由主進(jìn)程“硬著頭皮”自己處理。

壓力測試

在4GB+4Core的虛擬機(jī)下,測試HTTP服務(wù)器與數(shù)據(jù)庫讀寫:

一般的快速查詢和快速寫入測試:

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

慢查詢協(xié)程測試:

  • 16worker的Swoole HTTP服務(wù)器,并發(fā)執(zhí)行select sleep(1);請求的最大效率是15.72rps;
  • 16worker x 10coroutine的Swoole HTTP服務(wù)器,并發(fā)執(zhí)行select sleep(1);請求的最大效率是151.93rps。

這里為什么說最大效率呢?因為當(dāng)并發(fā)量遠(yuǎn)大于worker數(shù)目 x coroutine數(shù)目時,可開啟慢查詢協(xié)程的Swoole HTTP服務(wù)器的效率會逐漸跌向普通Swoole HTTP服務(wù)器。

select sleep(1);查詢語句耗時1秒,每個用戶請求都需要1秒時間來處理;不過,16進(jìn)程的、每個進(jìn)程可開啟10個慢查詢協(xié)程的Swoole HTTP服務(wù)器的每秒最多可以處理160個用戶請求,而16進(jìn)程的普通Swoole HTTP服務(wù)器每秒最多只能處理16個用戶請求。

延伸

其實利用yield,我們還可以實現(xiàn)各種各樣的“協(xié)程”。比如,Swoole2.1版本已經(jīng)開始支持go函數(shù)與通道,后續(xù)我們可能還可以將Lumen Controller中一些IO阻塞的操作的上下文移至go函數(shù)里執(zhí)行,這樣既保留了同步編程的風(fēng)格,由達(dá)到異步執(zhí)行的性能。

最后

以上理論,已經(jīng)在lumen-swoole-http項目中實現(xiàn)。
lumen-swoole-http是連接同步編程Lumen和異步編程Swoole的一座橋梁,可以幫助原生PHP的Lumen應(yīng)用項目快速遷移到Swoole HTTP服務(wù)器上;當(dāng)然也可以快速遷移回去??。
有興趣的同學(xué)可以嘗試使用:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 并發(fā)IO問題一直是服務(wù)器端編程中的技術(shù)難題,從最早的同步阻塞直接Fork進(jìn)程,到Worker進(jìn)程池/線程池,到現(xiàn)在...
    零一間閱讀 1,792評論 1 34
  • 出處:韓天峰 網(wǎng)址:rango.swoole.com/archives/508 并發(fā)IO問題一直是后端編程中的技術(shù)...
    meng_philip123閱讀 2,454評論 1 38
  • 更改ip和dnsVi /etc/sysconfig/network-scripts/ifcfg-eth0vi /...
    Xwei_閱讀 2,024評論 0 3
  • 大家好!我是一名高校老師,也是一位媽媽,今日分享一下學(xué)習(xí)Tyger課程體會。我和女兒一起學(xué)習(xí)了tyger老師課程5...
    wangIiIy閱讀 780評論 2 2
  • 這世界總是無法預(yù)料地前行。很多事情的發(fā)生,并不遵循因果。比如突然的愛和莫名其妙的厭倦。人們終其一生都在試圖掌控或者...
    南煙客丶江郎閱讀 285評論 0 0

友情鏈接更多精彩內(nèi)容