PHP异步并行扩展 - Swoole

一、概述

1.1 Swoole简介

Swoole是一个PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。 Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。 Swoole可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领域。 使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升,更加专注于开发创新产品。

1.2 Swoole优势

1.3 使用场景

Swoole为了弥补PHP在网络编程、异步IO等方面的不足而产生的。它用C语言实现网络通信及其中的异步等逻辑,PHPer同样可以用PHP写出高性能的服务端程序。 使用Swoole可以提供基于tcp的即时通信、异步任务调度、Websocket服务、异步IO等功能。

1.4 开发模式

Swoole给PHPer提供了一种全新的开发模式,其编程思路不同于传统的PHPWeb编程,更类似Python、Nodejs,在命令行通过PHP执行一个脚本即启动服务,中间该脚本有更新需要重新启动服务。Swoole实现了HTTP,所以可以不依赖Nginx等Web服务器。

1.5 研究目标

查看是否可以使用Swoole实现异步任务调度,如发送短信、推送、缓存更新等,其中的效率怎样?

二、Swoole功能介绍

2.1 Swoole安装

这里安装1.8.8-stable,PHP为PHP7.0.5,安装扩展同普通扩展一样。

# /usr/local/webserver/php7.0.5/bin/phpize
# ./configure --with-php-config=/usr/local/webserver/php7.0.5/bin/php-config
# make
# make install

然后修改php.ini添加到扩展里即可。

2.2 Swoole HttpServer

2.2.1 Web Server实现

Swoole实现了HTTP,一个简单HTTP示例如下:

<?php
$http = new swoole_http_server("0.0.0.0", 9501);
 
$http->on('request', function ($request, $response) use($http) {
    $response->end("Hello World");
});
 
$http->start();

在命令行启动该脚本即可监听9501端口,实现基本的Web服务功能。实现后第一个想到的问题是他的性能和PHP+Nginx的性能怎样?

2.2.2 压力测试

这里以Hello World作为参考,100个线程不断执行,下图是Swoole的压测结果:

可以看到,Hello World的吞吐量为4363,其中一直飙到5000,但这个时候开始出现错误了,稳定性上面打折扣,平均值18ms,90%在25ms以内,速度上还可以。接下来看看Nginx+PHP组合的Hello World

可以看到Nginx未出现Error的情况,效率也不比Swoole差。Swoole有一点优势就是支持异步去处理一下业务逻辑。比如:

<?php
$http = new swoole_http_server("0.0.0.0", 9501);
$http->set(array(
    'daemonize' => 0,  //以守护进程执行
    'max_conn' => 10240,
    'max_request' => 1024,
    'task_max_request' => 0,
    'dispatch_mode' => 2,
    'worker_num' => 4,       //一般设置为服务器CPU数的1-4倍
    'task_worker_num' => 8,  //task进程的数量
    "task_ipc_mode " => 3 ,  //使用消息队列通信,并设置为争抢模式
));
$http->on('request', function ($request, $response) use($http) {
    $http->task("param");
    $response->end("Hello World");
});
$http->on('Task', function(swoole_server $http, $task_id, $from_id, $data) {
    //do task
    return true;
});
$http->on('Finish', function(swoole_server $http, $task_id, $data) {
    echo "Task {$task_id} finish\n";
});
$http->start();

当然,PHP也可以直接起一个进程去执行脚本。根据压测的结果Http Server的稳定成都还不算满意。接下来进行一下TCP方式的测试。

2.3 Swoole TCP Server

2.3.1 TCP Server实现

设想一个这样子的场景,由PHP发送一个请求给TCP Server,Server可以同步返回也可以异步返回。 异步返回比如,将一些缓存的更新、短信的发送、推送等事件发送给TCP Server即可,Server收到请求后即返回; 同步的返回比如,获取用户数据,如果有缓存就直接从缓存取,没有缓存就读取数据后设置缓存并同步返回,这样子Server类似于MVC中的Model层,PHP做表现层。

最简单的Server写法写入:

$serv = new swoole_server("127.0.0.1", 9501);
$serv->set(array(
    'worker_num' => 8,   //工作进程数量
    'daemonize' => true, //是否作为守护进程
));
$serv->on('connect', function ($serv, $fd){
    echo "Client:Connect.\n";
});
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
    $serv->send($fd, 'Swoole: '.$data);
    $serv->close($fd);
});
$serv->on('close', function ($serv, $fd) {
    echo "Client: Close.\n";
});
$serv->start();

可以通过telnet连接测试,我们这里将Server与Task组合起来实现同步和异步。

小知识,TCP的封包过程一般有两种格式,Swoole也都支持。

这里为了压测方便,采用EOF的方式(分隔符为chr(35)),具体实现上还是采用自定义封包协议,示例如下:

