引出问题如下:
当一个基于http协议的papi接口中,有发起对第三方http接口的调用,且有多次数据库IO时,http请求代码放最前面和最后面性能有差异吗?
前提如下:
# 数据库有连接池,数量假设100, ab压测并发的客户端数为2000,即(ab -c2000 )
# 使用Swoole协程http server。
php代码如下:
<?php
<?php
/**
* Created by PhpStorm.
* User: randy
* Date: 2021/2/22
*/
$http = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_PROCESS);
\co::set(['hook_flags' => SWOOLE_HOOK_ALL]);
$http->on('start', function ($server) {
echo "Swoole http server is started at http://127.0.0.1:9501\n";
});
$http->on('workerStart', function ($server) {
//todo init redis conn
class CachePool
{
/**
* @var
*/
private static $redisCli;
const POOL_SIZE = 32;
/**
* @var \Swoole\Coroutine\Channel
*/
private static $pool;
public static function initPool()
{
self::$pool = new \Swoole\Coroutine\Channel(self::POOL_SIZE);
for ($i = 0; $i < self::POOL_SIZE; $i++) {
$conn = new Redis();
$conn->connect("127.0.0.1", 6379);
self::$pool->push($conn);
}
}
/**
* @return Redis
*/
public static function getConn()
{
return self::$pool->pop();
}
public static function recycle($conn)
{
return self::$pool->push($conn);
}
}
CachePool::initPool();
});
$http->on('request', function ($request, $response) use ($http) {
//position 1
$httpCli = new Swoole\Coroutine\Http\Client("127.0.0.1", 9090);
$httpCli->get("/");
$body = $httpCli->body;
$redis = CachePool::getConn();
if (empty($redis)) {
var_dump("error");
return;
}
defer(function () use ($redis) {
CachePool::recycle($redis);
});
for ( $i=0; $i<8; $i++) {
$redis->get("test_key1");
}
//position 2
$response->end("OK");
});
$http->start();
该思考题的意思就是 发起http请求的代码出于 postion1 和处于 positon2时,是否有性能上的差距?
压测命令如下: ab -n20000 -c2000 -k http://127.0.0.1:9501/
结果:http请求放最前面比放最后面平均qps 高出3倍左右,分别压测6轮,得出的平均值
为什么呢?
给出解答:(协程下面用coroutine代替,输入法不好打)
1、http请求放前面:当ab并发上来2000个客户端时,swoole会在瞬间创建2000个coroutine,且这2000个都在交替的执行,当执行到有数据库IO时,开始争夺连接池的连接,这时候因为连接池有限,实际上只有100个coroutine在并发的执行。当100个coroutine在并发数据库IO时,1900个http请求可能都已经返回结果了,所以后续的coroutine基本上省去了发起http请求的时间,直接从$redis = CachePool::getConn();开始执行代码。
2、http请求放后面:2000个并发一上来,前100个从数据库连接池拿到连接的coroutine,交替执行,剩下1900个在等待有空闲连接出现,这步就是在浪费cpu的时间了。当数据库io执行完成,100个coroutine交替发起http请求。这里有个关键问题时,协程没有执行完成,没有defer回收之前,连接是不会放进连接池的。
总结:
根本原因就是:连接池有数量限制压住了协程的并发,因为连接数用光之后,剩下的coroutine只能干等,而http请求你并发来多少,对应就有多少个coroutine交替并发的执行。1比2快在很多coroutine已经提现把http请求结果拿回来了,只需要执行数据库IO了。
上面的问题可以抽象成一个模型:
2000辆车 同时从地点A 到 B,距离1公里,假设所有车都是匀速20m/s,不考虑加塞什么的特殊情况
场景1: 假设A到B的公路设计为: 前500米2000车道宽,后500米100车道宽
场景2: 假设A到B的公路设计为: 前500米100车道宽,后500米2000车道宽
哪种场景2000辆车先到地点B?
从A到B耗时 = 1000/20 = 50秒
场景1 其实就是 先2000并发执行一会,然后100并发执行一会, 前500米耗时=25秒 , 后500米耗时= (2000/100) *25 = 500 总耗时= 500 + 25 = 525秒
场景2 其实就是100一组,并发执行,总耗时 = (2000/100 ) * 50 = 1000 秒