关于协程:
协程:其实就是用户态的线程,关于swoole协程的切换机制可以参考我的另外一篇文章
swoole从4.x版本开始后,加入了协程的特性,用法上和golang很相似,对于普通写业务的phper来说,基本上和写同步代码没什么区别。很多人都使用协程来写server代码,但是当你认真思考生产实际环境时,你会发现一些问题:
# swoole官方提供的数据库协程客户端只有:redis、mysql。当项目中使用了其他的数据库客户端时,堵塞IO就无法hook,协程直接陷入,上下文无法yield让出。所以这种场景没办法使用协程,
# php pecl那么多的扩展库,官方不可能单独为swoole去适配协程客户端。协程实现容易,但是生态却很难起来,历史包袱太重了。
那么没了协程的swoole就真的无法高性能了吗?no!Swoole 异步风格服务器已经能满足大部分的场景。且听我细细分析:
1、 在使用协程开发实际项目时,项目中的数据库连接,肯定会有连接池。
因为在swoole协程风格的server中,一个客户端由一个coroutine维护 (因为协程相对“廉价”,创建和销毁开销对比进程、线程小很多), 如果不用连接池,则mysql或者redis的连接数会随着客户端数量而增加,假设app同时有1万个客户端在线,那不可能创建1万个mysql或者redis连接。而且如果集群模式下,server假设有N台。那数据库服务器就要hold N* 1万的连接数。数据库的性能反而会严重受到影响。
2、当数据库连接池数量一定时,在某一个瞬间,并发上来,可能一下子就把连接池用光了,后续的请求要等待有新的连接可用。而swoole协程server中 一次http请求由一个协程管理,请求没有结束,协程不会把连接放回连接池的,所以这个时候swooler同步模式woker线程池数量和协程的数据库连接池一样时没差,因为worker线程池是一个worker进程使用一个连接实例。一个是连接池空了要等待可用连接,一个是worker进程都在处理请求,要等待空闲的worker进程,所以这里的差别开始接近
3、并发大时,协程虽然上下文切换开销小很多,但是架不住协程的数量等于并发的数量,而进程池就固定几百。 也就是说几百个进程上下文切换和成千上万的协程上下文切换开销上的差距又缩小了一些。
基于上述的分析,协程对比同步worker进程池模型,性能上不会有压倒性的优势。
下面给出压测数据:
协程swoole server下使用100个redis连接池 和异步swoole server开100个worker进程 。每次请求10次redis io 和每次请求20次redis io 的情况。异步多进程模型的性能有协程版本的75% 笔者有实际压测:
系统:virtualbox虚拟机 centos7
cpu:intel i3 4核cpu
redis:同一台,benchmark 10w qps左右
压测脚本: ab -n200000 -c2000 -k http://127.0.0.1:9501/
压测场景:
# echo server (echo server即server受到请求不处理任务逻辑,直接输出"hello world"的情况)
ab压测对比了协程风格的echo server和异步风格echo server,qps都是5w+。没什么差别。
# 一次http请求中有10次redis io的情况:
异步风格server和协程风格的server性能相差无几。异步server qps 4900+, 协程风格qps 6500+
给出异步多worker进程风格代码
<?php
/**
* Created by PhpStorm.
* User: randy
* Date: 2021/2/22
*/
$http = new Swoole\Http\Server('127.0.0.1', 9501);
$http->set([
'worker_num' => 100,
'enable_coroutine' => false,
]);
$http->on('start', function ($server) {
echo "Swoole http server is started at http://127.0.0.1:9501\n";
});
$http->on('workerStart', function ($server) {
class CachePool
{
/**
* @var Redis
*/
private static $redisCli;
public static function getInstance()
{
if (is_null(self::$redisCli)) {
self::$redisCli = new Redis();
self::$redisCli->connect("127.0.0.1", 6379);
}
return self::$redisCli;
}
}
});
$http->on('request', function ($request, $response) use ($http) {
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$val = CachePool::getInstance()->get("test_key1");
$response->end("OK" . "worker_id -> " . $http->worker_id);
});
$http->start();
压测结果1:当一个http接口,都是纯redis操作时,协程+连接池 对比 worker进程池同步堵塞 方式 qps为 100:75
压测结果2: 当一个http接口,都是纯mysql操作时,协程+连接池 对比 worker进程池同步堵塞 方式 qps 接近 1:1 (一个http请求有3次mysql IO,连接池数量和进程池数量都控制在128)
压测结论:当http接口中操作的网络IO,单次执行时长越长,协程的性能优势就越不明显
linux中多进程和多线程上下文切换的开销相差不大, 切换一次都在5us左右,但是线程的创建和销毁比进程快很多,由于实际项目都是进程池或者线程池,只有上下文切换,没有创建和销毁了。所以可以得出:进程池和线程池(数量都在几百以内时)在上下文切换上相差不太大
在Java SpringBoot中,使用单进程+多线程模型,写同步堵塞代码 + 线程池来解决高并发。而swoole非协程server使用多进程单线程模型,写同步堵塞代码+进程池解决高并发。
因为进程和线程切换开销不会有太大差异,而进程池或线程池正常都是几百以内。所以切换性能损耗应该是接近的, 单进程多线程需要解决线程间同步的问题性能有折损,而多进程单线程模型多了IPC通信开销,所以这边的损耗也不会有质的差距。
swoole多进程同步方式的模型和 java系的框架性能差不多。而java这种模型在许多公司的大量项目中都在使用,也很少听说使用java协程库fiber的。所以这种架构模型对付高并发足够了,剩下的就是加机器的事情了。
总结:
Swoole的协程确实性能比同步的高一些,而且写代码方式和同步没区别,但是协程生态不完善而且也看不到发展前景。而且很多fpm下的项目基本无法切到协程中,除了修改框架代码麻烦之外,根本原因可能是因为使用了协程不支持的php扩展。
swoole多进程同步堵塞方式,比起fpm性能还是高出非常多,fpm项目切入swoole还是能节约不少的机器。
个人建议:使用swoole的异步风格server + worker进程池的模型,既能接近协程的性能,又能兼容fpm项目以及官方大量的PHP扩展。只要改一些框架代码和注意一些业务代码的写法即可切入。