因项目需要,需要做php框架的后端技术选型,于是开始着手测试基于swoole的框架swoft
与laravel的扩展包laravel-swoole
进行评估。
刚开始打算是在cygwin
中使用laravel-s
这个laravel
扩展包,然而报出了一个cli_set_process_title() failed
异常。
找了半天原因,从swoole的官方文档中看到,在macOS与低版本的linux系统中,是无法使用cli_set_process_title
这个函数的。搜索了半天,也没有找到有效的解决方案,于是最后选择了替代方案:laravel-swoole
。
测试环境:
阿里云服务器4C8G
,数据库与服务器使用内网通信,排除网络io的干扰。
测试环境为线下的测试服务器与测试数据库,测试条件是查询根据传过去的用户uid查出一条用户记录,并返回查询结果,没有使用redis
、memcache
等缓存。
测试工具:
- ab
查询sql:
代码语言:javascript复制select * from where id = xxxx
测试过程中会出现以下问题:
代码语言:javascript复制[2020-12-15 10:43:50 *3602.1] NOTICE finish (ERRNO 1004): send 5 bytes failed, because session#2 is closed
这个问题是AB
工具本身的问题,具体的原因可以参考:https://wiki.swoole.com/wiki/page/1527.html
benchmark:
- 测试在持续60秒内在不同的并发数下的效果,具体执行条件为:
ab -t 60 -c 2000 http://127.0.0.1:1215/api/user-info/2052
,其中c
为变量,意思为并发数。
测试效果对比:
- 并发数为10:
- 并发数为100:
- 并发数200:
- 并发数500:
- 并发数1000:
- 并发数1500:
- 并发数2000:
- 并发数3000:
- 并发数5000:
运行时数据库状况:
关键指标:
- Complete requests:请求完成数
- Failed requests:请求失败数
- Connection Times:网络消耗时间。
- Time per request(mean): 服务器收到请求后,响应页面的平均时间
- Time per request(mean, across all concurrent requests): 并发的每个请求平均消耗时间
- Percentage of the requests served within a certain time (ms): 一定的时间内,完成的请求数所花的时间比。
总结:
- 从并发的对比图中,从请求成功数与请求失败数来看,
swoft
与laravel-swoole
相比,成功率较高;从网络消耗时间对比,由于有swoft
有连接池的存在,明显可以看出,网络IO的时间要优于laravel-swoole
;从响应页面的平均时间与并发的每个请求平均消耗时间看,swoft
性能还是强于laravel-swoole
;从一定的时间内,完成的请求数所花的时间比,swoft
大部分的情况下,处理完成的平均处理时间是优于laravel-swoole
。但是随着并发数的上升,请求的最大处理时间与laravel-swoole
对比,即最完成全部请求来需要花费的时间,性能相对来说差,综合性能上来看,swoft
有一定的优势。
没覆盖测试到的:laravel-swoole
加上数据库连接池中间件之后的效果。
从初步使用体验看,swoft
要求更高,约束更强,特别是引入了注解概念,所谓I注解即路由,增强了代码的简洁性,同时牺牲了代码的可读性。
语法上,使用PHP7的强类型语法约束与模型数据字段的映射,好处是增强了代码的稳健性,但是缺点也很明显:降低了php的开发效率。
swoft
文档比较简单,没有过多的停留在概念性解释上面,结合在搭建测试环境中遇到的问题,坑还是有不少,相关的搜索结果与laravel
相比会少很多,有些问题可能会需要从框架源码着手解决,因此对使用者会有一定的要求。
数据交换上,swoft
提供http
、rpc
、websocket
等支持,不再需要再引入第三方依赖,而laravel-swoole
作为laravel
的扩展包,主要是支持http
;在事件的支持上,swoft
与laravel
都支持同步与异步的事件驱动,在异步处理方面,swoft
是基于swoft
的协程,而laravel
是基于队列。
数据库驱动上,目前swoft
官方的文档上只有mysql
与redis
的驱动,如果项目中有用到mongoDB
、PostgreSQL
、SSDB
等其他数据库则需要使用第三方的轮子或自己造。
附测试使用swoft遇到的一个有意思的问题:
- 开启协程有
srun
与sgo
,两者有何不同?sgo
:开启新协程。srun
:启动协程并等待执行结束。 文档在这一点没说清楚,对两者的说明,网上搜索也没几个相关内容。 在swoft的命令行测试对比的结果:
echo 'begin'.PHP_EOL;
sgo(function(){
Co::sleep(2);
echo "middle".PHP_EOL;
});
echo "end".PHP_EOL;
此时输出:
代码语言:javascript复制begin
end
middle
如果换成:
代码语言:javascript复制echo 'begin'.PHP_EOL;
srun(function(){
Co::sleep(2);
echo "middle".PHP_EOL;
return true;
});
echo "end".PHP_EOL
那么此时输出:
代码语言:javascript复制begin
[WARNING] SwoftCo:run(83) Already is in coroutine, not need to use `run`!
middle
end
从上面对比看出,顺序执行了(即已经做了同步),但是会抛出一个警告,已经是协程环境不要使用run
方法,这可能就是框架作者反复强调再次强调,框架中只能使用 sgo 函数创建协程。的原因之一。因此,我们只能用sgo
方法在框架内开协程,srun
方法的应用场景更多的应该是在自定义进程等非框架内使用的。
可是如果我既想做顺序输出又不想抛出这个警告呢?
显然我们会注意到sgo方法会有第二个入参wait。然而,在框架文档里没有解释的,sgo方法wait到底是嘛玩意?既然默认是false,什么情况应该用true呢?既然文档没有,那么只能看源代码了。跟到代码里面去会发现这么一段:
代码语言:javascript复制return Coroutine::create(function () use ($callable, $tid, $wait) {
// Current cid
$id = Coroutine::getCid();
try {
// Storage fd
self::$mapping[$id] = $tid;
if ($wait) {
Context::getWaitGroup()->add();
}
PhpHelper::call($callable);
} catch (Throwable $e) {
Error::log(
"Coroutine internal error: %snAt File %s line %dnTrace:n%s",
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
// Trigger co error event
Swoft::trigger(SwoftEvent::COROUTINE_EXCEPTION, $e);
}
if ($wait) {
// Trigger defer
Swoft::trigger(SwoftEvent::COROUTINE_DEFER);
Context::getWaitGroup()->done();
}
看到了熟悉的waitgroup
,waitgroup
在swoole
的文档中就提到了它的作用,用来了做同步的, 一般操作有3个方法add
、done
,以及用来同步等待的wait
。那么同理,它在这里,肯定也是用来做同步的。那么让我们试试,看不能能输出预期的:
begin
middle
end
代码改成如下:
代码语言:javascript复制echo 'begin'.PHP_EOL;
sgo(function(){
Co::sleep(2);
echo "middle".PHP_EOL;
},true);
echo "end".PHP_EOL;
然而,输出结果并没有如预期,实际输出:
代码语言:javascript复制begin
end
middle
等等,回过头去看sgo
方法的实现,好像它这里少了点什么?
是的,没有看到哪里调用wait
方法做同步。
于是,我们跟到 Context::getWaitGroup()
里面去看代码:确实里面有个wait
方法。我们把这个函数加进去看看效果:
echo 'begin'.PHP_EOL;
sgo(function(){
Co::sleep(2);
echo "middle".PHP_EOL;
},true);
Context::getWaitGroup()->wait();
echo "end".PHP_EOL;
实际输出:
代码语言:javascript复制begin
middle
end
符合我们的预期,也就是意味着,如果我们在sgo
方法有同步需求的时候,需要自己手动在业务代码处调用Context::getWaitGroup()->wait();
这一个方法使数据同步。