微服务可靠性-超时
超时控制
超时控制,我们的组件能够快速失效(fail fast),因为我们不希望等到断开的实例直到超时。没有什么比挂起的请求和无响应的界面更令人失望。这不仅浪费资源,而且还会让用户体验变得更差。我们的服务是互相调用的,所以在这些延迟叠加前,应该特别注意防止那些超时的操作。
- 网络传递具有不确定性。
- 客户端和服务端不一致的超时策略导致资源浪费。
- ”默认值”策略。
- 高延迟服务导致 client 浪费资源等待,使用超时传递: 进程间传递 + 跨进程传递。
超时控制是微服务可用性的第一道关,良好的超时策略,可以尽可能让服务不堆积请求,尽快清空高延迟的请求,释放 Goroutine。
实际业务开发中,我们依赖的微服务的超时策略并不清楚,或者随着业务迭代耗时超生了变化,意外的导致依赖者出现了超时。
- 服务提供者定义好 latency SLO,更新到 gRPC Proto 定义中,服务后续迭代,都应保证 SLO。(避免出现意外的默认超时策略,或者意外的配置超时策略。)
- kit 基础库兜底默认超时,比如 100ms,进行配置防御保护,避免出现类似 60s 之类的超大超时策略。
- 配置中心公共模版,对于未配置的服务使用公共配置。
1 2 3 4 5 6 7 8 9 10 11 |
package google.example.library.v1; service LibraryService { // 可以标注清除, 超时 95线 和 99线 // Lagency SLO: 95th in 100ms, 99th in 150ms. rpc CreateBook(CreateBookRequest) returns (Book); rpc GetBook(GetBookRequest) returns Book); rpc ListBooks(ListBooksRequest) returns (ListBooksResponse); } |
ctx超时传递
超时传递: 当上游服务已经超时返回 504,但下游服务仍然在执行,会导致浪费资源做无用功。超时传递指的是把当前服务的剩余 Quota 传递到下游服务中,继承超时策略,控制请求级别的全局超时控制。
进程内超时控制
一个请求在每个阶段(网络请求)开始前,就要检查是否还有足够的剩余来处理请求,以及继承他的超时策略,使用 Go 标准库的 context.WithTimeout
。
1 2 3 4 5 6 7 8 9 |
func (c *asiiConn) Get(ctx context.Context, key string) (result *Item, err error) { c.conn.SetWriteDeadline(shrinkDeadline(ctx, c.writeTimeout)) if _, err = fmt.Fprintf(c.rw, "gets %s\r\n", key); err != nil { // ... } // ... } |
跨进程传递
- A gRPC 请求 B,1s超时。
- B 使用了300ms 处理请求,再转发请求 C。
- C 配置了600ms 超时,但是实际只用了500ms。
- 到其他的下游,发现余量不足,取消传递。
在需要强制执行时,下游的服务可以覆盖上游的超时传递和配额。
在 gRPC 框架中,会依赖 gRPC Metadata Exchange,基于 HTTP2 的 Headers 传递 grpc-timeout 字段,自动传递到下游,构建带 timeout 的 context。
双峰分布
- 95%的请求耗时在100ms内,5%的请求可能永远不会完成(长超时)。
- 对于监控不要只看 mean,可以看看耗时分布统计,比如 95th,99th。
- 设置合理的超时,拒绝超长请求,或者当Server 不可用要主动失败。
超时决定着服务线程耗尽。
Case Study
- SLB 入口 Nginx 没配置超时导致连锁故障。
- 服务依赖的 DB 连接池漏配超时,导致请求阻塞,最终服务集体 OOM。
- 下游服务发版耗时增加,而上游服务配置超时过短,导致上游请求失败。