后端服务开发中常常会有并发请求的需求,好比你须要获取10家供应商的带宽数据(每一个都提供不一样的url
),而后返回一个整合后的数据,你会怎么作呢?php
在PHP
中,最直观的作法foreach
遍历urls
,并保存每一个请求的结果便可,那么若是供应商提供的接口平均耗时5s
,你的这个接口请求耗时就达到了50s
,这对于追求速度和性能的网站来讲是不可接受的。nginx
这个时候你就须要并发请求了。apache
PHP
请求PHP
是单进程同步模型,一个请求对应一个进程,I/O
是同步阻塞的。经过nginx/apache/php-fpm
等服务的扩展,才使得PHP提供高并发的服务,原理就是维护一个进程池,每一个请求服务时单独起一个新的进程,每一个进程独立存在。segmentfault
PHP
不支持多线程模式和回调处理,所以PHP
内部脚本都是同步阻塞式的,若是你发起一个5s
的请求,那么程序就会I/O
阻塞5s
,直到请求返回结果,才会继续执行代码。所以作爬虫之类的高并发请求需求很吃力。后端
那怎么来解决并发请求的问题呢?除了内置的file_get_contents
和fsockopen
请求方式,PHP
也支持cURL
扩展来发起请求,它支持常规的单个请求:PHP cURL请求详解,也支持并发请求,其并发原理是cURL
扩展使用多线程来管理多请求。多线程
PHP
并发请求咱们直接来看代码demo
:并发
// 简单demo,默认支持为GET请求 public function multiRequest($urls) { $mh = curl_multi_init(); $urlHandlers = []; $urlData = []; // 初始化多个请求句柄为一个 foreach($urls as $value) { $ch = curl_init(); $url = $value['url']; $url .= strpos($url, '?') ? '&' : '?'; $params = $value['params']; $url .= is_array($params) ? http_build_query($params) : $params; curl_setopt($ch, CURLOPT_URL, $url); // 设置数据经过字符串返回,而不是直接输出 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $urlHandlers[] = $ch; curl_multi_add_handle($mh, $ch); } $active = null; // 检测操做的初始状态是否OK,CURLM_CALL_MULTI_PERFORM为常量值-1 do { // 返回的$active是活跃链接的数量,$mrc是返回值,正常为0,异常为-1 $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); // 若是还有活动的请求,同时操做状态OK,CURLM_OK为常量值0 while ($active && $mrc == CURLM_OK) { // 持续查询状态并不利于处理任务,每50ms检查一次,此时释放CPU,下降机器负载 usleep(50000); // 若是批处理句柄OK,重复检查操做状态直至OK。select返回值异常时为-1,正常为1(由于只有1个批处理句柄) if (curl_multi_select($mh) != -1) { do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } } // 获取返回结果 foreach($urlHandlers as $index => $ch) { $urlData[$index] = curl_multi_getcontent($ch); // 移除单个curl句柄 curl_multi_remove_handle($mh, $ch); } curl_multi_close($mh); return $urlData; }
在该并发请求中,先建立一个批处理句柄,而后将url
的cURL
句柄添加到批处理句柄中,并不断查询批处理句柄的执行状态,当执行完成后,获取返回的结果。curl
curl_multi
相关函数/** 函数做用:返回一个新cURL批处理句柄 @return resource 成功返回cURL批处理句柄,失败返回false */ resource curl_multi_init ( void ) /** 函数做用:向curl批处理会话中添加单独的curl句柄 @param $mh 由curl_multi_init返回的批处理句柄 @param $ch 由curl_init返回的cURL句柄 @return resource 成功返回cURL批处理句柄,失败返回false */ int curl_multi_add_handle ( resource $mh , resource $ch ) /** 函数做用:运行当前 cURL 句柄的子链接 @param $mh 由curl_multi_init返回的批处理句柄 @param $still_running 一个用来判断操做是否仍在执行的标识的引用 @return 一个定义于 cURL 预约义常量中的 cURL 代码 */ int curl_multi_exec ( resource $mh , int &$still_running ) /** 函数做用:等待全部cURL批处理中的活动链接 @param $mh 由curl_multi_init返回的批处理句柄 @param $timeout 以秒为单位,等待响应的时间 @return 成功时返回描述符集合中描述符的数量。失败时,select失败时返回-1,不然返回超时(从底层的select系统调用). */ int curl_multi_select ( resource $mh [, float $timeout = 1.0 ] ) /** 函数做用:移除cURL批处理句柄资源中的某个句柄资源 说明:从给定的批处理句柄mh中移除ch句柄。当ch句柄被移除之后,仍然能够合法地用curl_exec()执行这个句柄。若是要移除的句柄正在被使用,则这个句柄涉及的全部传输任务会被停止。 @param $mh 由curl_multi_init返回的批处理句柄 @param $ch 由curl_init返回的cURL句柄 @return 成功时返回0,失败时返回CURLM_XXX中的一个 */ int curl_multi_remove_handle ( resource $mh , resource $ch ) /** 函数做用:关闭一组cURL句柄 @param $mh 由curl_multi_init返回的批处理句柄 @return void */ void curl_multi_close ( resource $mh ) /** 函数做用:若是设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流 @param $ch 由curl_init返回的cURL句柄 @return string 若是设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流。 */ string curl_multi_getcontent ( resource $ch )
本例中使用到的 预约义常量:
CURLM_CALL_MULTI_PERFORM: (int) -1
CURLM_OK: (int) 0
PHP
并发请求耗时对比curl_multi_init
方法,并发请求105
次。foreach
方法,遍历105
次使用curl_init
方法请求。实际的请求耗时结果为:函数
刨除download
的约765ms
耗时,单纯的请求耗时优化达到了39.83/1.58
达到了25
倍,若是继续刨除建连相关的耗时,应该会更高。这其中的耗时:高并发
1.58s
105
个接口的平均耗时是384ms
这个测试的请求是个人环境的内部接口,因此耗时很短,实际爬虫请求环境优化会更明显。
curl_multi
会消耗不少的系统资源,在并发请求时并发数有必定阈值,通常为512
,是因为CURL
内部限制,超过最大并发会致使失败。
在我作的测试中,发起2000个相同的请求,并输出每个请求的响应结果。测试结果2000个请求共有366个成功,前331个均成功,在331-410次序之间共有35个成功的,第410个请求以后所有失败。所以咱们必定要注意并发数的限制,不要超过300个,或者你能够本身在本身的机器上作一下测试,来制定你的阈值。
使用以前,请必定要
注意并发数限制
!!
为了防止慢请求影响整个服务,能够设置CURLOPT_TIMEOUT
来控制超时时间,防止部分假死的请求无限阻塞进程处理,最后打死机器服务。
CPU
负载打满在代码示例中,若是持续查询并发的执行状态,会致使cpu
的负载太高,因此,须要在代码里加上usleep(50000);
的语句。
同时,curl_multi_select
也能够控制cpu
占用,在数据有回应前会一直处于等待状态,新数据一来就会被唤醒并继续执行,减小了CPU
的无谓消耗。
PHP手册 curl_multi_init
:http://php.net/manual/zh/func... PHP手册 curl预约义常量
:http://php.net/manual/zh/curl... PHP中foreach curl实现多线程
:http://www.111cn.net/phper/ph... Doing curl_multi_exec the right way
:http://www.adrianworlddesign.... Segmentfault PHP cURL请求详解
:https://segmentfault.com/a/11... CSDN 每次使用curl multi同时并发多少请求合适
:https://blog.csdn.net/loophom... 简书 Curl多线程及原理
:https://www.jianshu.com/p/f50...