【微服务】服务容错机制

服务容错概述

本文主要参考Netflix Hystrix机制。


Hystrix全局请求处理逻辑

服务容错机制即防御性编程,Design For Failure,核心思想如下:

  • 单一服务/节点的故障不会严重破坏用户的体验:降级、隔离、超时等
  • 系统具备自动或者半自动恢复的能力:多活重试、熔断器、限流等

超时/重试

在分布式服务调用的场景中,主要解决了当依赖服务出现网络连接或响应延迟时,当前服务不用无限等待的问题。
可以通过设置RPC、Redis、DB等调用的超时时间(一般可以设置为服务响应的99分位),及时释放关键资源,避免耗尽系统资源,导致系统不可用。

超时传递:从网关入口进行传递链路超时,以及请求消耗后的超时;

  • RPC中通过Metadata传递
  • HTTP接口通过header传递,然后通过语言内超时机制保证接口的超时处理;

重试:一般和超时结合使用,主要用于对下游服务有强依赖的场景,通过重试增加数据的可靠性。同步重试次数不宜过多,避免影响接口性能。

限流

限流主要用于下游服务容量有限,在面对流量激增(恶意刷子或者节日大促)时候压力过大导致拒绝服务的场景:通过牺牲一部分人,来保证大部分人的体验(服务整体可用)。

常见的限流分为:

  • 控制并发数:连接池、线程池等
  • 控制流量:漏洞和令牌桶

漏桶:由于恒定速率处理请求,对于突发流量不是很友好。
相当于请求先进入到桶中(等待队列),当桶满了以后开始丢弃请求;
水以恒定的速度从桶中流出(处理)请求。

漏桶

令牌桶

  • 和漏桶一样,基于固定速率放入,但是由于令牌桶是允许任意速度取出的,所以可以容忍瞬间的流量激增:但是总的时间窗口内的令牌数是固定的
  • 假设限制X的QPS,桶的大小为单位时间内允许的最大流量,即这里的X(单位时间这里是秒)
  • 可以分为初始值为空桶或者满的桶两种做法
  • 以1/X的速率向桶中放入令牌,桶满了则无法放入
  • 每个请求拿走1个令牌,所以单位时间内最多拿走桶的大小个令牌,达到限流目的


    令牌桶

分布式限流

关键为限流操作需要为原子化,可以使用redis+lua脚本来保证原子性。

-- 令牌桶限流: 不支持预消费, 初始桶是满的
-- KEYS[1]  string  限流的key

-- ARGV[1]  int     桶最大容量
-- ARGV[2]  int     每次添加令牌数
-- ARGV[3]  int     令牌添加间隔(秒)
-- ARGV[4]  int     当前时间戳

local bucket_capacity = tonumber(ARGV[1])
local add_token = tonumber(ARGV[2])
local add_interval = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

-- 保存上一次更新桶的时间的key
local LAST_TIME_KEY = KEYS[1].."_time";         
-- 获取当前桶中令牌数
local token_cnt = redis.call("get", KEYS[1])    
-- 桶完全恢复需要的最大时长
local reset_time = math.ceil(bucket_capacity / add_token) * add_interval;

if token_cnt then   -- 令牌桶存在
    -- 上一次更新桶的时间
    local last_time = redis.call('get', LAST_TIME_KEY)
    -- 恢复倍数
    local multiple = math.floor((now - last_time) / add_interval)
    -- 恢复令牌数
    local recovery_cnt = multiple * add_token
    -- 确保不超过桶容量
    local token_cnt = math.min(bucket_capacity, token_cnt + recovery_cnt) - 1
    
    if token_cnt < 0 then
        return -1;
    end
    
    -- 重新设置过期时间, 避免key过期
    redis.call('set', KEYS[1], token_cnt, 'EX', reset_time)                     
    redis.call('set', LAST_TIME_KEY, last_time + multiple * add_interval, 'EX', reset_time)
    return token_cnt
    
else    -- 令牌桶不存在
    token_cnt = bucket_capacity - 1
    -- 设置过期时间避免key一直存在
    redis.call('set', KEYS[1], token_cnt, 'EX', reset_time);
    redis.call('set', LAST_TIME_KEY, now, 'EX', reset_time + 1);    
    return token_cnt    
end

伪代码逻辑:

如果令牌桶存在(key A)
- 获取上次桶放入令牌的时间(key B)和当前时间的差距(比如50ms),则放入50*1/X个令牌(加上并且判断不超过容量X)
- 从桶中拿走一个令牌,判断是否被限流
桶不存在:则设置key A = X-1(容量-本次使用),key B=当前时间,EX时间均为时间窗口,即1s

熔断器

在工程实践中,由于一些系统异常或者网络异常导致的调用失败,可能需要一段时间才能恢复。
而这段时间内的请求会占用宝贵的系统资源,并且由于持续失败可能将资源消耗殆尽(比如db、redis连接池),从而导致系统不可用。

所以此时能够立即返回错误而不是等待超时是更好的选择 => 熔断器

关键参数配置

滑动窗口

window=“10s” - 即整个滑动窗口的大小
bucket=10 - 即bucket的数量,用window/bucket可以得到单个bucket的大小为1s;滑动窗口以bucket为单位进行滑动;
ratio=0.5 - 即滑动窗口内统计的总错误率到达50%时触发熔断阈值
request=100 - 即当滑动窗口内的请求数量过小时,暂不触发熔断,最小值100
sleep=100ms - 即熔断器从打开到半打开的时长

熔断器工作机制

熔断器状态流向
  1. closed -> open:滑动窗口内request > 100 && ratio > 0.5
  2. open -> half open:sleep 100ms后,进入半打开状态
  3. half open -> closed/open:半开后会放一个请求去执行,如果失败则继续open,如果成功则熔断器关闭

船舱隔离

在造船行业,往往使用此类模式对船舱进行隔离,利用舱壁将不同的船舱隔离起来,这样如果一个船舱破了进水,只损失一个船舱,其它船舱可以不受影响。
而借鉴造船行业的经验,在微服务架构中为每个服务单独设置资源,用尽后不影响其他服务:如db隔离、redis隔离、容器隔离

Fallback 回退/降级

当请求失败/超时/被熔断/被限流后,会进入Fallback逻辑:
降级逻辑:返回备用数据,默认数据,本地数据(客户端缓存)等
故障沉默 Fail-Silent:直接返回空值,相应模块不展示(比如商品的相关推荐)
快速失败 Fail-Fast:对于非强依赖的场景则直接报错(不影响体验的前提下)

参考

  1. 美团技术团队-服务容错模式
  2. Spring Cloud 源码学习之 Hystrix 熔断器
  3. 令牌桶模式
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容