ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

极简架构设计与微服务拆分:超时重试怎样才不放大故障

极简架构设计与微服务拆分:超时重试怎样才不放大故障 极简架构设计与微服务拆分超时重试怎样才不放大故障在单体架构里方法调用是进程内的要么成功要么抛出异常几乎没有中间状态。但在微服务拆分后网络变成了最不可靠的变量。很多团队在微服务拆分初期为了保证接口成功率会在 RPC 客户端加上简单的for循环重试“调用失败就自动重试 3 次”。这种粗暴的重试逻辑往往是微服务雪崩的罪魁祸首。当下游连接池饱和时固定次数、无退避的重试会放大请求量。应使用超时、重试预算、指数退避和抖动具体放大倍数取决于调用拓扑。重试风暴与级联隔离机制在分布式极简架构设计中重试绝不是简单的“失败就再试一次”。未经治理的重试是故障放大器必须通过重试预算Retry Budget、指数退避Exponential Backoff与随机抖动Jitter建立防线。缺乏防护时重试会集中涌向故障服务并放大压力重试预算、指数退避和随机抖动能将这部分流量隔离在可控范围内。通过这套机制重试请求在全站流量中的占比被严格限制在 10% 以内。同时退避延时分散了流量峰值给了下游服务自我恢复的时间。生产级 Go 语言 RPC 重试预算与指数退避客户端下面使用 Go 语言实现一个包含重试预算Retry Budget、带 Full Jitter 的指数退避算法以及状态隔离的生产级 RPC 客户端包装器。package main import ( context errors fmt math math/rand sync sync/atomic time ) // RetryBudget 重试预算令牌桶 type RetryBudget struct { tokens int64 maxTokens int64 lastRefill time.Time mu sync.Mutex } func NewRetryBudget(maxTokens int64) *RetryBudget { return RetryBudget{ tokens: maxTokens, maxTokens: maxTokens, } } // CanRetry 判断当前是否还有足够的重试预算防止重试风暴 func (rb *RetryBudget) CanRetry() bool { rb.mu.Lock() defer rb.mu.Unlock() // 规则每次重试消耗 10 个 Token常规成功请求会补充 1 个 Token if rb.tokens 10 { rb.tokens - 10 return true } return false } // RecordSuccess 成功请求回补 Token func (rb *RetryBudget) RecordSuccess() { rb.mu.Lock() defer rb.mu.Unlock() if rb.tokens rb.maxTokens { rb.tokens } } // ClientRPC 客户端 type ClientRPC struct { budget *RetryBudget totalAttempts int64 failedRetries int64 } func NewClientRPC() *ClientRPC { return ClientRPC{ budget: NewRetryBudget(100), // 初始 100 令牌最多连续重试 10 次 } } // CallWithRetry 带治理的重试调用 func (c *ClientRPC) CallWithRetry(ctx context.Context, serviceName string, req string) (string, error) { maxRetries : 3 baseBackoff : 50 * time.Millisecond maxBackoff : 1000 * time.Millisecond for attempt : 0; attempt maxRetries; attempt { atomic.AddInt64(c.totalAttempts, 1) // 执行实际 RPC 请求 resp, err : c.doRemoteRpcCall(ctx, req, attempt) if err nil { c.budget.RecordSuccess() return resp, nil } // 达到最大重试次数不再重试 if attempt maxRetries { return , fmt.Errorf(rpc_failed: 达到最大重试次数 (%d): %w, maxRetries, err) } // 检查重试预算预算不足时直接快速失败严禁放大故障 if !c.budget.CanRetry() { atomic.AddInt64(c.failedRetries, 1) return , fmt.Errorf(retry_budget_exhausted: 重试预算已耗尽放弃第 %d 次重试: %w, attempt1, err) } // 计算带 Full Jitter 的指数退避延时 sleepDuration : c.calculateJitterBackoff(attempt, baseBackoff, maxBackoff) fmt.Printf([Retry] 服务 [%s] 第 %d 次请求失败, 等待 %v 后重试...\n, serviceName, attempt1, sleepDuration) select { case -time.After(sleepDuration): case -ctx.Done(): return , fmt.Errorf(context_cancelled: 上下文已取消终止重试: %w, ctx.Err()) } } return , errors.New(unknown_error) } // 计算 Full Jitter 指数退避Sleep random(0, min(maxBackoff, base * 2^attempt)) func (c *ClientRPC) calculateJitterBackoff(attempt int, base, max time.Duration) time.Duration { temp : float64(base) * math.Pow(2, float64(attempt)) currentMax : time.Duration(temp) if currentMax max { currentMax max } // 引入 Full Jitter 随机打散防止所有客户端在同一毫秒并发重试造成二次冲击 return time.Duration(rand.Int64n(int64(currentMax))) } func (c *ClientRPC) doRemoteRpcCall(ctx context.Context, req string, attempt int) (string, error) { // 模拟前两次尝试均遇到短暂网络波动失败 if attempt 2 { return , errors.New(network_timeout: 远程 RPC 响应超时) } return RPC_SUCCESS_RESPONSE, nil } func main() { rand.Seed(time.Now().UnixNano()) client : NewClientRPC() ctx, cancel : context.WithTimeout(context.Background(), 3000*time.Millisecond) defer cancel() resp, err : client.CallWithRetry(ctx, OrderService, {\order_id\: 1024}) if err ! nil { fmt.Printf(调用最终失败: %v\n, err) } else { fmt.Printf(调用成功收到结果: %s\n, resp) } }在这段生产级重试治理代码中有两项设计取舍是避免故障放大的关键第一引入 Full Jitter 随机退避。普通的指数退避50ms, 100ms, 200ms会导致成百上千个并发请求在同一个时间点齐刷刷重新发起调用。加入了rand.Int64n随机抖动后重试请求在时间轴上被平滑分散开。第二全局 Retry Budget 预算机制。重试不是免费的。当一段时间内失败率太高时Budget 令牌迅速用光系统强行停止重试直接快速失败返回。微服务拆分后故障隔离的三条铁律把单体架构拆分为微服务时关于重试与超时控制有三条原则必须在团队内部严格执行设置全局 Context 传递与级联 Timeout链路最外层设置总超时如 2 秒内层 RPC 调用必须继承并递减这个超时时间。如果总时间只剩 50 毫秒直接终止下游调用。只对可重试的错误码发起重试网络超时、503 服务不可用可以重试但 400 参数错误、401 鉴权失败、404 资源不存在绝对不能重试。在最靠近客户端的最外层做重试严禁在微服务链路的每一层都加重试例如 A 试 3 次B 试 3 次C 试 3 次导致乘法效应 3×3×327 次。重试应该尽量收口在最边缘的 Gateway 或最上层服务。极简架构的精髓不在于代码写得有多复杂而在于用最少、最清晰的规则把潜在的分布式连锁故障阻断在萌芽状态。
返回列表