首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP cURL 並發請求實戰:多線程異步調用大幅降低後端延遲

     編輯:PHP 綜合知識

痛點背景

傳統串行 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+)
?>

進階模式:異步非阻塞 + 回調/Promise

<?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 + PoolAPI 友好、中間件生態、Promise 支持依賴重、底層仍用 curl_multi現代 PHP 項目、微服務聚合
Swoole/OpenSwoole協程原生異步、極高併發、內存常駐需擴展、學習成本高、調試難高併發長連接、實時推送、API 網關
ReactPHP純 PHP 事件循環、無擴展依賴性能受限於 PHP 解釋器、生態較小無法安裝擴展的環境、輕量異步

總結

cURL 並發是 PHP 提升後端聚合接口性能的最低成本、最高收益手段。核心要點:curl_multi 批量執行、超時與錯誤隔離、連接復用、合理並發度控制、熔斷降級保護。從串行到並發,往往能帶來 3-10 倍 的響應時間提升,是 API 網關、BFF 層、聚合服務的標配技能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved