服务治理
当从单体服务架构转变为微服务架构后,多个服务之间会通过网络调用形成错综复杂的依赖关系。但网络是脆弱的,RPC 请求有较大概率会遇到超时抖动、抖动、掉线等异常情况,都会影响微服务的可用性。通常对于上游服务调用请求超过下游服务的接受能力可能会导致下游服务瘫痪,如果下游服务质量不佳也会影响上游服务的质量。从两方面来考虑这个问题
- 容错性:接受下游服务的不可靠,在架构设计时充分考虑故障出现的容错方案。
- 流量控制:下游要对上游调用采取预防性策略,上游应该感知和保护下游服务。
1. 重试
由于网络抖动引起的服务之间调用请求失败,最简单的解决办法是重试,来提高调用的最终成功率。但是重试还需要考虑多种因素,如果下游服务不可用依然重试可能导致更严重的问题,需要合理控制重试时机和次数,如何对多次请求保持一致的效果涉及幂等性。
1.1 幂等性
对于可以被重试调用的接口应该满足幂等性。幂等指的是对于某系统接口,无论同一请求被重复执行多少 次,都应该与执行一次的结果相同。满足幂等性的接口被称为“幂等接口”,只有幂等接口可以被安全地重试调用。
读接口是天然幂等接口,但是写性质的 RPC 接口需要查看多次改变数据的结果是否与一次改变数据的结果相同。如何保证幂等性,常用方法如下。
1)分布式锁
接口使用 Redis 的 SET 命令保存已处理请求的UUID,并结合 NX 参数保证:当且仅当件不存在是才写入成功,否则不写入。同时考虑到 Redis 是基于内存的需要设置合理的过期时间 EX。SET UUID EX 3600 NX
- ( 1 ) 接口接受到请求,先尝试在 Redis 中执行 SET UUID NX 命令写入请求的UUID。
- ( 2 ) 写入成功,则说明请求未被处理过,可以正常处理。
- ( 3 ) 写入失败,说明请求已经被接口处理过,直接返回成功。

2) 数据库防重表
利用数据库的唯一索引的特点,创建一个表用于保存接口处理过的请求记录,并以UUID作为表的唯一索引。

3)token

4)其他
也可以通过版本号来保证幂等性,通过比较版本号来确定请求是否已经被处理,此外还可以通过状态机,待支付→支付中→已支付→已完成,通过判断状态来确保幂等性。
1.2 重试时机
RPC 接口调用遇到的错误可以分为以下几类。
- 业务逻辑错误,例如下游服务处理扣款发现用户余额不足或者请求有非法参数。
- 服务质量异常错误,下游请求限流、拒绝提供服务、下游触发熔断。
- 网络错误,请求超时、丢包、断网。
当遇到业务逻辑错误是不应该重试,因为此请求涉及的业务逻辑有异常,无论重试多少次都是一样的结果;当RPC接口调用遇到服务质量异常错误时, 由于服务质量处于异常状态是有一定的持续时间的,使用无退避策略(立即重试请求)会大概率继续请求失败,所以此时适合使用各种有退避的策略,比如指数退避策略,这样可以在一定程度上给足下游服务质量恢复的时间;当RPC接口调用遇到网络错误时,因为 网络错误具有随机性,重试请求再次失败的概率很小,所以可以在下游服务未熔断的前提下立即重试请求。
1.3 重试风险与风暴
重试既可以提高服务质量但也会给下游服务带来风险。当下游服务因质量下降而导致上游服务请求超时,而上游如果依然重试,这回导致下游服务负载持续升高最后拖垮整个服务。
重试带来的请求量放大会使下游服务的负载雪上加霜,如果不控制重复次数则会导致请求被无限放大,通常一个微服务链路涉及多层调用,每一层都会把上一层的请求重试放大,这样的级联重试会导致下游服务难以承受,这种现象被称为“重试风暴”。通常会将重试次数控制在三次以内。
1.4 重试控制
- 不重试
对于一些不必要的下游服务,可以接受其不执行请求重试 - 重试请求比
通过请求重试与正常请求之间的比例来决定是否重试。
2. 熔断与隔离
熔断和隔离都是上游服务可以采取的流量控制策略,其中熔断可以有效防止我们的服务被下游服务拖垮,同时可以在一定程度上保护下游服务;而隔离可以防止一个服务内各个接口之间因质量问题而相互影响。
2.1 服务雪崩
“服务雪崩”(Service Avalanche)是分布式系统中最严重的故障模式之一,指当系统中的某个核心服务或依赖节点发生故障时,故障通过服务间的依赖关系像多米诺骨牌一样蔓延,最终导致整个系统或大面积服务集群瘫痪的现象。
2.2 Hystrix 熔断器
Hystrix 熔断器的核心是**“状态机模型”**,通过监控依赖服务的调用状态(成功、失败、超时、拒绝),动态切换熔断器的三种状态,从而实现故障的“熔断-恢复”闭环。
熔断器的三态
Hystrix 熔断器存在三种核心状态,状态转换由“调用指标阈值”触发(如失败率、请求量)。
| 状态(State) | 核心逻辑 | 触发条件 |
|---|---|---|
| 闭合(Closed) | 熔断器默认状态,依赖调用正常执行; 同时监控调用指标(失败率、请求数等)。 | 初始状态;或“半打开”状态下,试探请求成功率达标后切换。 |
| 打开(Open) | 熔断器触发“熔断”,直接拒绝所有依赖调用,转而执行降级逻辑; 避免无效请求占用资源。 | 一定时间窗口内,满足两个条件: 1. 调用次数 ≥ 最小请求数(如20次); 2. 失败率 ≥ 阈值(如50%)。 |
| 半打开(Half-Open) | 熔断器从“打开”状态切换而来,允许少量试探请求调用依赖服务; 用于判断依赖是否恢复。 | “打开”状态持续一段时间(如5秒)后,自动切换为“半打开”。 |
状态转换流程:
闭合 →(触发阈值)→ 打开 →(等待恢复期)→ 半打开 →(试探成功)→ 闭合;
闭合 →(触发阈值)→ 打开 →(等待恢复期)→ 半打开 →(试探失败)→ 打开。

