傳統串行 cURL 請求:3 個 API 各 500ms,總耗時 1500ms。用戶等待時間線性增長,嚴重影響首屏渲染與接口響應。cURL 多句柄(curl_multi)實現真正並發,將總耗時壓縮至最慢單個請求時間。
<?php
namespace App\Http;
class ConcurrentCurl {
private array $handles = [];
private \CurlMultiHandle $mh;
private array $options = [
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_SSL_VERIFYPEER => false, // 生產環境請配置 CA 證書
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; ConcurrentCurl/1.0)',
];
public function __construct(array $defaultOptions = []) {
$this->options = array_merge($this->options, $defaultOptions);
$this->mh = curl_multi_init();
}
/**
* 添加請求任務
* @param string $key 業務標識鍵
* @param string $url 請求 URL
* @param array $options 覆蓋默認選項
* @return $this
*/
public function add(string $key, string $url, array $options = []): self {
$ch = curl_init($url);
curl_setopt_array($ch, $this->options);
curl_setopt_array($ch, $options);
$this->handles[$key] = [
'ch' => $ch,
'url' => $url,
'options' => $options,
];
curl_multi_add_handle($this->mh, $ch);
return $this;
}
/**
* 執行並發請求
* @param float $selectTimeout curl_multi_select 等待超時(秒)
* @return array ['key' => ['data' => string, 'info' => array, 'error' => string|null]]
*/
public function execute(float $selectTimeout = 1.0): array {
$results = [];
$running = null;
// 執行循環
do {
$mrc = curl_multi_exec($this->mh, $running);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
while ($running && $mrc === CURLM_OK) {
// 等待活動
if (curl_multi_select($this->mh, $selectTimeout) !== -1) {
do {
$mrc = curl_multi_exec($this->mh, $running);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
}
}
// 收集結果
foreach ($this->handles as $key => $handleData) {
$ch = $handleData['ch'];
$error = curl_error($ch);
if ($error === '') {
$data = curl_multi_getcontent($ch);
$info = curl_getinfo($ch);
$results[$key] = [
'data' => $data,
'info' => $info,
'error' => null,
'success' => $info['http_code'] >= 200 && $info['http_code'] < 300,
];
} else {
$results[$key] = [
'data' => null,
'info' => curl_getinfo($ch),
'error' => $error,
'success' => false,
];
}
curl_multi_remove_handle($this->mh, $ch);
curl_close($ch);
}
curl_multi_close($this->mh);
$this->mh = curl_multi_init(); // 重置供下次使用
$this->handles = [];
return $results;
}
/**
* 靜態快捷方法
*/
public static function fetchAll(array $urls, array $options = []): array {
$curl = new self($options);
foreach ($urls as $key => $url) {
$curl->add($key, $url);
}
return $curl->execute();
}
}
?>
<?php
// 定義並發請求
$urls = [
'user' => 'https://api.user.service/v1/profile/12345',
'orders' => 'https://api.order.service/v1/user/12345/orders?limit=10',
'recommend' => 'https://api.rec.service/v1/user/12345/recommend',
'notification' => 'https://api.notify.service/v1/user/12345/unread-count',
];
// 執行並發
$start = microtime(true);
$results = ConcurrentCurl::fetchAll($urls, [
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'X-Request-ID: ' . uniqid('req_'),
],
CURLOPT_TIMEOUT => 5, // 5秒總超時
]);
$elapsed = (microtime(true) - $start) * 1000;
// 處理結果
$profile = $orders = $recommend = $notifications = null;
$errors = [];
foreach ($results as $key => $result) {
if ($result['success']) {
$decoded = json_decode($result['data'], true);
switch ($key) {
case 'user': $profile = $decoded; break;
case 'orders': $orders = $decoded['data'] ?? []; break;
case 'recommend': $recommend = $decoded['items'] ?? []; break;
case 'notification': $notifications = $decoded['count'] ?? 0; break;
}
} else {
$errors[$key] = $result['error'];
// 可選:記錄降級日誌、返回默認值
}
}
echo "並發總耗時: {$elapsed}ms\n";
// 典型輸出:並發總耗時: 320ms (vs 串行 1200ms+)
?>
<?php
namespace App\Http;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
class AsyncCurl {
private \CurlMultiHandle $mh;
private array $pending = [];
private array $options;
public function __construct(array $options = []) {
$this->mh = curl_multi_init();
$this->options = array_merge([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
], $options);
}
/**
* 發起異步請求,返回 Promise
*/
public function request(string $url, array $options = []): PromiseInterface {
$deferred = new Deferred();
$ch = curl_init($url);
curl_setopt_array($ch, $this->options);
curl_setopt_array($ch, $options);
$id = (int) $ch;
$this->pending[$id] = [
'ch' => $ch,
'deferred' => $deferred,
'url' => $url,
];
curl_multi_add_handle($this->mh, $ch);
// 註冊事件循環定時器驅動(需 ReactPHP 事件循環)
// 此處簡化:實際項目接入 ReactPHP EventLoop
return $deferred->promise();
}
/**
* 處理事件循環(需在 EventLoop 中定期調用)
*/
public function tick(): void {
$running = null;
do {
curl_multi_exec($this->mh, $running);
} while (curl_multi_select($this->mh, 0) !== -1);
// 檢查完成的請求
while ($info = curl_multi_info_read($this->mh)) {
$ch = $info['handle'];
$id = (int) $ch;
if (isset($this->pending[$id])) {
$pending = $this->pending[$id];
$error = curl_error($ch);
if ($error === '' && $info['result'] === CURLE_OK) {
$data = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode >= 200 && $httpCode < 300) {
$pending['deferred']->resolve($data);
} else {
$pending['deferred']->reject(new \Exception("HTTP {$httpCode}: {$data}"));
}
} else {
$pending['deferred']->reject(new \Exception($error ?: "cURL error {$info['result']}"));
}
curl_multi_remove_handle($this->mh, $ch);
curl_close($ch);
unset($this->pending[$id]);
}
}
}
}
// 使用示例(需 ReactPHP 環境)
/*
$loop = React\EventLoop\Factory::create();
$curl = new AsyncCurl();
$promises = [
'user' => $curl->request('https://api/user/1'),
'posts' => $curl->request('https://api/posts?user=1'),
];
\React\Promise\all($promises)->then(function($results) {
// 所有完成
var_dump($results);
})->otherwise(function($e) {
// 任一失敗
echo "Error: {$e->getMessage()}";
});
$loop->addPeriodicTimer(0.01, [$curl, 'tick']);
$loop->run();
*/
?>
<?php
function benchmarkSerial(array $urls): float {
$start = microtime(true);
foreach ($urls as $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
}
return microtime(true) - $start;
}
function benchmarkConcurrent(array $urls): float {
$start = microtime(true);
ConcurrentCurl::fetchAll($urls);
return microtime(true) - $start;
}
// 測試數據
$testUrls = array_fill(0, 10, 'https://httpbin.org/delay/1'); // 10個 1秒延遲接口
echo "串行耗時: " . benchmarkSerial($testUrls) . " 秒\n"; // ~10秒
echo "並發耗時: " . benchmarkConcurrent($testUrls) . " 秒\n"; // ~1-2秒
?>
| 維度 | 建議 | 原因 |
|---|---|---|
| 超時控制 | 設置 CURLOPT_TIMEOUT + CURLOPT_CONNECTTIMEOUT | 防止單個慢請求阻塞整個並發池 |
| 錯誤隔離 | 單個失敗不影響其他,返回結構化錯誤信息 | 提高系統可用性,便於降級 |
| 連接復用 | 啟用 Keep-Alive,複用 TCP 連接 | 減少握手開銷,尤其 HTTPS |
| 並發限制 | 單次批次 ≤ 50,總並發 ≤ 200(視 CPU/文件描述符) | 避免耗盡文件描述符、觸發對端限流 |
| 重試策略 | 冪等接口指數退避重試 2-3 次 | 應對網絡抖動、臨時故障 |
| 熔斷降級 | 接入 Sentinel/Hystrix,失敗率超閾值快速失效 | 防止級聯故障,保護下游服務 |
| 監控指標 | 延遲 P50/P95/P99、成功率、錯誤分布、並發數 | 及時發現性能退化與異常 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| curl_multi (原生) | 無依賴、性能好、控制精細 | 代碼複雜、無內置重試/熔斷 | 中小項目、高性能要求 |
| Guzzle + Pool | API 友好、中間件生態、Promise 支持 | 依賴重、底層仍用 curl_multi | 現代 PHP 項目、微服務聚合 |
| Swoole/OpenSwoole | 協程原生異步、極高併發、內存常駐 | 需擴展、學習成本高、調試難 | 高併發長連接、實時推送、API 網關 |
| ReactPHP | 純 PHP 事件循環、無擴展依賴 | 性能受限於 PHP 解釋器、生態較小 | 無法安裝擴展的環境、輕量異步 |
cURL 並發是 PHP 提升後端聚合接口性能的最低成本、最高收益手段。核心要點:curl_multi 批量執行、超時與錯誤隔離、連接復用、合理並發度控制、熔斷降級保護。從串行到並發,往往能帶來 3-10 倍 的響應時間提升,是 API 網關、BFF 層、聚合服務的標配技能。