網(wǎng)絡(luò)編程一直是PHP的短板校焦,盡管Swoole擴(kuò)展彌補(bǔ)了這個(gè)缺陷运吓,但是其編程風(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)無(wú)可厚非愧旦,主要問(wèn)題出現(xiàn)在:
從NginX到PHP-FPM
NginX利用IO多路復(fù)用機(jī)制epoll,極大地減少了IO阻塞等待定罢,可以輕松應(yīng)對(duì)C10K∨蕴保可是每次NginX將用戶請(qǐng)求傳遞給PHP-FPM時(shí)祖凫,PHP-FPM總是需要從新加載PHP項(xiàng)目代碼:創(chuàng)建執(zhí)行環(huán)境,讀取PHP文件和代碼解析酬凳、編譯等操作一次又一次的重復(fù)執(zhí)行惠况,造成不小的消耗。
從PHP-FPM到MySQL
由于PHP代碼本身是同步執(zhí)行宁仔,PHP-FPM連接MySQL查詢數(shù)據(jù)時(shí)稠屠,只能空閑等待MySQL返回查詢結(jié)果。一個(gè)查詢語(yǔ)句執(zhí)行時(shí)間可能會(huì)需要幾秒鐘翎苫,期間PHP-FPM若是能暫時(shí)放下當(dāng)前用戶慢查詢請(qǐng)求权埠,而去處理其他用戶請(qǐng)求,效率必然有所提高煎谍。
Swoole HTTP服務(wù)器
Swoole HTTP服務(wù)器也采用了epoll機(jī)制攘蔽,運(yùn)行性能與NginX相比,雖不及呐粘,猶未遠(yuǎn)满俗。不過(guò)Swoole HTTP服務(wù)器嵌入PHP中作為其一部分转捕,可以直接運(yùn)行PHP,完全可以取代NginX + PHP-FPM組合唆垃。
以目前流行的為框架Lumen(Laravel的子框架)為例五芝,用Swoole HTTP服務(wù)器運(yùn)行Lumen項(xiàng)目十分簡(jiǎn)單,只需要在$worker->onRequest($request, $response)
(收到用戶請(qǐng)求)時(shí)將$request
傳給Lumen處理辕万,$response
再將Lumen的處理結(jié)果返回給用戶枢步,而且$worker
的整個(gè)生命周期里只會(huì)加載一次Lumen項(xiàng)目代碼,沒(méi)有多余的磁盤(pán)IO和PHP代碼編譯的開(kāi)銷蓄坏。
壓力測(cè)試
在4GB+4Core的虛擬機(jī)下价捧,測(cè)試HTTP服務(wù)器的靜態(tài)輸出:
- 2000客戶端并發(fā)500000請(qǐng)求,不開(kāi)啟HTTP Keepalive涡戳,平均QPS:
NginX + HTML QPS:25883.44
NginX + PHP-FPM + Lumen QPS:828.36
Swoole + Lumen QPS:13647.75
- 2000客戶端并發(fā)500000請(qǐng)求结蟋,開(kāi)啟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客戶端
以上都是鋪墊,以下才是整篇文章的重點(diǎn)??????
一個(gè)PHP應(yīng)用要做的事不會(huì)是單純的數(shù)據(jù)計(jì)算和數(shù)據(jù)輸出恍涂,更多的是與數(shù)據(jù)庫(kù)數(shù)據(jù)交互宝惰。以MySQL數(shù)據(jù)庫(kù)為例,在只有一個(gè)PHP進(jìn)程的情況再沧,有10個(gè)用戶同時(shí)請(qǐng)求執(zhí)行select sleep(1);
(耗時(shí)1秒)查詢語(yǔ)句尼夺,若是使用MySQL同步查詢,那么總耗時(shí)至少是10秒炒瘸;若是使用MySQL異步查詢淤堵,那么總耗時(shí)可能壓縮到1到2秒內(nèi)。
在PHP應(yīng)用中能夠?qū)崿F(xiàn)數(shù)據(jù)庫(kù)異步查詢顷扩,才能更大的突破性能瓶頸拐邪。
雖然Swoole提供了異步MySQL客戶端,但是其異步編程風(fēng)格與Lumen這種同步編程風(fēng)格的項(xiàng)目框架沖突隘截,那么有沒(méi)有可能在同步編程風(fēng)格代碼中調(diào)用異步MySQL客戶端呢扎阶?
一開(kāi)始我覺(jué)得這是不可能的,直到我看到了這片文章:Cooperative multitasking using coroutines (in PHP!)婶芭。當(dāng)然东臀,我看的是中文版: 在PHP中使用協(xié)程實(shí)現(xiàn)多任務(wù)調(diào)度,文中提到了PHP5.5加入的一個(gè)新功能:yield雕擂。
Yield
yield
是個(gè)動(dòng)詞啡邑,意思是“生成”,PHP中yield
生出的東西叫Generator
井赌,意思是“生成器”??????谤逼。
個(gè)人理解是:yield將當(dāng)前執(zhí)行的上下文作為當(dāng)前函數(shù)的結(jié)果返回(yield必須在函數(shù)中使用)贵扰。
在系統(tǒng)層面,各個(gè)進(jìn)程的運(yùn)行秩序由CPU調(diào)度流部;而有了yield戚绕,在PHP進(jìn)程內(nèi),程序員可以自由調(diào)度各個(gè)代碼塊的執(zhí)行順序枝冀。比如舞丛,當(dāng)“發(fā)現(xiàn)”當(dāng)前用戶請(qǐng)求的MySQL查詢將會(huì)花費(fèi)較多的時(shí)間,那么可以將當(dāng)前執(zhí)行上下文記錄起來(lái)果漾,交給異步MySQL客戶端處理(與用戶請(qǐng)求相關(guān)的$request
和$response
也傳遞過(guò)去)球切,而主進(jìn)程繼續(xù)處理下一個(gè)用戶請(qǐng)求。
約定聲明
前面用了“發(fā)現(xiàn)”這個(gè)詞绒障,當(dāng)然程序不可能智能地發(fā)現(xiàn)還沒(méi)執(zhí)行的查詢語(yǔ)句將會(huì)是個(gè)慢查詢吨凑,我們需要一些約定和聲明。
Lumen框架是經(jīng)典的MVC模式户辱,我們約定C即Controller是處理用戶請(qǐng)求的最后一步——Controller接受用戶請(qǐng)求$request
并返回響應(yīng)$response
鸵钝。同時(shí)我們聲明一個(gè)類,叫SlowQuery
庐镐,這個(gè)類十分簡(jiǎn)單(具體請(qǐng)參見(jiàn)SlowQuery.php):
<?php
namespace BL\SwooleHttp\Database;
class SlowQuery
{
public $sql = '';
public function __construct($sql)
{
$this->sql = $sql;
}
}
比如恩商,Lumen項(xiàng)目中有這么一個(gè)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
對(duì)象替換它:
<?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項(xiàng)目時(shí)必逆,我們一定會(huì)獲取Controller的返回結(jié)果怠堪。Controller的返回結(jié)果一般可以直接包裝成Lumen響應(yīng)返回給用戶的,但返回結(jié)果若是一個(gè)生成器Generator對(duì)象名眉,而且其當(dāng)前值是一個(gè)慢查詢SlowQuery對(duì)象的話研叫,那么我們可以取出SlowQuery對(duì)象的sql屬性,交由異步MySQL客戶端執(zhí)行璧针;在異步查詢的回調(diào)函數(shù)中將查詢結(jié)果放回Generator對(duì)象存儲(chǔ)的上下文中運(yùn)行,得到最后結(jié)果才返回給用戶渊啰;而主進(jìn)程沒(méi)有阻塞探橱,可以繼續(xù)處理其他用戶請(qǐng)求。
當(dāng)然绘证,如果想用Eloquent ORM隧膏,那也很簡(jiǎn)單:我們先繼承Lumen的Model,封裝成一個(gè)新的Model類(具體參見(jiàn)Model.php)嚷那,應(yīng)用中的數(shù)據(jù)模型都繼承于新的Model胞枕,Controller就可以這樣寫(xiě):
<?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);
}
}
以上三個(gè)Controller最終產(chǎn)出的用戶響應(yīng)都是一樣的,不過(guò)后兩者使用的是異步MySQL客戶端魏宽,效率更高腐泻。
任務(wù)調(diào)度器
當(dāng)然决乎,我們還需要一個(gè)任務(wù)調(diào)度器來(lái)執(zhí)行這些生成器,任務(wù)調(diào)度器的實(shí)現(xiàn)方法 在PHP中使用協(xié)程實(shí)現(xiàn)多任務(wù)調(diào)度文中“多任務(wù)協(xié)作”章節(jié)里有介紹派桩,這里不展開(kāi)构诚。
Lumen框架中的代碼保持了同步編程風(fēng)格,而任務(wù)調(diào)度器中使用了異步編程風(fēng)格來(lái)調(diào)用異步MySQL客戶端铆惑。任務(wù)調(diào)度器是在Swoole HTTP服務(wù)器層面使用的范嘱,具體參見(jiàn)Service.php。
連接限制
其實(shí)员魏,每開(kāi)啟一個(gè)Swoole異步MySQL客戶端丑蛤,主進(jìn)程就會(huì)新建一個(gè)線程連接MySQL,若是建立太多連接(線程)撕阎,會(huì)增加自身服務(wù)器的壓力受裹,也會(huì)增加MySQL數(shù)據(jù)庫(kù)服務(wù)器的壓力。
這種利用yield來(lái)調(diào)用異步MySQL客戶端處理慢查詢而產(chǎn)生的線程闻书,暫且稱它為“慢查詢協(xié)程”名斟。
為了限制數(shù)據(jù)庫(kù)連接數(shù)量,我們可以設(shè)置一個(gè)全局變量記錄可新建慢查詢協(xié)程的數(shù)量MAX_COROUTINE
魄眉,開(kāi)啟一個(gè)異步MySQL客戶端時(shí)讓其減一砰盐,關(guān)閉一個(gè)異步MySQL客戶端時(shí)讓其加一;當(dāng)用戶請(qǐng)求慢查詢時(shí)坑律,MAX_COROUTINE
大于0則由異步MySQL客戶端處理岩梳,MAX_COROUTINE
等于0時(shí)則由主進(jìn)程“硬著頭皮”自己處理。
壓力測(cè)試
在4GB+4Core的虛擬機(jī)下晃择,測(cè)試HTTP服務(wù)器與數(shù)據(jù)庫(kù)讀寫(xiě):
一般的快速查詢和快速寫(xiě)入測(cè)試:
- 200并發(fā)50000請(qǐng)求讀冀值,利用HTTP Keepalive,平均QPS:
NginX + PHP-FPM + Lumen + MySQL QPS:521.56
Swoole + Lumen + MySQL QPS:7509.99
- 200并發(fā)50000請(qǐng)求寫(xiě)宫屠,利用HTTP Keepalive列疗,平均QPS:
NginX + PHP-FPM + Lumen + MySQL QPS:449.44
Swoole + Lumen + MySQL QPS:1253.93
慢查詢協(xié)程測(cè)試:
- 16worker的Swoole HTTP服務(wù)器,并發(fā)執(zhí)行
select sleep(1);
請(qǐng)求的最大效率是15.72rps浪蹂; - 16worker x 10coroutine的Swoole HTTP服務(wù)器抵栈,并發(fā)執(zhí)行
select sleep(1);
請(qǐng)求的最大效率是151.93rps。
這里為什么說(shuō)最大效率呢坤次?因?yàn)楫?dāng)并發(fā)量遠(yuǎn)大于worker數(shù)目 x coroutine數(shù)目時(shí)古劲,可開(kāi)啟慢查詢協(xié)程的Swoole HTTP服務(wù)器的效率會(huì)逐漸跌向普通Swoole HTTP服務(wù)器。
select sleep(1);
查詢語(yǔ)句耗時(shí)1秒缰猴,每個(gè)用戶請(qǐng)求都需要1秒時(shí)間來(lái)處理产艾;不過(guò),16進(jìn)程的、每個(gè)進(jìn)程可開(kāi)啟10個(gè)慢查詢協(xié)程的Swoole HTTP服務(wù)器的每秒最多可以處理160個(gè)用戶請(qǐng)求闷堡,而16進(jìn)程的普通Swoole HTTP服務(wù)器每秒最多只能處理16個(gè)用戶請(qǐng)求隘膘。
延伸
其實(shí)利用yield,我們還可以實(shí)現(xiàn)各種各樣的“協(xié)程”缚窿。比如棘幸,Swoole2.1版本已經(jīng)開(kāi)始支持go函數(shù)與通道,后續(xù)我們可能還可以將Lumen Controller中一些IO阻塞的操作的上下文移至go函數(shù)里執(zhí)行倦零,這樣既保留了同步編程的風(fēng)格误续,由達(dá)到異步執(zhí)行的性能。
最后
以上理論扫茅,已經(jīng)在lumen-swoole-http項(xiàng)目中實(shí)現(xiàn)蹋嵌。
lumen-swoole-http
是連接同步編程Lumen和異步編程Swoole的一座橋梁,可以幫助原生PHP的Lumen應(yīng)用項(xiàng)目快速遷移到Swoole HTTP服務(wù)器上葫隙;當(dāng)然也可以快速遷移回去??栽烂。
有興趣的同學(xué)可以嘗試使用:
- 安裝
- 使用
- 配置
- 慢查詢協(xié)程
- ...