PHP实现并发请求

后端服务开发中常常会有并发请求的需求,好比你须要获取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_contentsfsockopen请求方式,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;
}

在该并发请求中,先建立一个批处理句柄,而后将urlcURL句柄添加到批处理句柄中,并不断查询批处理句柄的执行状态,当执行完成后,获取返回的结果。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并发请求耗时对比

  1. 第一次请求使用上面的curl_multi_init方法,并发请求105次。
  2. 第二次请求使用传统的foreach方法,遍历105次使用curl_init方法请求。

实际的请求耗时结果为:
请求耗时对比函数

刨除download的约765ms耗时,单纯的请求耗时优化达到了39.83/1.58达到了25倍,若是继续刨除建连相关的耗时,应该会更高。这其中的耗时:高并发

  • 方案1:最慢的一个接口达到了1.58s
  • 方案2: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的无谓消耗。

参考资料

  1. PHP手册 curl_multi_inithttp://php.net/manual/zh/func...
  2. PHP手册 curl预约义常量http://php.net/manual/zh/curl...
  3. PHP中foreach curl实现多线程http://www.111cn.net/phper/ph...
  4. Doing curl_multi_exec the right wayhttp://www.adrianworlddesign....
  5. Segmentfault PHP cURL请求详解https://segmentfault.com/a/11...
  6. CSDN 每次使用curl multi同时并发多少请求合适https://blog.csdn.net/loophom...
  7. 简书 Curl多线程及原理https://www.jianshu.com/p/f50...
相关文章
相关标签/搜索