PHP的curl_multi可以使用多线程处理http请求,一定程度上可以提高请求接口的效率。但是,启用多线程也是会消耗资源的事情,那么使用curl_multi时每次同时并发多少个请求合适呢?
让我们来通过实验说话吧,先说说实验的结论:
实验结论 1)首先要检查发起请求服务器的网络带宽是否正常,避免请求服务器出现带宽瓶颈。 2)curl_multi并发请求并发数有一个阈值,过高的并发不能提升效率,反而会导致请求不成功,这个阈值与服务端的性能有关。 3)CURLOPT_TIMEOUT必须跟进实际业务设置合适的值
实验:通过curl_multi请求远程服务器上的一个接口,接口只是简单的返回字符串'1',验证请求成功的比例。
<?php $max_request = $argv[1]; $ch_list = array(); $multi_ch = curl_multi_init(); for ($i = 1;$i <= $max_request;++$i) { $ch_list[$i] = curl_init("http://www.xxx.com/a.php"); curl_setopt($ch_list[$i], CURLOPT_RETURNTRANSFER, true); curl_setopt($ch_list[$i], CURLOPT_TIMEOUT, 10); curl_multi_add_handle($multi_ch, $ch_list[$i]); } $active = null; do { $mrc = curl_multi_exec($multi_ch, $active); //处理在栈中的每一个句柄。无论该句柄需要读取或写入数据都可调用此方法。 } while ($mrc == CURLM_CALL_MULTI_PERFORM); //Note: //该函数仅返回关于整个批处理栈相关的错误。即使返回 CURLM_OK 时单个传输仍可能有问题。 while ($active && $mrc == CURLM_OK) { if (curl_multi_select($multi_ch) != -1) {//阻塞直到cURL批处理连接中有活动连接。 do { $mrc = curl_multi_exec($multi_ch, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } } //获取http返回的结果 $true_request = 0; foreach ($ch_list as $k => $ch) { $result = curl_multi_getcontent($ch); curl_multi_remove_handle($multi_ch,$ch); curl_close($ch); if ($result == 1) { $true_request += 1; } } curl_multi_close($multi_ch); echo $true_request, PHP_EOL;
实验结果:当并发请求次数大于600之后,成功请求次数并非线性关系,而是在650左右浮动。到目标服务器检查nginx日志,发现请求成功的日志一共有45925条,而PHP程序返回成功请求的一共有45056条。
此时,猜想:curl请求没有发出,或者返回值并没有被成功接收。
我们需要通过curl error错误码,看看发生了什么事情,这次我们使用并发为800作为一个例子,修改获取curl返回值的循环,打印出curl的错误码:
foreach ($ch_list as $k => $ch) { $result = curl_multi_getcontent($ch); $errstr = curl_error($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_multi_remove_handle($multi_ch,$ch); curl_close($ch); if ($result == 1) { $true_request += 1; } else { echo "{$code}:{$errstr}", PHP_EOL; } }
当请求失败时打印HTTP状态码,以及curl的errstr 值,却发现HTTP状态码为0,然而curl的errstr并没有值。没有errstr的值,似乎无从入手了,后面使用strace进行尝试,发现请求失败的recvfrom返回。
recvfrom(370, 0x3cccc68, 16384, 0, 0, 0) = -1 ECONNRESET (Connection reset by peer) recvfrom(390, "", 16384, 0, NULL, NULL) = 0
ECONNRESET (Connection reset by peer)说明的是服务端关闭了该请求(写是关闭), recvfrom返回0也是服务端关闭了请求(读时关闭),看来最大的问题出在server端上!
后记:如何解决Connect time out的问题。
通过数据分析curl耗时,并设置CURLOPT_TIMEOUT的设置在合适的时间内。之前做了一个监控的daemon程序,CURLOPT_TIMEOUT设置为3s,发现有非常多的请求出现Connect time out的情况。CURLOPT_TIMEOUT设置为15s后,Connect time out的情况就少了,那么CURLOPT_TIMEOUT应该设置为多少比较合适呢?这个可以先收集请求的响应时间,接下来对CURLOPT_TIMEOUT进行优化。
curl_getinfo()函数可以返回几个有助于我们分析请求时间的指标:
参数名称 | 说明 |
total_time | curl所花费的总时间 |
namelookup_time | 域名解析所花费的时间 |
connect_time | 连接目标服务器所花费的时间 |
redirect_time | 重定向所花费的时间 |
starttransfer_time | 数据传输开始时间 |
PS:指标的单位均是秒(seconds),如果要计算数据传输的时间,可以通过total_time - starttransfer_time的差值获取。
每次请求结束后记录total_time,通过数据分析,得出合适的CURLOPT_TIMEOUT设置值。
从上图可以看出,由于服务器端请求响应时间非常不稳定,但是趋势是响应时间越大的请求数会越来越小。因此,CURLOPT_TIMEOUT我这面设置为15s是比较合适的,而监控项目上线后,也很小会出现Connect time out 的情况。