webhook重试必须采用指数退避:从1s开始,每次×2(加jitter随机偏移),上限30–60s,总次数5–7次;仅对5xx、连接超时等临时错误重试,4xx客户端错误跳过;需持久化死信、控制并发goroutine并设置http超时。

Webhook 重试必须带指数退避,不能简单 for 循环重试
直接用 for + time.Sleep 固定间隔重试,大概率导致下游服务雪崩或被限流。Go 标准库没有内置指数退避,得自己控制重试间隔:从 1s 开始,每次 ×2,上限建议设为 30–60s,总重试次数控制在 5–7 次以内。
关键点:
- 每次重试前生成唯一
retry_id(比如用uuid.NewString()),和原始事件 ID 一起记录日志,方便排查哪次重试失败 - 重试间隔要 jitter(加随机偏移),避免大量请求在同一时刻涌向下游,例如:
base * (2 ^ n) * (0.5 + rand.Float64()*0.5) - HTTP 客户端必须设置超时——
Timeout控制整个请求生命周期,IdleConnTimeout防连接池堆积,推荐分别设为 10s 和 30s
死信队列不能只靠内存 channel,得持久化落地
用 chan 或 sync.Map 存失败事件,进程一挂就全丢。真实场景下,死信必须写入磁盘或外部存储。最轻量且可靠的做法是写本地文件(按天分片),配合定期人工检查或自动告警。
实操建议:
- 死信路径用
./dlq/webhook_20240520.jsonl这类格式,每行一个 JSON 事件(含原始 payload、错误信息err.Error()、重试次数、时间戳) - 写入前先
os.OpenFile(..., os.O_CREATE|os.O_WRONLY|os.O_APPEND),用bufio.Writer缓冲,每 10 条或 1s 刷一次盘,避免高频 I/O - 不要在重试 goroutine 里同步写死信——改用无缓冲 channel 转发给单独的 writer goroutine,防止阻塞主流程
重试触发时机要区分错误类型,网络失败才重试
不是所有 HTTP 错误都该重试。400 Bad Request、401 Unauthorized、404 Not Found 这类客户端错误,重试毫无意义;而 5xx、连接超时、DNS 解析失败、TLS 握手失败才需要重试。
判断逻辑应明确:
- 检查
err != nil:包括net.OpError、url.Error、http.ErrHandlerTimeout - 检查
resp.StatusCode >= 500:注意别漏掉503 Service Unavailable和504 Gateway Timeout - 跳过
4xx(除429 Too Many Requests可选重试,但必须读取Retry-Afterheader) - 对
429,优先用响应头里的Retry-After,没这个头再 fallback 到指数退避
并发重试需控制 goroutine 数量,避免打爆本地资源
每个 webhook 失败都起一个 goroutine 重试,积压几百个失败事件就会创建几百个 goroutine,内存和调度开销陡增,还可能触发系统级限制(如 ulimit -u)。
稳妥做法是用 worker pool 控制并发度:
- 启动固定数量(如 4–8 个)长期运行的 worker goroutine,从任务 channel 中取重试任务
- 重试任务结构体包含:
eventID、targetURL、payload、retryCount、nextDelay - worker 拿到任务后执行单次请求,成功则返回;失败则根据策略决定是否重新推回 channel(并更新
nextDelay) - channel 用带缓冲的(如
make(chan retryTask, 100)),防突发流量压垮 worker
本地开发时容易忽略 ulimit 和文件描述符限制,上线前务必用 ulimit -n 检查,并在初始化时调用 http.DefaultTransport.MaxIdleConnsPerHost = 100 配合调整。











