线上调用 OpenAI API 一旦出现超时,最烦的不是“偶发失败”,而是你不知道到底卡在 DNS、TLS、代理,还是你自己的连接池。

这篇给你一套可直接落地的排查顺序:先判定超时发生在哪一段,再用指标和最小实验定位,最后给可复制的 Go 配置模板,避免同类事故反复出现。

一、先把“超时”拆成 4 段,不要一锅炖

在 Go 的 HTTP 调用链里,OpenAI 请求通常会经过:

  1. DNS 解析
  2. TCP 建连 + TLS 握手
  3. 代理转发(如果你走公司出口或自建网关)
  4. 请求发送与响应读取(含流式返回)

只看 context deadline exceeded 没意义。你必须知道“超时发生在哪一段”。

二、最小可观测配置:把链路耗时打出来

先上一个可观测的 http.Transport 模板:

package main

import (
	"context"
	"crypto/tls"
	"log"
	"net"
	"net/http"
	"net/http/httptrace"
	"time"
)

func newClient() *http.Client {
	dialer := &net.Dialer{Timeout: 3 * time.Second, KeepAlive: 30 * time.Second}
	tr := &http.Transport{
		Proxy:                 http.ProxyFromEnvironment,
		DialContext:           dialer.DialContext,
		TLSHandshakeTimeout:   5 * time.Second,
		ResponseHeaderTimeout: 30 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		IdleConnTimeout:       90 * time.Second,
		MaxIdleConns:          200,
		MaxIdleConnsPerHost:   50,
		MaxConnsPerHost:       100,
		TLSClientConfig:       &tls.Config{MinVersion: tls.VersionTLS12},
		ForceAttemptHTTP2:     true,
	}
	return &http.Client{Transport: tr, Timeout: 45 * time.Second}
}

func withTrace(req *http.Request) *http.Request {
	trace := &httptrace.ClientTrace{
		DNSStart:          func(i httptrace.DNSStartInfo) { log.Printf("dns_start host=%s", i.Host) },
		DNSDone:           func(i httptrace.DNSDoneInfo) { log.Printf("dns_done addrs=%v err=%v", i.Addrs, i.Err) },
		ConnectStart:      func(network, addr string) { log.Printf("connect_start %s %s", network, addr) },
		ConnectDone:       func(network, addr string, err error) { log.Printf("connect_done %s %s err=%v", network, addr, err) },
		TLSHandshakeStart: func() { log.Printf("tls_start") },
		TLSHandshakeDone:  func(state tls.ConnectionState, err error) { log.Printf("tls_done vers=%x cipher=%x err=%v", state.Version, state.CipherSuite, err) },
		GotConn:           func(info httptrace.GotConnInfo) { log.Printf("got_conn reused=%v was_idle=%v idle=%s", info.Reused, info.WasIdle, info.IdleTime) },
	}
	return req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
}

func main() { _ = withTrace }

三、按优先级排查(实战顺序)

1) DNS 超时

典型症状:lookup api.openai.com: i/o timeout

dig api.openai.com
nslookup api.openai.com

处理:稳定 DNS、容器显式 dnsConfig、复用 client 减少重复解析。

2) TLS 握手超时

典型症状:net/http: TLS handshake timeout

openssl s_client -connect api.openai.com:443 -servername api.openai.com -brief
curl -v --connect-timeout 5 https://api.openai.com/v1/models

处理:检查代理证书链和系统时间,别无脑把超时调到 60s。

3) 代理链路抖动

处理:对比直连与代理成功率,给代理做健康检查,幂等请求才重试。

4) 连接池配置不当

处理:全局复用 http.Client,调大 MaxConnsPerHost,流式响应必须 close body。

四、保守参数建议

  • Dial: 3s
  • TLS: 5s
  • ResponseHeader: 30s
  • Client.Timeout: 45s
  • MaxConnsPerHost: 100
  • MaxIdleConnsPerHost: 50

五、总结

超时不是玄学。拆链路、看 trace、按段修,你会比“盲目重试”快得多。