$swoole_server = new swoole_server("0.0.0.0", 9501);
$swoole_server->set(array(
    'daemonize' => 0,  //以守护进程执行
    'max_conn' => 10240,
    'max_request' => 1024,
    'task_max_request' => 0,
    'dispatch_mode' => 2,
    'worker_num' => 4,   //一般设置为服务器CPU数的1-4倍
    'task_worker_num' => 8,  //task进程的数量
    "task_ipc_mode " => 3 ,  //使用消息队列通信,并设置为争抢模式
    "log_file" => "./taskqueueu.log" ,//日志
    "open_length_check" => true,
    "package_max_length" => 8192,
    /*
    "package_length_type" => "N",
    "package_length_offset" => 0,*/
    "package_body_offset" => 11,
     
    'open_eof_check'=> true,
    'package_eof' => chr(35),
));
$swoole_server->on('Start', function($server) {
    echo "start:".convert(memory_get_usage(true));
});
// 请求:4字节长度 + 2字节命令 +  4字节请求ID + 1字节同步异步(1同步,0异步) + JSON内容
// 返回:4字节长度 + 2字节状态码 + 4字节请求ID + JSON内容
$swoole_server->on('Receive', function(swoole_server $server, $fd, $from_id, $data) {
    if(strlen($data) < $server->setting["package_body_offset"]) {
        return ;
    }
    $param = unpack("Nlen/ncmd/Nid/Csync" , substr($data, 0, $server->setting["package_body_offset"]));
    //print_r($param);
    $body = substr($data, $server->setting["package_body_offset"] , $param['len']);
    if($param["sync"] == 0) {
        $server->task( $body );
        $server->send($fd, set_task_output(0, $param["id"]));
    } else {
        $rtn = handle($body);
        $server->send($fd, set_task_output(0, $param["id"], array("body" => $rtn)));
    }
});
$swoole_server->on('Task', function(swoole_server $server, $task_id, $from_id, $data) {
    return handle($data);
});
$swoole_server->on('Finish', function(swoole_server $server,$task_id, $data) {
    echo "Task {$task_id} finish, Memory ".convert(memory_get_usage(true))."\n";
});
$swoole_server->on('Close', function($server, $fd) {
    //echo "Client Close{$fd}, Memory ".convert(memory_get_usage(true))."\n";
});
$swoole_server->start();
function set_task_output($code = 0, $id = 0, $data = array())
{
    $len = 10;
    if(! empty($data)) {
        $data = json_encode($data);
    } else {
        $data = "";
    }
    $len += strlen($data);
    $data = pack("N", $len) . pack("nN", $code, $id) . $data.chr(35);
    return $data;
}
function handle($data)
{
    //echo "Data = {$data}\n";
    //sleep(3);
    return $data . str_repeat(".", rand(1, 1000));
}
function convert($size){
    $unit=array('b','kb','mb','gb','tb','pb');
    return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];
}

因为swoole仅能在Linux下使用,为方便开发这里不使用扩展自带的swoole client请求,直接使用fsockopen函数

$fp = fsockopen("192.168.1.168", 9501, $errno, $errstr, 30);
if (! $fp) {
    //exit;
}
$ramd = rand(1, 1000);
$data = json_encode(array(
    "cmd" => "get_user_info".$ramd,
    "param" => str_repeat("*", $ramd)
));
$sync = 0;
$msg = pack("N" , strlen($data)). pack("n", rand(0, 65535)) . pack("N", rand()). pack("C", $sync).  $data . chr(35);
//file_put_contents("pack.txt", bin2hex($msg.chr(35)));
fwrite($fp, $msg);
$length = 4;
$read = 0;
$ret = '';
while ($read < $length && ($buf = fread($fp, $length - $read))) {
        $read += strlen($buf);
        $ret .= $buf;
}
$ret = unpack('N', $ret);
$length = $ret[1] - 4;
$read = 0;
$ret = '';
while ($read < $length && ($buf = fread($fp, $length - $read))) {
    $read += strlen($buf);
    $ret .= $buf;
}
$param = unpack("ncode/Nid", substr($ret, 0, 6));
print_r($param);
$data = substr($ret, 6);
if($data) {
    print_r(json_decode($data, true));
}
fclose($fp);

这样子封包过程和解包过程就实现了。下面也看看压测的结果。

2.3.2 压力测试

效率上也没有太大问题,但跟Http Server一样,同样存在不稳定,当压测到一定成都之后Task就挂掉了,无法接收新的任务,需要重启服务。官方对task的使用说明:

task操作的次数必须小于onTask处理速度,如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求

目前的访问量来看不会出现这么大量,但不能确保系统不会做一些批量操作之类的,这样导致服务挂掉的情况。而稳定性在生产环境上却是首要考虑的问题。

2.4 其他功能

Swoole还提供了很多功能,比如WebScoket、AsyncIO、内存操作、进程操作,Mysql、Reds连接池等等,但和上面某类似的感觉,会存在一些不靠谱的情况。比如异步IO中的异步写文件:

swoole_async_write("a.txt", "Hello World", -1);

当不指定第四个参数回调函数时,执行报段错误,若是传入一个NULL则不会。但文档上标注的是一个可写参数。

三、总结

整体来说,Swoole给PHPer提供了一种新的开发思路,是件不错的事情。测试脚本中没有什么业务的前提下看到内存的控制是不错的,直到挂掉也还是占用2M的内存。Swoole也提供了一些参数来控制进程执行请求数的数量,当达到这个数量后进程后重启。但文档的欠缺,稳定性上的不足是需要考虑的,社区的活跃程度也不高。我觉得官方需要做的不是提供一个又一个的功能,而是从程序的健壮性和完善文档方面来考虑,让PHP在服务端网络编程方面也能占一席之地。

最后,虽然官网声称有很多公司在用,在通过自测的情况来看,Swoole在生产环境中的使用还需要观望。

-- EOF --
发表于: 2016-08-12 14:24
标签: PHP Swoole