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_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_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:最慢的一個介面達到了
1.58s
- 方案2:
105
個介面的平均耗時是384ms
這個測試的請求是我的環境的內部介面,所以耗時很短,實際爬蟲請求環境優化會更明顯。
注意項
併發數限制
curl_multi
會消耗很多的系統資源,在併發請求時併發數有一定閾值,一般為512
,是由於CURL
內部限制,超過最大併發會導致失敗。
超時時間
為了防止慢請求影響整個服務,可以設定CURLOPT_TIMEOUT
來控制超時時間,防止部分假死的請求無限阻塞程序處理,最後打宕機器服務。
CPU
負載打滿
在程式碼示例中,如果持續查詢併發的執行狀態,會導致cpu
的負載過高,所以,需要在程式碼里加上usleep(50000);
的語句。同時,curl_multi_select
也可以控制cpu
佔用,在資料有迴應前會一直處於等待狀態,新資料一來就會被喚醒並繼續執行,減少了CPU
的無謂消耗。