线上 Go 服务调用 OpenAI Responses 时,最容易踩的坑不是“模型不准”,而是链路抖动:连接池不稳、超时预算乱配、重试叠加把自己打挂。

这篇给一套可落地的基线配置:HTTP/2 连接复用、分层超时、错误预算和退避重试,目标是把 5xx 与超时比例压到可控范围,并且能快速定位瓶颈。

先给结论:生产默认基线

如果你还没有一套标准配置,先用下面这组:

  • 强制开启 HTTP/2(默认支持 TLS 场景)
  • 全链路超时预算:请求总超时 > 单次调用超时 > 传输超时
  • 只在可重试错误上重试(429/5xx/网络瞬断)
  • 重试次数 2~3 次,指数退避 + 抖动
  • 用并发信号量限制每实例同时在飞请求

一、Transport:先把连接复用配对

很多“随机慢请求”其实是连接层问题。先固定一版可控 Transport:

tr := &http.Transport{
    MaxIdleConns:        512,
    MaxIdleConnsPerHost: 128,
    MaxConnsPerHost:     256,
    IdleConnTimeout:     90 * time.Second,
    TLSHandshakeTimeout: 5 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
    ForceAttemptHTTP2:   true,
}

client := &http.Client{
    Transport: tr,
    Timeout:   25 * time.Second, // 单次调用硬上限
}

建议从 MaxConnsPerHost=CPU核数*16 起步,按实际 p95 延迟和 QPS 再调。

二、超时预算:别把所有超时都塞进一个 Timeout

推荐三层预算:

  1. 请求总预算(业务层):比如 30 秒
  2. 单次 OpenAI 调用预算(HTTP Client):比如 25 秒
  3. 每次重试的剩余预算:自动按 context 截断
ctx, cancel := context.WithTimeout(parentCtx, 30*time.Second)
defer cancel()

resp, err := callOpenAIWithRetry(ctx, client, payload)

如果你把业务层和调用层都写成 30 秒,一旦有重试就必然超预算,尾延迟会很难看。

三、重试策略:只重试“值得重试”的错误

可重试:

  • HTTP 429
  • HTTP 500/502/503/504
  • context deadline exceeded(且总预算仍有剩余)
  • 临时网络错误(reset/timeout)

不可重试:

  • 400/401/403/404
  • 参数校验失败
  • 明确的配额耗尽且短期不可恢复
func backoff(attempt int) time.Duration {
    base := 200 * time.Millisecond
    max := 2 * time.Second
    d := base * time.Duration(1<<attempt)
    if d > max { d = max }
    jitter := time.Duration(rand.Int63n(int64(120 * time.Millisecond)))
    return d + jitter
}

四、并发与熔断:防止雪崩

给每个实例一层轻量并发闸门,避免突发流量把上游和自己一起拖死:

sem := make(chan struct{}, 64) // 每实例最多 64 个在飞请求

func withLimit(fn func() error) error {
    sem <- struct{}{}
    defer func(){ <-sem }()
    return fn()
}

再加一层简单熔断指标:

  • 最近 1 分钟错误率 > 20%
  • 且样本量 > 100

触发后短暂降级(比如降模型、降并发、返回兜底响应)。

五、观测面板:最少要盯这 6 个指标

  • openai_requests_total(按 status_code 分组)
  • openai_request_latency_ms(p50/p95/p99)
  • openai_retries_total
  • openai_timeout_total
  • openai_inflight
  • openai_error_budget_burn_rate

一条经验:先看 timeout_totalinflight,比盯单个报错字符串更快定位。

常见错误与排查路径

1) p95 延迟突然翻倍

先查:

netstat -an | grep ESTABLISHED | wc -l
lsof -iTCP -sTCP:ESTABLISHED -n -P | grep <your-service-name> | wc -l

如果连接数异常升高,优先检查 MaxConnsPerHost、重试风暴、上游限流。

2) 429 激增

排查顺序:

  1. 是否多个服务共享同一 API key
  2. 是否批量任务与在线流量抢额度
  3. 是否重试策略没有抖动,导致同步重试

3) deadline exceeded 占比高

优先看调用链预算是否冲突:网关 20s、服务 25s、客户端 30s 这种配置会直接制造“假超时”。

最小可行上线方案(MVP)

如果你今天就要上线,至少做这四件事:

  1. 固化 Transport + HTTP/2 参数
  2. 设置三层超时预算(30s/25s/重试剩余)
  3. 重试只覆盖 429/5xx/瞬断,最多 2 次
  4. 接 Prometheus 的 6 个核心指标

做到这一步,稳定性通常就能从“偶发雪崩”变成“可观测、可预期、可回滚”。

总结

Go 调 OpenAI Responses 的核心,不是单点参数,而是“连接复用 + 超时预算 + 错误预算”三件套。

把这三件事先做扎实,再谈更复杂的多供应商路由和成本优化,你的线上系统会省很多夜里报警。