Buzeli
buzeliSoluções Digitais
事故

CURLOPT_TIMEOUT = 0:让支付网关卡死60秒的无限超时

发布于 2026年4月10日

CURLOPT_TIMEOUT = 0 travando gateway Pagar.me por até 60 segundos — latência p99 5,4s, max 60s, fix em 6 arquivos

误导诊断的症状

一个支付网关开始出现间歇性缓慢。行为很奇怪:服务器CPU占用4%,内存稳定,没有流量峰值。但p99延迟达到5.410秒,记录的最大值高达60.001秒。在30分钟的观察窗口内,1,152个请求中有161个(14%)超过了2秒。

慢速事故中CPU占用低是一个经典陷阱。它排除了密集计算的假设,但无法排除阻塞I/O——阻塞I/O在锁定线程等待外部服务响应时,CPU消耗为零。

nginx日志中出现了499错误(客户端在收到响应前断开连接),确认用户在收到响应前放弃了请求。支付网关锁定了PHP-FPM线程,等待外部API的响应——而响应要么从未到来,要么来得太晚。

根本原因:6个文件中的CURLOPT_TIMEOUT = 0

对网关PHP文件的调查在6个不同位置发现了同一行代码:

复制
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
// 或等价地:
curl_setopt($ch, CURLOPT_TIMEOUT, 0);  // 无超时

在cURL中,CURLOPT_TIMEOUT = 0不意味着'未设置限制'——它意味着'无限期等待'。 这是违反直觉的行为:零不是禁用超时,而是将超时设置为无穷大。

受影响的6个文件覆盖了网关的所有关键流程:

Checkout.php — 标准结账流程

CheckoutTransparent.php — 透明结账(信用卡)

GatewayServices.php — 辅助服务

Subscriptions.php — 定期订阅流程

SessionHandler.php — 网关会话管理

Refunds.php — 退款和退单

为什么这些文件中会出现零值

值0经常从文档示例或遗留代码中复制,开发者的本意是'不想让PHP默认值干预——让cURL来控制'。目的是通过将控制权委托给cURL来禁用PHP的超时(通过max_execution_time)。实际效果恰恰相反:由于cURL中没有定义超时,任何向支付网关发出的请求——因为API不稳定、网络延迟或临时过载而耗时较长——都会无限期挂起。

最大值60.001秒并非巧合:这是PHP的默认超时(max_execution_time = 60s)作为最后防线发挥作用。没有它,线程将一直被锁定直到进程重启。

诊断:日志中的证据

为了确认阻塞I/O的假设,交叉分析了nginx和PHP-FPM的日志:

复制
# nginx中的499错误——客户端在响应前断开连接
grep ' 499 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn

# 卡住的PHP-FPM worker——进程状态
curl http://localhost/fpm-status?full | grep -E 'state|last request uri|request duration'

# 支付网关请求的延迟分布
grep 'payment-gateway' /var/log/app/requests.log | awk '{print $NF}' | sort -n |   awk 'BEGIN{c=0} {a[c++]=$1} END{
    print "p90: " a[int(c*0.90)];
    print "p95: " a[int(c*0.95)];
    print "p99: " a[int(c*0.99)];
    print "max: " a[c-1]
  }'

PHP-FPM状态显示worker处于'Reading headers'状态长达数十秒——等待支付网关的响应。当足够多的线程同时被卡住时,新请求开始排队,加剧了缓慢。

修复方案:每次3个文件,每批之间验证

复制
// 修复前——无限超时
curl_setopt($ch, CURLOPT_TIMEOUT, 0);

// 修复后——5秒超时
curl_setopt($ch, CURLOPT_TIMEOUT, 5);

5秒的值基于支付网关API在正常条件下的历史p99(低于3秒)加上余量来选择。与支付网关的请求如果合理情况下超过5秒,是网关API问题的症状——不是正常操作——应该快速失败以释放线程并向用户返回可处理的错误。

重要:CURLOPT_TIMEOUT影响总的cURL操作时间(连接+传输)。为了更精细的控制,还应使用CURLOPT_CONNECTTIMEOUT来单独限制连接建立时间。

复制
// 推荐的完整配置
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);  // 最多3秒连接
curl_setopt($ch, CURLOPT_TIMEOUT, 5);          // 最多5秒总计

结果

在6个文件中应用修复并重新加载PHP-FPM(无停机)后,499错误在第一个小时的监控中降至零。在正常条件下,p99恢复到1秒以下。部署期间没有支付交易丢失——修复逐个文件应用,每次部署之间进行验证。

一个看起来'已禁用'(零)但实际上意味着'无限'的值,是那种在生产中存活数月的bug——因为它不产生明确的错误,只有间歇性的缓慢。找到它的唯一方法是主动搜索发出外部请求的文件。

如何审计您的代码

在PHP项目中查找所有CURLOPT_TIMEOUT = 0的使用:

复制
# 在PHP文件中查找值为0的CURLOPT_TIMEOUT
grep -rn 'CURLOPT_TIMEOUT.*0' /var/www/html/ --include='*.php'

# 也搜索数字常量(13 = CURLOPT_TIMEOUT)
grep -rn 'CURLOPT_TIMEOUT\b' /var/www/html/ --include='*.php' | grep -v '//.*CURLOPT_TIMEOUT'

# 检查同一文件中是否缺少CURLOPT_CONNECTTIMEOUT
grep -rL 'CURLOPT_CONNECTTIMEOUT' $(grep -rl 'curl_setopt' /var/www/html/ --include='*.php')

任何使用cURL发出外部请求的文件都必须明确设置CURLOPT_TIMEOUT > 0和CURLOPT_CONNECTTIMEOUT > 0。在与支付API、邮政编码查询、通知或任何外部服务的集成中缺少这些设置,是直接的可用性风险。