• PHP实现并发请求


    后端服务开发中经常会有并发请求的需求,比如你需要获取10家供应商的带宽数据(每个都提供不同的url),然后返回一个整合后的数据,你会怎么做呢?

    PHP中,最直观的做法foreach遍历urls,并保存每个请求的结果即可,那么如果供应商提供的接口平均耗时5s,你的这个接口请求耗时就达到了50s,这对于追求速度和性能的网站来说是不可接受的。

    这个时候你就需要并发请求了。

    PHP请求

    PHP是单进程同步模型,一个请求对应一个进程,I/O是同步阻塞的。通过nginx/apache/php-fpm等服务的扩展,才使得PHP提供高并发的服务,原理就是维护一个进程池,每个请求服务时单独起一个新的进程,每个进程独立存在。

    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_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内部限制,超过最大并发会导致失败。

    具体的测试结果我没有做,可以参考别人的文章:每次使用curl multi同时并发多少请求合适

    超时时间

    为了防止慢请求影响整个服务,可以设置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...

    原文地址:https://segmentfault.com/a/1190000016343861

  • 相关阅读:
    js中如何在不影响既有事件监听的前提下新增监听器
    史上最走心webpack4.0中级教程——配置之外你应该知道的事
    webpack4.0各个击破(10)—— Integration篇
    webpack4.0各个击破(9)—— karma篇
    webpack4.0各个击破(8)—— tapable篇
    webpack4.0各个击破(7)—— plugin篇
    MYSQL的安装
    数据库的分类
    python接口调用把执行结果追加到测试用例中
    python分布式接口,参数化实战二
  • 原文地址:https://www.cnblogs.com/lalalagq/p/9971447.html
Copyright © 2020-2023  润新知