通用的服务可用性治理手段
  • 一、上游保护下游
  • 1、重试
  • 2、熔断
  • 3、资源
  • 二、下游自我保护
  • 1、幂等
  • 2、限流
  • 3、降级


内容总结自《亿级流量系统架构设计与实战》

一、上游保护下游

1、重试

接口超时情况:

  1. 请求发送超时,下游服务并没有接收到请求
  2. 请求处理超时,下游服务接收到请求,但是处理时间超时
  3. 响应请求超时,下游服务已经处理完请求,但是响应报文超时未回复


失败是否重试:

  1. 不重试
    a. 下游服务返回业务异常,即业务请求不符合业务逻辑而返回的错误
  2. 重试
    a. 服务质量异常错误,如反映下游服务稳定性的相关错误
    b. 网络错误,如请求超时、数据丢包、网络抖动、连接断开


重试的退避策略:

  1. 无退避策略:请求失败后立即重试
  2. 线性退避策略:请求失败后等待固定的时间后再重试
  3. 随机退避策略:再一个时间范围内随机选择一个时间等待重试
  4. 指数退避策略:对一个请求连续重试,每次等待时间都是上一次的两倍
  5. 综合退避策略:指数退避策略与随机退避策略结合的形式,各开源消息中间件再对应消息失败时常使用此策略

重试风险:

  1. 不限制重试次数,会导致请求量被无限放大,对下游服务影响也会无限放大
  2. 下游系统因为服务容量不足导致服务负载升高,质量下降。上有服务继续重试不仅无法解决问题,反而会使下游服务负载压力雪上加霜
  3. 重试风暴,即请求链路上涉及多级的服务调用,如A->B->C->D,如果每个接口都具备重试特性,底层服务D的请求压力是指数级上升


规避重试风险:

  1. 非关键下游服务,失败不重试
  2. 当遇到下游服务为限流错误或熔断错误时,不再重试
  3. 防止重试风暴,在接收到上级请求后,检查这个请求是否为上级请求的重试请求,如果是,则在调用下游服务遇到失败时不再重试请求,防止请求量被级联放大。重试请求需要额外携带重试请求标记
  4. 计算一段时间内,重试请求总数与正常请求总数的比例(重试请求比),小于某个特定值时允许重试


2、熔断

由于网络原因或服务设计问题,微服务一般很难保证100%对外可用。如A->B->C->D,D因为请求量突增或设计不合理导致宕机,导致C服务请求大量阻塞,最终拖垮B和A

熔断器(在业务上游)三种状态

  1. Closed:默认关闭,此时认为下游服务可正常提供服务
  2. Open:当服务失败率到达一定阈值后,则会开启,此时认为下游服务不可用,即不再对此下游服务进行请求
  3. Half-Open:熔断器Open一段时间后,会进入该状态,此状态允许一个请求尝试调用下游服务,如果下游调用成功,则状态置为Closed,如果下游服务失败,则状态置为Open


3、资源

资源共享:

  1. 多个服务共用线程池资源


资源隔离:

  1. 新开线程发起服务调用,达到调用下游服务资源的隔离
  2. 上游服务对下游调用服务进行PV信号量的并发控制(类比上游对下游请求的限流)



二、下游自我保护

1、幂等

接口必备幂等特性(读接口天然具备幂等)
幂等性 = 请求接口携带唯一ID + 下游接口具备幂等校验策略

请求接口携带唯一ID:

  1. 请求接口生成UUID
  2. 请求接口携带业务参数具备唯一性(业务key、序列化请求参数)
  3. 预请求下游服务,获取唯一ID

下游接口具备幂等校验策略:

  1. 请求处理前,将请求的唯一ID写入Reids
  2. 借助业务数据表,数据插入报错
  3. 创建请求处理记录表,指定请求的唯一ID为记录表的唯一索引


2、限流

单机实例(单服务、服务集群)限流+固定阈值:某机器实例(单服务、服务集群)在N秒内可处理M个请求:

  1. 固定窗口
  2. 滑动窗口
  3. 漏桶算法
  4. 令牌桶算法


自适应限流:

  1. 借助消息队列:注意服务过载(消息堆积)
  2. 基于请求排队时间:如微信的过载控制系统Dagor,优先保证业务优先级或用户优先级更高的请求被允许通过,而低业务优先级、低用户优先级的请求被丢弃。排队时间=请求开始被处理时间-请求到达服务时间,设置平均排队时间阈值是20ms,如果请求排队时间超过20ms,则认为服务过载,触发限流
  3. 基于延迟比率:如Netflix的自适应限流组件concurrency-limits,其借鉴TCP拥塞控制的部分思想,其中的gradient算法实现如下,new_limit(真是的限流) = current_limit(当前限流窗口的大小) * (RRT_noload(无负载时最佳请求延迟) ➗ RRT_actual(当前请求采样请求延迟)) + queue_size(允许一定程度的排队,一般为current_limit的平方根)
  4. 其他:bilibili的Kratos微服务中的BBR limiter,其算法实现时使用CPU的负载做启发阈值,
    a. 判断CPU负载是否超过默认值80%
    b. 如果超过阈值,则判定服务当前正在处理的请求数是否大于服务最近最大吞吐量
    ⅰ. 是,则请求丢低
    ⅱ. 否,请求可以被处理
    c. 如果没有超过阈值,则判定上次请求被丢失的时间距现在是否拆过1s
    ⅰ. 超过1s,则请求可以被执行
    ⅱ. 没有超过1s,继续判定服务当前正在处理的请求数是否大于服务最近最大吞吐量。是则请求被丢弃,否则请求可以被处理


3、降级

服务依赖度降低:

  1. 二元:强依赖(出现故障时业务不可接受)和弱依赖(出现故障时业务可暂时接受);
  2. 三元:一级依赖(故障导致服务完全不可用)、二级依赖(故障基本不影响服务的可用性,会有少许可接受的用户投诉)和三级依赖(故障不影响服务的可用性,没有用户投诉)


读请求降低:

  1. 缓存:可以在请求链路上(客户端、接入层网关、HTTP/RPC服务)设置本地缓存,并在配置中心中动态控制是否使用缓存
  2. 兜底数据:使用另一个数据源(离线数据)作为兜底数据;使用静态兜底数据

写请求降低:

  1. 异步写:将写操作转变为用户提交写请求和查询结果两阶段
  2. 聚合写:预处理一定周期内同一条数据的多个操作,最终合并为一个操作,降低写请求压力
  3. 丢弃写:大数据量场景,且业务场景对数据一致性要求不高,可以控制一定比例的写丢失