跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

如何在Golang中处理由第三方负载均衡器返回的502/504错误

Go HTTP客户端默认不重试502/504错误,遇到此类状态码时返回resp.StatusCode=502或504且err==nil,需显式检查状态码并手动重试,否则易导致JSON解析失败或字段为空。 Go HTTP客户端默认不重试502/504 Go标准库的
http.Client
对502(Bad Gateway)和504(Gateway Timeout)完全不重试——它把它们当最终错误处理,哪怕后端只是瞬时抖动。这是最常被忽略的前提:你写的
http.Get
或
http.Do
调用,遇到负载均衡器返回502/504,立刻返回
*http.Response
且
err == nil
,但
resp.StatusCode
是502或504,不会自动重发请求。 常见错误现象:
if err != nil
检查通过,但业务逻辑直接拿
resp.Body
解析JSON,结果解码失败或字段为空,日志里却看不到明显错误。 必须显式检查
resp.StatusCode >= 500 && resp.StatusCode
不要依赖
err != nil
来判断请求失败 502/504是服务端错误,但责任在中间链路(LB或上游),不是你的代码bug,重试通常是合理策略 用RoundTripper包装实现带条件重试 直接改
http.Client.CheckRedirect
没用——它只管3xx跳转;真正要拦截并重试,得替换
http.Client.Transport
,用自定义
RoundTripper
。核心是在
RoundTrip
方法里捕获502/504响应,并按需重新发起原始请求。 注意:重试前必须
resp.Body.Close()
,否则连接不释放;重试次数建议≤3次,避免雪崩;重试间隔用指数退避(如100ms、300ms、900ms)。 立即学习 “ go语言免费学习笔记(深入) ”; 别用
time.Sleep
硬等,用
time.AfterFunc
或循环中
time.Sleep
加
select
超时控制 原请求的
Body
如果是
io.ReadCloser
(比如文件上传),重试时需能重放——要么用
bytes.Buffer
提前缓存,要么确保Body可seek(如
strings.NewReader
) GET/HEAD请求才适合无脑重试;POST/PUT建议加幂等性头(如
X-Idempotency-Key
)再重试
type RetryTransport struct { base http.RoundTripper }

func (rt RetryTransport) RoundTrip(req http.Request) (http.Response, error) { var resp http.Response var err error for i := 0; i < 3; i++ { resp, err = rt.base.RoundTrip(req) if err != nil { if i == 2 { return nil, err } time.Sleep(time.Millisecond time.Duration(100int(math.Pow(3, float64(i)))))) continue } if resp.StatusCode == 502 || resp.StatusCode == 504 { resp.Body.Close() if i == 2 { break } time.Sleep(time.Millisecond time.Duration(100int(math.Pow(3, float64(i)))))) continue } return resp, nil } return resp, err }

第三方库选型:net/http vs. third-party clients 自己写
RoundTripper
可控但易出错(比如Body未关闭、上下文未传递)。如果项目允许引入依赖,
github.com/hashicorp/go-retryablehttp
是更稳的选择——它内置了502/504重试逻辑,且默认开启,还支持
RetryMax
、
RetryWaitMin
等配置。 但它有个坑:默认重试所有5xx,包括500(Internal Server Error),而500大概率是后端真实故障,不该盲目重试。所以必须覆盖
CheckRetry
函数: 用
retryablehttp.DefaultRetryPolicy
作基础,再额外过滤掉500、503(Service Unavailable) 确保
req.Context()
透传到重试请求,避免父上下文取消后子请求还在跑 该库不兼容
http.Client.Timeout
,超时必须设在
retryablehttp.Client.RetryMaxTime
里
client := retryablehttp.NewClient() client.RetryMax = 2 client.RetryWaitMin = 100 * time.Millisecond client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { if err != nil { return true, nil // 连接层错误重试 } if resp.StatusCode == 502 || resp.StatusCode == 504 { return true, nil // 明确只重试这两个 } return false, nil // 其他状态码不重试 }
负载均衡器行为差异影响重试效果 不是所有502/504都适合重试。有些LB(比如AWS ALB)返回502是因为目标组里所有实例健康检查失败,此时重试只是浪费资源;而Nginx或Traefik返回504,往往只是上游响应慢,重试成功率高。 关键看LB日志或监控:如果502/504集中出现在某台后端节点故障期间,说明LB已做剔除,重试无意义;如果分散在多个节点、伴随高延迟,重试才有价值。 在HTTP Header里加
X-Request-ID
,方便关联LB日志和你的重试请求 给重试请求加
X-Retry-Count
头,让后端识别并避免二次处理(比如重复扣款) 生产环境务必记录重试次数分布:如果30%请求需要≥2次重试,说明LB或上游稳定性已出问题,不能只靠客户端补救 重试逻辑越简单越可靠,但边界情况(如Body不可重放、上下文取消、LB粘性会话)永远比文档写的多。上线前用
httptest.NewUnstartedServer
模拟502响应,手动验证重试是否触发、Body是否正确读取、超时是否生效——这些没法靠单元测试全覆盖。

相关文章