Swoole在2.0开始内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在名空间Swoole\Coroutine\*)。

2.0.2或更高版本已支持PHP7

协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程,这会带来了以下优势:

  1. 开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。

  2. 同时由于swoole是在底层封装了协程,所以对比传统的php层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield,这极大的提高了开发效率。

协程API目前针对了TCP,UDP等主流协议client的封装,包括:

  • UDP
  • TCP
  • HTTP
  • Mysql
  • Redis

可以满足大部分开发者的需求。对于私有协议,开发者可以使用协程的TCP或者UDP接口去方便的封装。

启用

Prerequisite:

  • PHP版本要求:>= 5.5,包括5.5、5.6、7.0、7.1
  • 基于swoole_server或者swoole_http_server进行开发,目前只支持在onRequetonReceiveonConnect事件回调函数中使用协程。

swoole2.0需要通过添加--enable-coroutine编译参数启用协程能力,示例如下:

phpize
./configure --with-php-config={path-to-php-config}  --enable-coroutine
make
make install

添加编译参数,swoole server将切换到协程模式。

开启协程模式后,swoole_serverswoole_http_server将以为每一个请求创建对应的协程,开发者可以在onRequetonReceiveonConnect 3个事件回调中使用协程客户端。

相关配置

Swoole\Server的set方法中增加了一个配置参数max_coro_num,用于配置一个worker进程最多同时处理的协程数目。因为随着worker进程处理的协程数目的增加,其占用的内存也会增加,为了避免超出php的memory_limit限制,请根据实际业务的压测结果设置该值,默认为3000。

使用示例

$client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
$client->connect("127.0.0.1", 88880.5);
//调用connect将触发协程切换
$client->send("hello world from swoole");
//调用recv将触发协程切换
$ret = $client->recv();
$client->close();
echo $ret;

当代码执行到connect()和recv()函数时,swoole会触发进行协程切换,此时swoole可以去处理其他的事件或者接受新的请求。当此client连接成功或者后端服务回包后,swoole server会恢复协程上下文,代码逻辑继续从切换点开始恢复执行。开发者整个过程不需要关心整个切换过程。具体使用可以参考client的文档。

注意事项

  1. 全局变量:协程使得原有的异步逻辑同步化,但是在协程的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及static变量的一致性。
  2. 请勿在以下场景中触发协程切换:
    • 析构函数
    • 魔术方法__call()
  3. gcc 4.4下如果在编译swoole的时候(即make阶段),出现gcc warning:dereferencing pointer ‘v.327’ does break strict-aliasing rulesdereferencing type-punned pointer will break strict-aliasing rules 请手动编辑Makefile,将CFLAGS = -Wall -pthread -g -O2替换为CFLAGS = -Wall -pthread -g -O2 -fno-strict-aliasing,然后重新编译make clean;make;make install
  4. 与xdebug、xhprof等zend扩展不兼容,例如不能使用xhprof对协程server进行性能分析采样。
  5. 在PHP5中,原生的call_user_func和call_user_func_array中无法使用协程client,请使用\Swoole\Coroutine::call_user_func和\Swoole\Coroutine::call_user_func_array代替
  6. 在PHP7中可直接调用原生的call_user_func和call_user_func_array
 

方法列表

getDefer()


bool getDefer();
  • 返回值:返回当前设置的defer

setDefer()


bool setDefer([bool $is_defer = true]);
  • $is_defer:bool值,为true时,表明该Client要延迟收包,为false时,表明该Client非延迟收包,默认值为true
  • 返回值:设置成功返回true,否则返回false。只有一种情况会返回false,当设置defer(true)并发包后,尚未recv()收包,就设置defer(false),此时返回false。
  • 如果需要进行延迟收包,需要在发包之前调用

recv()


mixed recv();
  • 返回值:获取延迟收包的结果,当没有进行延迟收包或者收包超时,返回false。
  •  

并发调用

Client并发请求


在协程版本的Client中,实现了多个客户端并发的发包功能。

通常,如果一个业务请求中需要做一次redis请求和一次mysql请求,那么网络IO会是这样子:

redis发包->redis收包->mysql发包->mysql收包

以上流程网络IO的时间就等于 redis网络IO时间 + mysql网络IO时间。

而对于协程版本的Client,网络IO可以是这样子:

redis发包->mysql发包->redis收包->mysql收包

以上流程网络IO的时间就接近于 MAX(redis网络IO时间, mysql网络IO时间)。

现在支持并发请求的Client有:

  • Swoole\Coroutine\Client
  • Swoole\Coroutine\Redis
  • Swoole\Coroutine\MySQL
  • Swoole\Coroutine\Http\Client

