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系统无可厚非,主要问题出现在:

从NginX到PHP-FPM

NginX利用IO多路复用机制epoll,极大地减少了IO阻塞等待,可以轻松应对C10K。可是每次NginX将用户请求传递给PHP-FPM时,PHP-FPM总是需要从新加载PHP项目代码:创建执行环境,读取PHP文件和代码解析、编译等操作一次又一次的重复执行,造成不小的消耗。

从PHP-FPM到MySQL

由于PHP代码本身是同步执行,PHP-FPM连接MySQL查询数据时,只能空闲等待MySQL返回查询结果。一个查询语句执行时间可能会需要几秒钟,期间PHP-FPM若是能暂时放下当前用户慢查询请求,而去处理其他用户请求,效率必然有所提高。

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代码编译的开销。

压力测试

在4GB+4Core的虚拟机下,测试HTTP服务器的静态输出:

  • 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应用中能够实现数据库异步查询,才能更大的突破性能瓶颈。

虽然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服务器上;当然也可以快速迁移回去😂。
有兴趣的同学可以尝试使用:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 并发IO问题一直是服务器端编程中的技术难题,从最早的同步阻塞直接Fork进程,到Worker进程池/线程池,到现在...
    零一间阅读 1,714评论 1 34
  • 出处:韩天峰 网址:rango.swoole.com/archives/508 并发IO问题一直是后端编程中的技术...
    meng_philip123阅读 2,402评论 1 38
  • 更改ip和dnsVi /etc/sysconfig/network-scripts/ifcfg-eth0vi /...
    Xwei_阅读 1,814评论 0 3
  • 大家好!我是一名高校老师,也是一位妈妈,今日分享一下学习Tyger课程体会。我和女儿一起学习了tyger老师课程5...
    wangIiIy阅读 666评论 2 2
  • 这世界总是无法预料地前行。很多事情的发生,并不遵循因果。比如突然的爱和莫名其妙的厌倦。人们终其一生都在试图掌控或者...
    南烟客丶江郎阅读 244评论 0 0