服务容错概述
本文主要参考Netflix 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 - 即熔断器从打开到半打开的时长
熔断器工作机制
- closed -> open:滑动窗口内request > 100 && ratio > 0.5
- open -> half open:sleep 100ms后,进入半打开状态
- half open -> closed/open:半开后会放一个请求去执行,如果失败则继续open,如果成功则熔断器关闭
船舱隔离
在造船行业,往往使用此类模式对船舱进行隔离,利用舱壁将不同的船舱隔离起来,这样如果一个船舱破了进水,只损失一个船舱,其它船舱可以不受影响。
而借鉴造船行业的经验,在微服务架构中为每个服务单独设置资源,用尽后不影响其他服务:如db隔离、redis隔离、容器隔离
Fallback 回退/降级
当请求失败/超时/被熔断/被限流后,会进入Fallback逻辑:
降级逻辑:返回备用数据,默认数据,本地数据(客户端缓存)等
故障沉默 Fail-Silent:直接返回空值,相应模块不展示(比如商品的相关推荐)
快速失败 Fail-Fast:对于非强依赖的场景则直接报错(不影响体验的前提下)