除了Swoole\Coroutine\Client,其他Client都实现了defer特性,用于声明延迟收包。

因为Swoole\Coroutine\Client的发包和收包方法是分开的,所以就不需要实现defer特性了,而其他Client的发包和收包都是在一个方法中,所以需要一个setDefer()方法声明延迟收包,然后通过recv()方法收包。

 

协程版本Client并发请求示例代码:

<?php
$server = new Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE);

$server->set([
    'worker_num' => 1,
]);

$server->on('Request', function ($request, $response) {

    $tcpclient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
    $tcpclient->connect('127.0.0.1', 95010.5)
    $tcpclient->send("hello world\n");

    $redis = new Swoole\Coroutine\Redis();
    $redis->connect('127.0.0.1', 6379);
    $redis->setDefer();
    $redis->get('key');

    $mysql = new Swoole\Coroutine\MySQL();
    $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'user',
        'password' => 'pass',
        'database' => 'test',
    ]);
    $mysql->setDefer();
    $mysql->query('select sleep(1)');

    $httpclient = new Swoole\Coroutine\Http\Client('0.0.0.0', 9599);
    $httpclient->setHeaders(['Host' => "api.mp.qq.com"]);
    $httpclient->set([ 'timeout' => 1]);
    $httpclient->setDefer();
    $httpclient->get('/');

    $tcp_res  = $tcpclient->recv();
    $redis_res = $redis->recv();
    $mysql_res = $mysql->recv();
    $http_res  = $httpclient->recv();

    $response->end('Test End');
});
$server->start();
 

实现原理

Swoole2.0基于setjmplongjmp实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。

示例代码

$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

#1
$server->on('Request', function($request, $response) {
    $mysql = new Swoole\Coroutine\MySQL();
    #2
    $res = $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    ]);
    #3
    if ($res == false) {
        $response->end("MySQL connect fail!");
        return;
    }
    $ret = $mysql->query('show tables', 2);
    $response->end("swoole response is ok, result=".var_export($ret, true));
});

$server->start();
  • 此程序仅启动了一个1个进程,就可以并发处理大量请求。
  • 程序的性能基本上与异步回调方式相同,但是代码完全是同步编写的

运行过程

  • 调用onRequest事件回调函数时,底层会调用C函数coro_create创建一个协程(#1位置),同时保存这个时间点的CPU寄存器状态和ZendVM stack信息。
  • 调用mysql->connect时发生IO操作,底层会调用C函数coro_save保存当前协程的状态,包括Zend VM上下文以及协程描述信息,并调用coro_yield让出程序控制权,当前的请求会挂起(#2位置)
  • 协程让出程序控制权后,会继续进入EventLoop处理其他事件,这时Swoole会继续去处理其他客户端发来的Request
  • IO事件完成后,MySQL连接成功或失败,底层调用C函数core_resume恢复对应的协程,恢复ZendVM上下文,继续向下执行PHP代码(#3位置)
  • mysql->query的执行过程与mysql->connect一致,也会进行一次协程切换调度
  • 所有操作完成后,调用end方法返回结果,并销毁此协程

协程开销

相比普通的异步回调程序,协程多增加额外的内存占用。

  • Swoole2.0协程需要为每个并发保存zend stack栈内存并维护对应的虚拟机状态。如果程序并发很大可能会占用大量内存,取决于C函数、ZendVM 调用栈深度
  • 协程调度会增加额外的一些CPU开销

压力测试

  • 环境:Ubuntu16.04 + Core I5 4核 + 8G内存 PHP7.0.10
  • 脚本:ab -c 100 -n 10000 http://127.0.0.1:9501/

测试结果:

Server Software:        swoole-http-server
Server Hostname:        127.0.0.1
Server Port:            9501

Document Path:          /
Document Length:        348 bytes

Concurrency Level:      100
Time taken for tests:   0.883 seconds
Complete requests:      10000
Failed requests:        168
   (Connect: 0, Receive: 0, Length: 168, Exceptions: 0)
Total transferred:      4914560 bytes
HTML transferred:       3424728 bytes
Requests per second:    11323.69 [#/sec] (mean)
Time per request:       8.831 [ms] (mean)
Time per request:       0.088 [ms] (mean, across all concurrent requests)
Transfer rate:          5434.67 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       2
Processing:     0    9   9.6      6      96
Waiting:        0    9   9.6      6      96
Total:          0    9   9.6      6      96

Percentage of the requests served within a certain time (ms)
  50%      6
  66%      9
  75%     11
  80%     12
  90%     19
  95%     27
  98%     43
  99%     51
 100%     96 (longest request)