2.3 Resilience4j 和 Sentinel 熔断器
| 熔断器 | 熔断策略 |
|---|---|
| Hystrix | 时间窗口请求失败率 |
| Resilience4j | 时间窗口请求失败率和慢调用比例 |
| Sentinel | 时间窗口请求失败率和慢调用比例、错误计数 |
2.4 隔离
一般的业务服务都会定义固定大小的线程池来处理对下游服务的请求调用,在下游服务响应慢的情况下,线程池作为服务内共享资源有可能导致服务内各接口的质量相互影响。
在船舶工业当中经常会使用舱壁将船舱分隔成多个隔离的空间,当一个船舱漏水时不会影响到其他船舱。“舱壁”的概念可以被应用在资源隔离问题上,我们需要以某种形式的 “舱壁”将共享资源分隔为多个独立资源。
实现
1) 线程池隔离
为每一个服务调用创建一个单独的线程池,但是存在问题当一个服务涉及多个服务的调用时会导致线程池过多从而导致较大的内存开销,同时也会带来更大线程调度和上下文切换到的开销。
2) 信号量隔离
信号量隔离(Semaphore Isolation) 是一种轻量级的隔离方式,核心通过 “信号量计数器” 控制对依赖服务的并发访问量,避免单个依赖的故障耗尽系统全局资源。
3. 限流
前面所提到的重试、熔断、隔离都是在上游服务的操作为了提高服务质量和适当保护下游服务而采用的策略。限流是为了应对上游服务的请求过多导致服务被打垮。
3.1 基于时间窗口
通过固定时间窗口实现,入限制 1s 内请求不超过100个,将时间线划分为以 1s 为单位的时间窗口,每个窗口都维护这 1s 内允许通过的计数100。当请求来时计数大于 0,则计数减 1 并允许请求通过,否则被丢弃。

固定时间存在明显缺点,当请求集中在后 500ms,和下一个窗口的前 500ms 时,1s 内的请求数远超100,限流效果差。
通过滑动窗口来解决该缺点,将时间划分为更加细粒度的槽,并且时间口随时间不断向后滑动。

3.2 漏桶算法
一个漏桶承接水流,并通过一个出水口匀速出水,当水流过大、漏桶已满时会导致水溢出。水流被视为进入服务器的请求,出水口匀速出水可被视为服务器处理请求的固定速率,当请求过多导致漏桶满了时,将开始拒绝新来的请求。

漏斗限流算法存在明显缺陷,当有并发请求时,服务有能力处理但是依然需要排队处理导致性能浪费。
3.3 令牌桶算法

