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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM