後端服務開發中常常會有併發請求的需求,好比你須要獲取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...