核心思想是通过「固定速率生成令牌、按需消耗令牌」实现流量削峰填谷,其核心逻辑可概括为 3 点:
- 桶的容量固定:存在一个预设容量的“令牌桶”,桶内最多存储
capacity个令牌(超出部分会被丢弃)。 - 令牌匀速生成:系统按固定速率
rate(如 10 个/秒)向桶内补充令牌,若桶已满则新令牌直接丢弃。 - 请求按需取令牌:每次请求需要获取
n个令牌(默认 1 个):- 若桶内令牌数 ≥
n:取走n个令牌,请求放行; - 若桶内令牌数 <
n:请求被拒绝(或等待令牌补充,取决于是否配置超时)。
- 若桶内令牌数 ≥
关键优势:支持“突发流量”——若桶内有存量令牌,短时间内的高并发请求可直接消耗令牌,无需等待;当令牌耗尽后,流量会被限制在 rate 速率内,避免后端过载。
4. 自适应限流
前述的限流策略都是人为提前设定的,但在实际情况当中每个服务的性能都是动态变化的,当进行服务性能优化之后可能就需要调整限流策略,而当服务变得复杂或者性能下降也需要及时的调整策略,很显然这不是一个合适的方法,因此我们需要一个动态的限流策略——自适应限流。
4.1 基于请求排队时间
Dagor 是微信团队研发的微服务过载控制系统。Dagor 使用等待队列中请求的平均等待时间来判断服务是否过载,排队时间 = 请求开始处理时间 - 到达服务器的时间。
为什么Dagor不使用CPU使用率作为服务过载的检测标准?这是因为CPU使用率过高固然可以反映出一个服务是否处于高负载的状态,然而,它只是一个必要不充分条件, 只要服务可以及时处理请求,即使CPU使用率再高,我们也不应该认为服务过载,此时不宜限流。
为什么Dagor不使用请求响应时间(请求最终被响应的时间与请求到达服务的时间的 差值)作为服务过载的检测标准?这是因为请求响应时间受下游服务请求处理能力的影响 过大,如果下游服务的请求处理能力不佳,则不应该认为调用者服务负载过高。
4.2 基于延迟比率
Netflix concurrency-limits组件的核心思想“基于系统实时状态动态调整限流窗口”展开,本质是通过反馈机制平衡系统稳定性与资源利用率。
核心:以“延迟/队列”为反馈信号,动态收敛限流窗口
所有算法的底层逻辑可概括为:
通过实时监测请求延迟(如RTT)或队列长度等指标,判断系统当前负载状态(是否过载、是否有排队),并据此动态调整允许的最大并发请求数(限流窗口),最终使窗口收敛到一个“既能保证请求延迟较低,又能最大化吞吐量”的合理范围。
gradient算法:基于延迟比率的自适应调整(核心实现)
作为基础算法,其逻辑最具代表性,核心依赖两个公式构建反馈闭环:
-
梯度(gradient)计算:衡量延迟偏离程度
以“无负载最佳延迟”与“当前实际延迟”的比率作为判断依据:gradient = RTT_noload / RTT_actual- RTT_noload:取最近一段时间的最小请求延迟(代表系统无负载时的最优状态);
- RTT_actual:当前采样请求的实际延迟。
- 含义:gradient=1 表示无请求排队,系统处于理想状态;gradient<1 表示存在排队(实际延迟高于最优值),需收紧限流。
-
限流窗口调整:结合梯度与排队容忍度
基于gradient动态更新限流窗口大小:new_limit = current_limit × gradient + queue_size- current_limit:当前限流窗口大小(初始值较小,逐步调整);
- queue_size:允许的排队请求数(通常取current_limit的平方根,控制合理排队范围);
- 特性:窗口较小时,乘gradient后增长更快(快速探索系统能力);窗口较大时,增长放缓(避免过度激进导致过载),类似TCP拥塞控制的“慢启动”与“拥塞避免”逻辑,既响应突发流量,又防止系统被压垮。
4.3 BBR limiter
Kratos 框架的 BBR 限流器是一种基于 CPU 负载的自适应过载保护机制,其核心思想是通过动态监测系统资源状态,自动调整限流策略,避免服务因过载而崩溃。
核心设计理念
Kratos的BBR限流器借鉴了TCP拥塞控制算法的思想,但将其从网络层迁移到了应用层,核心目标是:
- 实时感知系统压力:通过CPU使用率、请求处理量、响应时间等指标动态评估系统负载。
- 动态调整限流阈值:根据历史数据计算系统最大承载能力,避免静态配置的局限性。
- 优雅降级:在过载时主动丢弃部分请求,确保服务可用性,而非被动等待崩溃。
5. 降级策略
服务降级的目的是重点保障用户的 核心体验和服务的可用性。在异常、高并发的情况下可以忽略非核心场景或换一种简单处 理方式,以便释放资源给核心场景,保证核心场景的正常处理与高性能执行。服务降级的 实施方案灵活性较大,一般与业务场景息息相关,接下来我们介绍几种思路。
5.1 服务依赖度降级
服务依赖度指的是下游服务对上游服务的重要程度。优先保证更重要的服务,将不必要的关闭或减少资源。
5.2 读请求降级
读请求的服务降级策略主要是缓存和兜底数据。缓存可以在配置中心动态控制可层之间是否使用缓存,以及缓存的过期时间。兜底数据可以是另一个数据源的数据,也可以是静态数据。

5.3 写请求降级
写请求降级策略有异步写和写聚合。在某些业务场景下,我们还可以直接丢弃写请求,一个值得介绍的例子就是直播间弹幕自见。
假设直播间有100万用户,每秒有100个用户发送弹幕,为了让100万用户都看见,弹幕服务需要每秒下发1亿条消息,网络带宽被占满。
降级:自见、直接丢弃消息、随机发给部分用户。
6. 小结

要点:
1)重试:提高单次请求成功率。接口要保持幂等性,防止重试风暴。
2)熔断:上游服务保护下游服务。
3)隔离:防止下游服务调用相互影响,线程池隔离,信号量隔离策略。
4)限流:保护自身。
5)降级:保障服务核心功能的可用性。
1597

被折叠的 条评论
为什么被折叠?



