应使用 curl 多路复用替代循环 file_get_contents,因其通过共享连接、dns 缓存和 ssl 会话实现真正异步并发,避免阻塞与重复建连开销,并需正确配置 curlopt_dns_cache_timeout、tcp_keepalive、ssl 验证及 http/2 等参数以优化性能。

用 cURL 多路复用替代循环 file_get_contents
直接用 file_get_contents 发多个 HTTP 请求,在高并发下会卡死或超时,本质是阻塞式串行执行。PHP 本身不支持原生并发 HTTP,必须靠 cURL 的 curl_multi_* 系列函数做异步复用。
关键不是“能不能发”,而是“连接要不要重复建”。每次 curl_exec 都新建 TCP 连接,开销极大;而 curl_multi_exec 复用同一个 cURL 句柄池,底层共享连接、DNS 缓存和 SSL 会话。
- 必须显式调用
curl_multi_setopt($mh, CURLMOPT_PIPELINING, 1)启用管线化(PHP 7.2.22+ / 8.0+) - 每个子请求的
CURLOPT_RETURNTRANSFER必须设为true,否则结果拿不到 - 别忘了
curl_multi_remove_handle清理已完成句柄,否则内存泄漏 - 错误检查不能只看
curl_multi_info_read返回值,还要查curl_error($ch)和curl_getinfo($ch, CURLINFO_HTTP_CODE)
while ($active && $mrc == CURLM_OK) {
if (curl_multi_select($mh, 1) > 0) {
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
}
}
curl_setopt 关键参数避坑清单
很多性能问题其实出在几个默认参数上:DNS 缓存关闭、SSL 验证强制开启、连接不复用。不改这些,curl_multi 也白搭。
-
CURLOPT_DNS_CACHE_TIMEOUT设为600(秒),避免反复查 DNS -
CURLOPT_TCP_KEEPALIVE设为1,保持空闲连接存活 -
CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST在内网或可信环境可关(设为false),省掉证书链校验耗时 -
CURLOPT_CONNECTTIMEOUT_MS比CURLOPT_TIMEOUT_MS更重要——它控制建连阶段,建议设 1500~3000ms,防止卡在 SYN 重传 - 务必加
CURLOPT_USERAGENT,某些 CDN 或 API 网关会拦截空 UA 请求
并发数不是越大越好:受限于 PHP 的 max_connections 和系统文件描述符
你设了 100 个并发,但 PHP-FPM 的 pm.max_children 是 32,或者系统 ulimit -n 是 1024,那实际能跑的远少于预期,还会触发 Too many open files 错误。
立即学习“PHP免费学习笔记(深入)”;
- 先用
cat /proc/$(pgrep php-fpm)/limits | grep "Max open files"查当前限制 - 单次
curl_multi并发建议控制在 20~50,再高收益递减,错误率上升 - 如果要处理上千 URL,得自己分批 + 异步队列,别指望一次全塞进
curl_multi - 注意
curl_multi_add_handle后不能立刻unset($ch),句柄还被多路复用器持有,得等curl_multi_remove_handle之后再释放
别忽略 DNS 和 TLS 握手的隐性延迟
一个 HTTPS 请求里,DNS 查询和 TLS 握手占总耗时 30%~60%,尤其首次访问新域名时。PHP 不像 Node.js 或 Go 那样有全局 DNS 缓存池,每次都要走系统 resolver。
- 用
curl_setopt($ch, CURLOPT_RESOLVE, ["example.com:443:192.0.2.1"])强制指定 IP,跳过 DNS - 对固定后端服务,提前用
gethostbyname解析一次,缓存 IP 列表,构造CURLOPT_RESOLVE数组 - TLS 层面,启用
CURLOPT_SSL_ENABLE_NPN和CURLOPT_SSL_ENABLE_ALPN(PHP 7.3.5+),支持 HTTP/2 协议协商加速 - 如果目标服务支持 HTTP/2,且你用的是 libcurl 7.62.0+,记得加
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0
真正卡住你的,往往不是代码逻辑,而是没意识到 DNS 和 TLS 是串行阻塞点,更不会自动复用。把这两个环节稳住,QPS 提升比调并发数明显得多。











