首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

服务稳定性建设之超时和重试机制 学习笔记

  • 25-04-23 01:21
  • 3654
  • 6689
juejin.cn

前言

双十一大促或者春运抢票这样的流量高峰期,是最考验系统稳定性的时候,在这样的高并发场景下,下游访问偶尔响应时间变得很长时,可能给我们的服务带来怎样的影响呢?

当请求下游迟迟不能返回结果时,我们服务与下游服务之间的连接就无法释放,而且正在等待请求返回的协程也会被读请求给阻塞住。一旦响应时间变长的请求数量变多,极有可能使我们服务的机器资源被耗尽,最终使得我们的服务崩溃。

image.png

那么,面对请求下游偶现超时,进而可能导致我们服务崩溃的这种情况,我们又该采取什么样的措施来应对呢?

超时机制

很容易想到的一个方案就是采取超时主动快速失败的策略。若下游在一定时间内没有返回,则我们的服务就主动释放请求下游占用的连接和协程,避免因无限等待下游,导致系统的资源被耗尽⽽宕机。

超时时间设置多少合适?

谈到超时,其中很重要的一点是超时时间的设置。如果超时时间设置过短,就像下面的图一样,假设下游服务访问数据库就要 50ms,而你将调用下游的超时时间设置为 40ms,就会导致调用下游大量超时失败。

image.png

但超时时间也不能设置过长,过长达不到快速失败、释放资源的目的。比如就像下面的图一样,上游调用你的服务设置超时时间是 30ms,而你调用下游服务的超时时间设置为 40ms,实际上,⼀旦下游发生延时抖动,上游服务 30ms 就超时返回了,你对下游的调⽤在 30ms 之后返回毫无意义。

image.png

为了避免调用下游大量超时失败,我们可以基于调用下游服务的 p99 延时(99% 的请求都在这个时间内返回),外加一定的冗余时间作为超时时间。而且为了尽量避免无意义的等待,这个超时时间应该小于上游调用我们服务设置的超时时间。

image.png

超时快速失败的方案的确可以尽量防止我们被下游拖垮,但是假如我们调用下游的请求是一个重要请求,比如说在下单前的校验请求,超时失败会直接导致用户下不了单,造成公司收入上的损失。对于这种重要请求超时,我们就要特别对待了,需要采取一些重试机制。

重试机制

实际上,由于网络抖动或者下游服务单台机器的问题,线上请求偶尔出现超时是非常正常的。

为了提升我们服务整体的可用性,在确保下游服务接口调用幂等的情况下,你可以采取超时重试的策略。在超时失败时,向下游服务的另外一台机器发起请求,在很多时候,重试请求都能够成功收到下游响应。

然而,重试不是无限的重试,对于每个请求,我们都需要限制超时重试的次数。

  • 一方面是避免因重试次数过多,增加下游系统的负载,导致超时现象更加严重。
  • 另一方面是为了节约机器资源,在我们的上游服务调用超时返回之后,我们对下游的重试请求会占用连接和协程资源,并且毫无意义。例如,上游调你的服务超时时间是 30ms,在 30ms 之后它就返回了,你在 30ms 之后对下游的重试属于无效重试。 image.png
重试次数设置多少合适?

实践中,一般设置成 2-3 次,而且这个重试次数,需要小于上游超时时间除以我们调下游的超时时间得到的次数,避免无效重试。

然而,如果所处的是高并发场景,在此情形下,还必须警惕一种情况,那就是当下游出现较多超时时,我们的服务若频繁进行大量重试,是极有可能把下游服务给压垮的。

现在很多系统采用的都是微服务架构,除了我们自身服务会有重试操作外,当对下游服务的调用一直失败时,位于我们上游链路的服务说不定也会展开重试操作,这样一来,便很可能引发重试风暴问题,进而使得下游服务的故障程度进一步加剧。

例如,假设我们的服务在调用下游服务时会重试 3 次,而上游的服务 B 因为调用失败又会重试 3 次,再往上的服务 A 由于超时失败同样也会重试 3 次。这样一来,虽然用户发出的仅仅是一个请求,实际上我们的下游服务累计会被调用多达 27 次。

image.png

重试链路中止 + 熔断

为了避免大面积重试把下游服务打垮,可以采用链路中止策略,对于上游链路过来的重试请求,不再对下游进行超时重试,避免重试风暴问题。而且,对下游的重试调用,你可以设置重试阈值熔断,当在一个时间窗口内,重试请求达到正常请求的一定比例,就不再进行重试。这样就避免因层层重试导致下游服务过载或雪崩。

具体策略可以有:

  1. 通过请求上下文控制链路重试最大次数:每个请求都会经过多个服务,在请求链路中 增加一个重试次数的标识,这样每个服务都可以判断 整个请求链路已经被重试了多少次,如果超过设定的阈值,就 不再进行重试,直接返回错误。例如在 HTTP header或 gRPC metadata 里增加 X-Retry-Count 字段,每个服务接收请求时,检查该字段的值,超过阈值则拒绝重试,转发请求时增加 X-Retry-Count 的值。
  2. 请求幂等判断(防重放设计):如果每个重试请求都重新执行一次操作(如创建订单),可能会导致 重复操作,因此需要判重,可以在请求中携带唯一 Request-ID(Trace ID) ,下游服务存储已处理的 Request-ID,如果接收到相同请求,直接返回缓存结果,而不是重新执行,确保即使发生重试,也不会多次执行相同的业务逻辑。
  3. 引入指数退避+随机抖动策略:如果必须进行重试,最好不要立即重试,减少瞬时高并发请求,导致请求拥堵。
  4. 熔断器(Circuit Breaker):微服务架构通常使用熔断器 来避免对故障服务持续发送请求。典型的熔断器实现包括:当失败次数达到阈值时,短时间内直接拒绝请求,一段时间后尝试半开(Half-Open),如果服务恢复,则恢复调用。go服务可以参考这个包:github.com/alibaba/sentinel-golang/core/circuitbreaker 或者 github.com/sony/gobreaker
  5. API 网关 / Service Mesh 限制重试:在 API 网关(如 Kong、Nginx、Envoy)中配置最大重试次数,超过后直接失败;在Service Mesh(如 Istio、Linkerd)可以限制每个请求的最大重试次数。

总结

在高并发场景,当下游访问偶现响应时间很长时,我们究竟应当采取哪些措施,才能够尽可能地确保我们所提供的服务始终保持稳定且可用呢?

  1. 首先,我们需要对下游调用设置合理的超时时间,避免因长时间等待下游返回,我们服务的机器资源不能释放而耗尽;
  2. 其次,如果对下游的调用是重要的请求类型,比如说涉及到关键业务流程的校验等环节,在保证下游调用是幂等的情况下,我们需要进行超时重试,尽量提升我们服务整体的可用性;
  3. 接着,我们需要采用链路中止策略,避免重试风暴给下游造成较大压力;
  4. 最后,我们需要设置重试阈值熔断,控制重试比例,避免大面积超时重试直接把下游打崩。

image.png

一句话小结:给线上服务配置超时时间,对于重要的下游调用可以加上重试策略,确保你的服务更加稳定可用。

参考

《go服务开发高手课》

注:本文转载自juejin.cn的GoPilot的文章"https://juejin.cn/post/7478949354112745482"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top