Go 微服务治理先把超时、重试和限流写明白一、服务治理不是上服务网格才开始很多小团队一聊服务治理就想到服务网格、注册中心、熔断框架和全链路追踪。工具当然有用但最基础的治理应该从每个 Go 服务里开始超时是多少失败是否重试重试几次调用量如何限制降级返回什么。我见过一个挺典型的场景团队引进了服务网格、配了 Istio、挂了 Jaeger、甚至还上了 Chaos Engineering。看起来很完整。结果有一天晚上用户投诉支付页面卡了 15 秒排查了半天发现——下游数据库连接池只有 10 个连接上游某接口没有设超时请求全部挂在那里排队。服务网格那一层什么都没做错它忠实地把流量路由到了正确的后端然后看着请求一个个超时。问题的根在代码里没有人给这个调用写超时。如果这些基础策略没有写清楚上再多平台也救不了。线上事故里最常见的不是没有高级组件而是某个接口无限等待、某个客户端疯狂重试、某个下游被流量打穿。治理的起点不是工具而是每一个服务调用都能回答三个问题最多等多久失败了怎么办打不过来怎么保护自己二、调用链路每层都要有预算flowchart TD A[入口请求 3s预算] -- B[服务A 1s] B -- C[服务B 800ms] B -- D[缓存 100ms] C -- E[数据库 500ms] C -- F[模型推理 300ms] B -- G[降级路径: 20ms]超时要从入口预算往下拆。用户最多等 3 秒下游调用就不能每个都设置 3 秒。否则多级调用叠加后整个链路会拖到不可控。拆解超时预算有个实操技巧先画出依赖拓扑对每个依赖标注最慢可以等多久然后自底向上加。比如数据库查询平均 30ms给它 300ms 足够覆盖抖动模型推理平均 800ms给它 2s。如果一条链路上有 4 个串行调用每个设 1s那总时间至少 4s远超入口预算。这时候就要考虑并行调用、缓存或异步化。超时还要分读和写。读超时通常可以短一点写超时必须留足处理时间——但也要有上限。一个没有超时的写请求如果下游卡住连接和 goroutine 就会一直挂着逐步耗尽资源。三、代码示例context 控制超时package client import ( context net/http time ) // NewClient 创建一个带默认超时的 HTTP 客户端 func NewClient(timeout time.Duration) *http.Client { return http.Client{ Timeout: timeout, // 包括连接读取响应 } } func DoRequest(parent context.Context, req *http.Request) (*http.Response, error) { // 从上层传入的 context 派生确保链路超时一致 ctx, cancel : context.WithTimeout(parent, 800*time.Millisecond) defer cancel() req req.WithContext(ctx) client : NewClient(1 * time.Second) return client.Do(req) } // 批量调用时context 取消应传播到所有子任务 func BatchRequest(ctx context.Context, reqs []*http.Request) ([]*http.Response, error) { ctx, cancel : context.WithCancel(ctx) defer cancel() results : make(chan *http.Response, len(reqs)) for _, req : range reqs { go func(r *http.Request) { resp, err : DoRequest(ctx, r) if err ! nil { // 可以通知其他 goroutine 退出 cancel() return } results - resp }(req) } // … 收集结果 … return nil, nil }context 不是摆设。请求取消后下游调用也应该取消。很多 Go 服务内存和 goroutine 慢慢涨就是因为用户已经断开后台还在做无用功。批量调用的场景特别要注意如果发起 10 个并发请求其中一个失败了要不要取消另外 9 个如果业务逻辑是全成功才返回那一个失败就应该 cancel 其余如果是尽量多地返回那就不该 cancel。这个选择要明确写在代码里不是默认行为。四、工程边界重试要配合幂等和退避重试不是默认开启。读请求、幂等写请求可以谨慎重试非幂等写请求必须有幂等键。重试还要有退避和上限否则下游已经慢了上游一重试压力更大。雪崩常常不是原始流量造成的而是重试放大的。重试退避有几个常见策略固定退避每次重试等固定时间简单但效果一般指数退避每次等待时间翻倍加随机抖动jitter防止同时重试退避加上限指数增长但有封顶防止等待时间过长。生产中推荐指数退避 jitter 上限。不加 jitter 的话大量客户端在同一个时刻发动重试叠加效应会把下游瞬间打穿。限流也要分层。入口限流保护整个系统用户级限流保护公平性下游客户端限流保护依赖服务。不要等下游报错才想起来限制。服务治理的核心是在系统失控前主动收住。取舍方面严格超时会让部分请求失败更快但能保护整体延迟无限等待看似提高成功率实际会拖垮线程、连接池和用户体验。小团队做微服务先把这些基本功做好比追新组件更划算。降级方案要提前写不要事故时现想。比如推荐服务失败时返回热门列表风控评分超时时进入人工复核AI 摘要失败时返回原文链接。降级不是丢脸而是承认系统会失败并把失败控制在用户能接受的范围内。配置也要可治理。超时、重试、限流阈值不要散落在代码里至少要集中配置、带默认值、能审计变更。一次错误配置可能比一次代码 bug 影响更大。最后服务治理要配合压测。没有压测就不知道限流阈值从哪来没有线上指标就不知道压测是否贴近真实。治理参数不是拍脑袋是靠数据慢慢校准。还要建立依赖清单。每个服务依赖哪些下游、是否强依赖、超时多少、失败后能否降级都应该能查到。事故发生时依赖关系越清楚定位越快。很多小团队的服务图只存在老员工脑子里这本身就是风险。服务治理不是一次性项目而是每次新增接口都要顺手做的基本动作。把这些动作做成模板团队执行成本会低很多也更容易坚持。五、总结Go 微服务治理从超时、取消、重试、幂等、限流和降级开始。工具可以后上边界必须先写。能把失败控制住服务才算真正可治理。不管用不用服务网格每个服务的代码里都应该回答那三个问题等多久、怎么重试、打不过来怎么办。
Go 微服务治理:先把超时、重试和限流写明白
Go 微服务治理先把超时、重试和限流写明白一、服务治理不是上服务网格才开始很多小团队一聊服务治理就想到服务网格、注册中心、熔断框架和全链路追踪。工具当然有用但最基础的治理应该从每个 Go 服务里开始超时是多少失败是否重试重试几次调用量如何限制降级返回什么。我见过一个挺典型的场景团队引进了服务网格、配了 Istio、挂了 Jaeger、甚至还上了 Chaos Engineering。看起来很完整。结果有一天晚上用户投诉支付页面卡了 15 秒排查了半天发现——下游数据库连接池只有 10 个连接上游某接口没有设超时请求全部挂在那里排队。服务网格那一层什么都没做错它忠实地把流量路由到了正确的后端然后看着请求一个个超时。问题的根在代码里没有人给这个调用写超时。如果这些基础策略没有写清楚上再多平台也救不了。线上事故里最常见的不是没有高级组件而是某个接口无限等待、某个客户端疯狂重试、某个下游被流量打穿。治理的起点不是工具而是每一个服务调用都能回答三个问题最多等多久失败了怎么办打不过来怎么保护自己二、调用链路每层都要有预算flowchart TD A[入口请求 3s预算] -- B[服务A 1s] B -- C[服务B 800ms] B -- D[缓存 100ms] C -- E[数据库 500ms] C -- F[模型推理 300ms] B -- G[降级路径: 20ms]超时要从入口预算往下拆。用户最多等 3 秒下游调用就不能每个都设置 3 秒。否则多级调用叠加后整个链路会拖到不可控。拆解超时预算有个实操技巧先画出依赖拓扑对每个依赖标注最慢可以等多久然后自底向上加。比如数据库查询平均 30ms给它 300ms 足够覆盖抖动模型推理平均 800ms给它 2s。如果一条链路上有 4 个串行调用每个设 1s那总时间至少 4s远超入口预算。这时候就要考虑并行调用、缓存或异步化。超时还要分读和写。读超时通常可以短一点写超时必须留足处理时间——但也要有上限。一个没有超时的写请求如果下游卡住连接和 goroutine 就会一直挂着逐步耗尽资源。三、代码示例context 控制超时package client import ( context net/http time ) // NewClient 创建一个带默认超时的 HTTP 客户端 func NewClient(timeout time.Duration) *http.Client { return http.Client{ Timeout: timeout, // 包括连接读取响应 } } func DoRequest(parent context.Context, req *http.Request) (*http.Response, error) { // 从上层传入的 context 派生确保链路超时一致 ctx, cancel : context.WithTimeout(parent, 800*time.Millisecond) defer cancel() req req.WithContext(ctx) client : NewClient(1 * time.Second) return client.Do(req) } // 批量调用时context 取消应传播到所有子任务 func BatchRequest(ctx context.Context, reqs []*http.Request) ([]*http.Response, error) { ctx, cancel : context.WithCancel(ctx) defer cancel() results : make(chan *http.Response, len(reqs)) for _, req : range reqs { go func(r *http.Request) { resp, err : DoRequest(ctx, r) if err ! nil { // 可以通知其他 goroutine 退出 cancel() return } results - resp }(req) } // … 收集结果 … return nil, nil }context 不是摆设。请求取消后下游调用也应该取消。很多 Go 服务内存和 goroutine 慢慢涨就是因为用户已经断开后台还在做无用功。批量调用的场景特别要注意如果发起 10 个并发请求其中一个失败了要不要取消另外 9 个如果业务逻辑是全成功才返回那一个失败就应该 cancel 其余如果是尽量多地返回那就不该 cancel。这个选择要明确写在代码里不是默认行为。四、工程边界重试要配合幂等和退避重试不是默认开启。读请求、幂等写请求可以谨慎重试非幂等写请求必须有幂等键。重试还要有退避和上限否则下游已经慢了上游一重试压力更大。雪崩常常不是原始流量造成的而是重试放大的。重试退避有几个常见策略固定退避每次重试等固定时间简单但效果一般指数退避每次等待时间翻倍加随机抖动jitter防止同时重试退避加上限指数增长但有封顶防止等待时间过长。生产中推荐指数退避 jitter 上限。不加 jitter 的话大量客户端在同一个时刻发动重试叠加效应会把下游瞬间打穿。限流也要分层。入口限流保护整个系统用户级限流保护公平性下游客户端限流保护依赖服务。不要等下游报错才想起来限制。服务治理的核心是在系统失控前主动收住。取舍方面严格超时会让部分请求失败更快但能保护整体延迟无限等待看似提高成功率实际会拖垮线程、连接池和用户体验。小团队做微服务先把这些基本功做好比追新组件更划算。降级方案要提前写不要事故时现想。比如推荐服务失败时返回热门列表风控评分超时时进入人工复核AI 摘要失败时返回原文链接。降级不是丢脸而是承认系统会失败并把失败控制在用户能接受的范围内。配置也要可治理。超时、重试、限流阈值不要散落在代码里至少要集中配置、带默认值、能审计变更。一次错误配置可能比一次代码 bug 影响更大。最后服务治理要配合压测。没有压测就不知道限流阈值从哪来没有线上指标就不知道压测是否贴近真实。治理参数不是拍脑袋是靠数据慢慢校准。还要建立依赖清单。每个服务依赖哪些下游、是否强依赖、超时多少、失败后能否降级都应该能查到。事故发生时依赖关系越清楚定位越快。很多小团队的服务图只存在老员工脑子里这本身就是风险。服务治理不是一次性项目而是每次新增接口都要顺手做的基本动作。把这些动作做成模板团队执行成本会低很多也更容易坚持。五、总结Go 微服务治理从超时、取消、重试、幂等、限流和降级开始。工具可以后上边界必须先写。能把失败控制住服务才算真正可治理。不管用不用服务网格每个服务的代码里都应该回答那三个问题等多久、怎么重试、打不过来怎么办。