curl_multi_exec绑定IP后连接复用异常问题求助
问题描述
背景与需求
- 正在开展性能测试,需尽可能快地执行请求,目标端点部署在CloudFront之后
- 测试需求:选定4个IP,每个IP发送2个请求;先建立连接,等待5秒后复用这些连接执行后续请求
遇到的问题
- 第一次8个并行请求(4×2)能正常对应各自指定的IP,但第二次复用连接时所有请求都使用同一个IP
- 若不使用
CURLOPT_RESOLVE仅依赖DNS解析,首次请求连接耗时较长;第二次请求的app_connect_time为0,疑似复用了连接,但不清楚curl_multi_exec的内部机制
相关代码
$ips = [ 1 => 'x.x.x.x', 2 => 'x.x.x.x', 3 => 'x.x.x.x', 4 => 'x.x.x.x', 5 => 'x.x.x.x', 6 => 'x.x.x.x', 7 => 'x.x.x.x', 8 => 'x.x.x.x', ]; $multiCurl = array(); // 存储返回数据 $result1 = $result2 = $info = $info2 = $headers1 = $headers2 = array(); // 初始化multi句柄 $mh = curl_multi_init(); for($i=1; $i<=8; $i++){ $fetchURL = 'https://endpoint/timestamp?'.$i; $multiCurl[$i] = curl_init(); curl_setopt($multiCurl[$i], CURLOPT_URL,$fetchURL); curl_setopt($multiCurl[$i], CURLOPT_HEADER,1); curl_setopt($multiCurl[$i], CURLOPT_RETURNTRANSFER,1); curl_setopt($multiCurl[$i], CURLOPT_RESOLVE , ['endpoint:443:'.$ips[$i]] ); curl_multi_add_handle($mh, $multiCurl[$i]); } $index=null; $t1 = microtime(true); do { curl_multi_exec($mh,$index); } while($index > 0); $t2 = microtime(true); foreach($multiCurl as $k => $ch) { $response = curl_multi_getcontent($ch); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header = substr($response, 0, $header_size); $headers1[$k] = $this->get_headers($header); $body = substr($response, $header_size); $result1[$k] = json_decode($body, true); curl_multi_remove_handle($mh, $ch); $info[$k]['CURLINFO_EFFECTIVE_URL'] = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); $info[$k]['CURLINFO_TOTAL_TIME'] = curl_getinfo($ch, CURLINFO_TOTAL_TIME); $info[$k]['CURLINFO_NAMELOOKUP_TIME'] = curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME); $info[$k]['CURLINFO_CONNECT_TIME'] = curl_getinfo($ch, CURLINFO_CONNECT_TIME); $info[$k]['CURLINFO_PRETRANSFER_TIME'] = curl_getinfo($ch, CURLINFO_PRETRANSFER_TIME); $info[$k]['CURLINFO_STARTTRANSFER_TIME'] = curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME); $info[$k]['CURLINFO_PRIMARY_IP'] = curl_getinfo($ch, CURLINFO_PRIMARY_IP); $info[$k]['CURLINFO_NUM_CONNECTS'] = curl_getinfo($ch, CURLINFO_NUM_CONNECTS); $info[$k]['CURLINFO_APPCONNECT_TIME'] = curl_getinfo($ch, CURLINFO_APPCONNECT_TIME); $info[$k]['CURLINFO_APPCONNECT_TIME_T'] = curl_getinfo($ch, CURLINFO_APPCONNECT_TIME_T); $info[$k]['CURLINFO_CONNECT_TIME_T'] = curl_getinfo($ch, CURLINFO_CONNECT_TIME_T); $info[$k]['CURLINFO_NAMELOOKUP_TIME_T'] = curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME_T); $info[$k]['CURLINFO_PRETRANSFER_TIME_T'] = curl_getinfo($ch, CURLINFO_PRETRANSFER_TIME_T); $info[$k]['CURLINFO_STARTTRANSFER_TIME_T'] = curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME_T); $info[$k]['CURLINFO_TOTAL_TIME_T'] = curl_getinfo($ch, CURLINFO_TOTAL_TIME_T); } sleep(5); for($i=1; $i<=8; $i++){ $fetchURL = 'https://endpoint/timestamp?'.$i+10; $multiCurl[$i] = curl_init(); curl_setopt($multiCurl[$i], CURLOPT_URL,$fetchURL); curl_setopt($multiCurl[$i], CURLOPT_HEADER,1); curl_setopt($multiCurl[$i], CURLOPT_RETURNTRANSFER,1); curl_multi_add_handle($mh, $multiCurl[$i]); } $index=null; $t3 = microtime(true); do { curl_multi_exec($mh,$index); } while($index > 0); $t4 = microtime(true); foreach($multiCurl as $k => $ch) { $response = curl_multi_getcontent($ch); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header = substr($response, 0, $header_size); $headers2[$k] = $this->get_headers($header); $body = substr($response, $header_size); $result2[$k] = json_decode($body, true); curl_multi_remove_handle($mh, $ch); $info2[$k]['CURLINFO_EFFECTIVE_URL'] = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); $info2[$k]['CURLINFO_TOTAL_TIME'] = curl_getinfo($ch, CURLINFO_TOTAL_TIME); $info2[$k]['CURLINFO_NAMELOOKUP_TIME'] = curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME); $info2[$k]['CURLINFO_CONNECT_TIME'] = curl_getinfo($ch, CURLINFO_CONNECT_TIME); $info2[$k]['CURLINFO_PRETRANSFER_TIME'] = curl_getinfo($ch, CURLINFO_PRETRANSFER_TIME); $info2[$k]['CURLINFO_STARTTRANSFER_TIME'] = curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME); $info2[$k]['CURLINFO_PRIMARY_IP'] = curl_getinfo($ch, CURLINFO_PRIMARY_IP); $info2[$k]['CURLINFO_NUM_CONNECTS'] = curl_getinfo($ch, CURLINFO_NUM_CONNECTS); $info2[$k]['CURLINFO_APPCONNECT_TIME'] = curl_getinfo($ch, CURLINFO_APPCONNECT_TIME); $info2[$k]['CURLINFO_APPCONNECT_TIME_T'] = curl_getinfo($ch, CURLINFO_APPCONNECT_TIME_T); $info2[$k]['CURLINFO_CONNECT_TIME_T'] = curl_getinfo($ch, CURLINFO_CONNECT_TIME_T); $info2[$k]['CURLINFO_NAMELOOKUP_TIME_T'] = curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME_T); $info2[$k]['CURLINFO_PRETRANSFER_TIME_T'] = curl_getinfo($ch, CURLINFO_PRETRANSFER_TIME_T); $info2[$k]['CURLINFO_STARTTRANSFER_TIME_T'] = curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME_T); $info2[$k]['CURLINFO_TOTAL_TIME_T'] = curl_getinfo($ch, CURLINFO_TOTAL_TIME_T); if(!isset($weight))$weight = $headers2[$k][0]['response-time']; if($headers2[$k][0]['response-time']<$weight)$weight=$headers2[$k][0]['response-time']; } // 关闭multi句柄 curl_multi_close($mh);
请求输出
第一次并行请求输出
{ "1": { "CURLINFO_PRIMARY_IP": "1st ip" // 其他字段省略 }, "2": { "CURLINFO_PRIMARY_IP": "2nd IP" // 其他字段省略 }, "3": { "CURLINFO_PRIMARY_IP": "3rd ip" // 其他字段省略 } // 剩余条目省略 }
第二次并行请求输出
{ "1": { "CURLINFO_PRIMARY_IP": "1st ip" // 其他字段省略 }, "2": { "CURLINFO_PRIMARY_IP": "1st ip" // 其他字段省略 }, "3": { "CURLINFO_PRIMARY_IP": "1st ip" // 其他字段省略 } // 剩余条目省略 }
问题分析与解决
核心原因
- 连接复用规则:cURL的连接池以
主机:端口为键复用连接。第一次请求通过CURLOPT_RESOLVE绑定了不同IP,但第二次请求未设置该选项,cURL会默认复用连接池中已有的第一个可用连接,导致所有请求走同一个IP。 - 连接池生命周期:第一次请求后移除句柄,但cURL的全局连接池(或multi句柄关联的连接池)会保留已建立的连接一段时间,第二次请求无IP规则约束时会优先复用这些连接。
解决方案
方案1:第二次请求保留CURLOPT_RESOLVE配置
第二次循环初始化curl句柄时,继续绑定对应IP,确保每个请求复用指定IP的连接:
for($i=1; $i<=8; $i++){ // 修复运算优先级问题:$i+10需加括号,避免字符串拼接后转数字运算 $fetchURL = 'https://endpoint/timestamp?'.($i+10); $multiCurl[$i] = curl_init(); curl_setopt($multiCurl[$i], CURLOPT_URL,$fetchURL); curl_setopt($multiCurl[$i], CURLOPT_HEADER,1); curl_setopt($multiCurl[$i], CURLOPT_RETURNTRANSFER,1); // 保留对应IP的解析规则 curl_setopt($multiCurl[$i], CURLOPT_RESOLVE , ['endpoint:443:'.$ips[$i]] ); curl_multi_add_handle($mh, $multiCurl[$i]); }
方案2:复用第一次的curl句柄,而非重新初始化
第一次请求后不丢弃句柄,仅重置必要选项(如URL),cURL会自动复用该句柄对应的连接:
// 第一次请求循环后,仅移除句柄但保留对象 foreach($multiCurl as $k => $ch) { $response = curl_multi_getcontent($ch); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header = substr($response, 0, $header_size); $headers1[$k] = $this->get_headers($header); $body = substr($response, $header_size); $result1[$k] = json_decode($body, true); curl_multi_remove_handle($mh, $ch); // 不销毁句柄,保留连接配置 } sleep(5); for($i=1; $i<=8; $i++){ $fetchURL = 'https://endpoint/timestamp?'.($i+10); // 复用已有的句柄,仅更新URL curl_setopt($multiCurl[$i], CURLOPT_URL, $fetchURL); // 无需重新设置CURLOPT_RESOLVE,句柄已保存该配置 curl_multi_add_handle($mh, $multiCurl[$i]); }
补充说明
- 若依赖DNS解析,cURL会缓存DNS结果,连接池仍基于
主机:端口复用。CloudFront返回多个IP时,cURL会尝试建立多个连接,但复用规则优先选择已有连接,可能出现请求集中在单个IP的情况。 - 可通过
CURLOPT_MAXCONNECTS限制连接池大小,或用CURLOPT_FORBID_REUSE强制关闭连接(但会失去复用优势,不推荐)。
内容的提问来源于stack exchange,提问作者Raul Radulescu
相关产品推荐
相关产品推荐

