我们继续讲第三节过载保护
我们前面讲了一个微服务
它必须要保证自己能够存活
一个节点一个微服务
或者是一个进程
它要保证自己能够存活
然后它才能做接下来的像降级重试
或者是别的一些策略
因为如果节点本身已经宕掉的话
你很多策略就做不了
所以我们前面讲超时的时候
是让我们流量尽快地能够被消耗掉
那过载保护实际上是当我们服务处于过载的时候
也就是说它流量过多的时候
一种主动拒绝请求的一种机制
过载保护和限流这两块
都是围绕这个话题去深入的
我们先看一下过载保护
大家也应该知道像有我们知道的一些限流算法
像令牌桶算法
令牌桶算法简单理解就是说
像有一个存放固定容量的这么一个桶
我们叫令牌叫一个token
它存放的是固定的一个容量
那么它是按照固定的速率
按照一定的速率能够往桶里面添加令牌
你比方说我的限速可能是1秒钟10个请求
所以我大概是1秒钟放10个令牌进去
所以大概是100毫秒
100毫秒放一个
如果我平摊到一秒里面
就一秒钟大概放10个令牌进去
这是一种机制
然后我在获取的时候
我去看桶里面有没有令牌
如果有的话我可能这个请求就放行
如果没有的话就会拒绝
当然了我们这个桶的容量
桶是有一个容量大小的
我的放入速度可能是一秒钟10个
但是我的桶的容量可能是20个
意味着我能够什么
令牌桶算法它可以扛一定的峰值
也就是说我有一个时刻
我可以扛1秒钟20个请求
但是接下来因为我
把它现存在桶里面的令牌已经消耗掉了
所以接下来它的速率又会退化成1秒钟10个请求
我的峰值流量可能是20个
但是当我的桶里面的令牌消耗掉的时候
它又会退化成1秒钟10个
所以它是这样的一种机制
我们也可以直接使用像
go官方的一个subpackage
它的一个子包
里面有个叫x/time/rate的一个包
实现了一个令牌桶的算法
像我们看Nginx
像Nginx叫limit_request这个模块
它就有使用类似像令牌桶的做法
它一般会配两个参数
一个就是它的限流的你的速率是多少
一个就是你峰值的流量大概是多少
它其实相对来说比较灵活一些
这种配置的方式
因为你要做过载保护
是当你扛不住的时候
你要主动去拒绝一些请求
但是问题就在于说
我配置的阈值到底是多少
一个服务它能扛100个请求50个请求
它跟物理机的你部署的环境有关系
然后跟你的业务代码有关系
并且你的业务代码会经常什么
经常的变化会迭代
它效率有可能会变糟
也可能会变好
所以这个阈值是很难去设定的
它需要大量的测试值
所以令牌桶算法
它虽然比较简单
就实现起来比较简单
库也非常现成
我觉得它的整个也比较灵活
因为它支持峰值流量
也支持恒定的一个速率去放行请求
但是它最难的地方还是在于阈值的设定
那么除了令牌桶
我们还有一个叫漏桶
类似一个算法我们叫兄弟算法
就是大体的实现是差不多的
然后这边有一个uber开源的一个
用漏桶的一个算法的实现
那么它的核心思想大差不差
你可以理解为有一个固定容量的漏桶
那么这个漏桶它是按照固定的流出的速率
之前是流入的速率是固定的
那么现在是流出的
按照常量的一个速度去流出
流出一些令牌
所以如果桶是空的话
如果桶是空的话
你就没有token可以消费了
没有令牌可以消费了
所以这个时候就要拒绝请求
当然了你可以任意的速率去放入到这个漏桶里面
也就是说你可以快速填充
但是无论你怎么填充
它的流出的速度都是恒定的
所以它是一种用于流量整形
流量控制
它是可以控制它的流出速率
所以它能给保证一个相对比较
恒定的一种叫流动整形
就一条直线一样
恒定的一个流出速率
所以但是这两种算法它都有个本质缺陷是什么
我们本质上是要保证系统不被拖垮的
那么不管漏桶
漏桶还是令牌桶
它的保护思路都是要设定一个指标
当我们超过指标后就阻止或者减少流量的继续进入
那么这个时候系统负载就会降低到某一个水平
然后之后再恢复流量进入
但是这种做法通常是比较被动的
就是它的实际的效果取决于
你的限流的阈值设定是否合理
往往设定这一个值是非常困难的
为什么有几些因素
第一
我们集群的增加
集群的增加或者减少
这个阈值要不要重新设置
因为有可能你是依赖下游的服务
下游的服务有些变化
你下游的Redis扩容了
你的Redis扩容了
你的MySql扩容了
它这个东西会变化
第二就是说设定限流阈值的依据是什么
你是压测出来的
还是你是观测出来的
还是说你随便拍的一个值
另外就是说这个阈值的设定
是不是人力运维的成本过高
它能不能它是经常因为你业务也会经常变化
如果你要去我们叫所谓的观测值或者一个合理的值
它应该是被压测到极限
然后观测出来的一个值
那么你去设定它
人力运维成本包括它的上线都会非常复杂
还有一个就是说当我们调用方发现产生限流了
比方说报错报了429
HTTP状态码429
那么你这个时候如果你人工的去重新设定限流的阈值
有可能业务方的流量高峰等你反应过来的时候啊
流量高峰可能已经错过了
那么你再去设定是否有意义
你是主动设置还是等出问题的时候
再来设置等等一系列的因素
它似乎用起来
本质上还是这个算法本身是没什么问题的
关键是它的缺点就是说
这些指标是很难去界定和设定的
我觉得是非常困难的
它太被动
它整个就是阈值指标的设定是非常被动的
不能快速适应流量的变化
包括我们环境的变化
比方物理机的扩缩融
我们反而是需要一种自适应的限流算法
因为我们把这种机制叫过载保护
英文叫low的吸引
就是它能够根据负载自动去
根据负载自动去丢弃流量
这个机制是相对来说比较完整的
那前面说的它不能够
你是通过设定
提前设定指标阈值来去做到一个限流效果的
有没有一个办法能够根据系统的负载
因为你想啊系统的负载是可以测量的
并且什么情况处于高水平
比方CPU90%处于系统高负载
那么这个东西可被测量
这个时候我意味着要进入到一个节流的模式
就是我要限流
能不能这样的话
其实它是什么
你跟接口已经没关系了
你接口如果改快了或者改慢了
效率变高变低了变差了 都没关系
因为你的负载是很
90%负载是比较好设定的
那么只要负载高我就要节流
关键是这样的一种做法的话
它是整体上我们理解为它是一种自适应的
就根据我的负载变大
我的负载变大了
然后我就节流
我负载恢复了
我的流量可以继续放行
它不需要配太多的一些指标
就配起来可能只需要配一个
什么叫高负载90%非常简单
所以我们后来就想象一下
有没有类似的一个解法
就是类似像服务器临近过载的时候
我们主动抛弃一定的流量来达到一个服务的自保
因为有自保我们才有后面的什么降级
一定要自保
第二情况下就是说
在系统稳定的前提下
保证足够多的吞吐
这什么意思
前面可以理解
就是我出现高负载的时候我要丢流量
但是很多做限流算法
其实它忽略了一个值
就是在我系统能够稳定的前提下
尽可能多的去服务更多的请求
我不是说要一刀切
我应该是在我能接受的范围内
去服务更多的请求
其实它是有两个目标的
一个是自保护
一个是服务更多的请求
否则你的错误率太高了
这里面我就要介绍一个法则叫利特尔法则
那么利特尔法则它的公式比较简单
我们整个利特尔法则它表示的是一种叫容量
比方说我们这个地方有个叫L
就表示的是个容量我们想象一下
我们一个商店
它整个店铺的一个容量L
这个L它会等于多少
它等于我们每一个顾客进入这个店
进入这个门店它的速率是多少
比方说一分钟大概有5个客人
一分钟大概5个客人
所以叫速率
还有就是说每个人在这个店里面
它要逛
它逛多少时间
它是逛5分钟逛10分钟
所以说最终你这个店面的吞吐能力等于多少
是不是等于商店的流入的速率
乘以每个人在商店逛的时间W
所以L=λW
比方说一分钟两个顾客
那么每个人在里面逛4分钟
所以它整个店铺的容量等于8
这样的公式能表示什么意思呢
表示什么意思呢
意味着我们可以通过QPS
就是流入服务的一个速率
那么响应时间late
就是每个请求它需要处理多少时间
那么QPS乘以latent等于
我这个服务的能够接受的
请求的总量
就是我能承载的总量
那么这个值有什么用呢
我们可以想象一下
如果你的服务临近高负载的时候
测量的值
是不是用这个值
作为我的最大的QPS限流的一个阈值或者是指标
是不是就更简单了
我是基于反馈机制的
基于CPU基于内存来做一个信号量
或者做一个反馈机制来进行节流
进入到高负载临近高负载的时候
临近高负载的前几秒钟
利用利特尔法则测量到的
它的服务的最大吞吐
因为它临近负载的当前的一个吞吐
得出来就是我的
需要被限流的阈值的QPS
所以它是一种我觉得是一种比较容易想到的
就是基于负载测量它的最大的吞吐
然后基于这个吞吐来做限流
这个QPS就比你之前
我们去压测或者是
手动设定一个阈值
要高级的多
它会高级的多
具体怎么实现
首先我们就要想
接近峰值时的系统吞吐怎么去测量
第一我们要统计CPU
我要统计CPU利用率
怎么做呢
我们其实就是起一个goroutine
这个goroutine
大概每隔100或者250毫秒
那个值可以设定
比方说像我们当时是250毫秒来触发一次测量
那么每250毫秒它会测量到一个值叫v1
第二次叫v2第三次v3
用测量到的CPU
因为大家知道CPU它变化其实是变化蛮快的
但我也不能简单的用均值来做
因为这样不准确
因为假设有个瞬间的CPU的一个波动
那么你用均值的话可能会被风会滑掉
会滑的比较厉害
我们当时的策略是说
使用简单的一个滑动均值
来去掉瞬间抖动的这种影响
但是又不会影响特别大
看我们这边图
滑动均值其实在天气预报里面
其实也会用的比较多
比方过去12个月的一个天气的一些情况什么的
那么滑动均值我们看这张图
蓝色的这条线
Vt=θt
它的抖动非常厉害
其实跟CPU有点像
我们再看一下橙色这条线
橙色这条线其实就是用
过去的一个样本
Vt=β*Vt+(1-β)*θt
表示的是用过去的一个样本
和当前的一个样本做一个加法
那么这个better其实是0~1之间的一个浮点数
比方说等于0.4
等于0.4意味着过去的样本它的占比权重是0.4
当前的样本占比是0.6
你这样的话当前样本肯定是体现了
体现一个重要性更高一点
所以你当前把权重大一点0.6
过去可能0.4
因为你随着时间漂移
肯定过去的样本肯定它的时效性就越差
所以你要有一定的衰减
所以这个就是很简单的一个
简单的一个滑动均值的做法
所以你看我们橙色的这条曲线
它拟合的就相对来说比较准确 对
当然还有一些别的一些算法
但其实思路都是
思路基本上都是类似的
思路都是类似的
所以它拟合的时候这个曲线会更好看一些
所以我们当时是用类似的做法来统计CPU的使用率
拿到这个值以后
我是不是有办法做一个阈值
大于90%然后去测量
测量什么
测量我当前的吞吐
当前的吞吐
那么我们再看一下第二个指标叫Inflight
Inflight表示我当前这个服务
正在进行的请求的数量
也就是我当前的服务的QPS是多少
这个好统计吗
其实很好统计
你在你的gRPC或者net/http
这个包里面来一个请求加1
消耗掉一个请求减1
意味着我当前serving
我当前serving的request总数是多少
你就来一个加一个
消耗掉一个减一个
那不就是我当前服务正在处理多少个请求
它不就是它的当前的一个并发
或者当前它的一个Inflight
正在处理的请求数量
这是第二个那么第三个是什么呢
我要得到临近负载时的一个吞吐
我就要想办法拿到过去的QPS和延迟
这个其实也比较简单
我们会一直不断地用滑动的窗口
用滑动的窗口去采集最近5秒
最近5秒内
以每100毫秒为一个采样窗口
每100毫秒为一个采样窗口
然后你滑动统计你滑动统计
然后我会去用过去5秒钟某一个
就是100毫米一个窗口
然后我会去拿这100毫秒为窗口的采样周期内
它的全局的一个QPS大概是多少
然后它的雷克斯平均响应时间是多少
那这样的话
我是不是很方便去计算出临近负载时
就接近前5秒最大的一个QPS
和我的平均的一个延迟
这样的话我就可以算出什么
我的服务的接近极限时的一个吞吐
这个就是我们测量值
那么滑动的
这种滑动统计其实非常常见
我后面在讲熔断的时候也会讲到滑动统计
后面我们再来简单介绍一下
它怎么去计算的
所以后来我们算法就变成
首先我们计算出来CPU
那么它用滑动均值统计出来的
在我们算法实现里面是CPU大于800
就是CPU大于80%
作为一个启发的阈值
也就是说超过80%
我认为就要开始限流
我就要开始限流
那么我们现在的算法做的比较简单就是
pass表示成功的请求数量乘以rt letters
这个就是表示我们的临近负载时的吞吐L
如果我当前的Inflight比它大
表示我过载
就是我能处理的请求就是超过了我的极限
我就要什么
丢弃
如果比我小
我就放行
我测量比方测量L等于100
我当前QPS等于90
我当然可以放行了
如果测量出来我的当前的并发度110
或者当前正在处理的请求110
那我就丢掉10个
所以它这个做法比较比较简单
那么这个算法我们做完之后
在kratos1.x的版本里面
大家可以找到这个算法
在ratelimit库里面可以找得到
待会我们也可以去看一下
在我们1.x版本里面应该是在
在pkg目录
然后在ratelimit里面有个叫bbr的目录
里面就会介绍这个算法的一个实现
大家也可以去切过去看一下
包括像CPU
CPU怎么统计的
然后滑动窗口怎么计算的
都会在里面
这个算法我们当时上完之后我们看一下
右边这张图我们先看一下
最右边这张图
那么蓝色这条线
蓝色这条线表示的是我放行的请求的数量
那么橙色的这条线表示的是延迟
表示的是延迟
那么绿色大家注意看里面有一条绿色的线
表示的是我成功放行的请求数量
大家可以看到从效果上来看
它的延迟抖动非常厉害
然后这条绿色的这条线它抖动也非常厉害
为什么会出现这种情况
你可以想象一下
为什么会出现这种情况
因为随着你的请求数量加大
那么一旦你触发80%
CPU触发超过80%
那么你测量出的极限的吞吐
你立马一丢弃流量
是不是意味着CPU
大于80%的条件容易被破坏
因为你丢掉一些请求可能CP立马下降
那么下降瞬间又放了好多流量进去
它又会触发然后又丢掉很多流量
所以它会来回的fliping
就是所谓的跳动它会来回的跳
所以当时的效果不是特别好
它抖动特别厉害
所以我们把这个算法做了一些小的调整
也就是说我一旦触发这个阈值以后
比方说大于80%处于触发这个阈值以后
我们立马可以干嘛
我们可以让它持续一段时间
比方说持续5秒的时间内都用这个策略
我们上了这个策略以后
那么5秒以后怎么办
5秒以后再看它是不是CPU大于80%
如果大于80%
继续5秒处于这个判定条件
就它不会来回的跳
我会让它持续一段时间
使用冷却时间这个做法以后
我们再看一下蓝色的是放行的
蓝色的是放行的请求
绿色是成功的请求
橙色是雷特斯
所以你看到我们这个算法一旦介入以后
我们的成功放行的请求数量基本上是恒定的
成功的数量是恒定的
延迟也可以可控
也就是说随着我的流量继续加压
我的成功请求数量不变
然后我的雷克斯不变
但大家知道吗
我们经常自己研发的一些服务一些节点
你去压测它的时候
大家其实压的时候有一点没有掌握到
当你的压测的时候它报错时候
我们的压测系统一般都会熔断
它立马就会停止压测
或者说我们同学会吼
啊服务扛不住了
马上停止已经开始大量报错了
但是实际上真正出故障的时候
没有人会给你停止的
用户只会不断的重试来
让它的功能能够使用
这个时候其实你的服务就容易被雪崩
就容易被打挂
所以说压测的时候压到它有报错的时候
你可以继续加压
我们要极限压测
你继续放更大的流量
放更多的流量进去
看这个服务是什么表现
我们知道这种情况下
大部分服务就死透了
就死透了它彻底就活不过来了
它就死透了
做的过载保护做的好的服务
你持续给它加压
它往往放行成功的请求数量是恒定不变的
延迟也基本可控为什么
因为有一部分钱被它丢弃了
它能保持请求是相对比较稳定的
这个是它最终过载保护的一个
我认为比较好的一种机制
就是在你流量扛不住的时候
因为你CPU比较高了
那么主动的丢弃掉一些流量
把负载给卸掉
我们叫卸载流量或者叫卸载
卸载一些负载
那么让我的服务能够处于一个稳定的状态
这个效果其实是非常不错的
但这里面还有一个点会比较麻烦
假设你现在你的极限是100个请求
我加压到200
你丢100个请求
我加压到1000
你要丢900个请求
那么这种情况下
实际上过载保护也扛不住为什么
因为拒绝请求仍然需要成本
你要回复HTTP请求
回复一个错误码429
流量不够了
或者是out of
out of quarter之类的
就是你扛不住了你要主动报错
但是主动报错或你答复这个请求
它也需要一点点的成本
它也需要一点点成本
所以当你的流量继续变大的时候
它终究是会被彻底打挂的
所以说过载保护它能解决可能大概90%
大概90%的问题
那么剩下这10%的靠谁解决
靠限流
这也是我们接下来要去讲
限流的这一块的一些知识
所以我们前面看到啊
超时是让我们的请求尽快消化掉
并且通过超时传递能够控制全局请求的
控制一个请求的全局的生命周期
那么过载保护是尽可能让自己临近过载的时候
主动卸载一定的流量
剩下的这10%是靠谁
靠限流
因为这种你是需要现有的手段
让你的服务能够恢复的
它才能够我们后面包括降级重试的这些策略
才能够去生效
所以前面这三块是非常重要的
我们来看一下啊
限流指的是一段时间内
因为我们刚刚讲过载保护
它是一种特化的限流
属于临近过载的时候限流
临近过载的时候主动丢弃流量
它也本身本质上也能叫它是限流
我把它当做拆开来讲了
我们再来看一下限流
限流其实就是一段时间内
定义某个客户或者应用
能够接受或者处理多少个请求
它是定义出来的它的另一个指标
我们过载保护是自适应
它是自动算出来的
那么通过限流
你可以过滤掉产生流量峰值的客户和微服务
这什么意思
因为有些流量
你是个多租户的场景
你的微服务
好多人可能会来调用你
所以它是一种多租户的场景
所以有些峰值的流量来自某一些用户
或者某一些微服务
你可以针对这些微服务进行一些限流
来确保你的应用
在我们的叫Auto Scaling的机制介入前不会挂掉
因为我们可以想象一下
假设我们这个时候超时介入了
主动丢弃流量
过载保护
丢弃流量
那么限流还是要兜最后的那10%的一些底
那么兜住这些底之后
我们通常会干嘛会降级
那么其实还有一个就是
扩节点叫Auto Scaling
如果你没有out scan
你没有自动扩缩容
那么有可能是需要什么
需要你手动去扩建
因为我们出现故障的时候
大家第一反应是什么
赶紧看线上看一下监控看看指标
如果它是服务压力比较大
我们一般会立马去扩容
这种人工介入扩容也是一种扩容
那么像我们的呃 k版空方
Daisy或者是现在很多呃 容器
编排框架可能都有
所谓的这种定义一些信号量
比方说超过多少个节点75线的节点
然后CPU超过多少了
然后我们就自动的Auto Scaling的及时介入
把它从10个节点扩成20个
你这样的话其实尽可能去扛住这些压力
这是一种
除了因为你要让这套机制生效
它需要一定的响应的时间
如果你的前面的超时过载保护
没有兜住底你可能直接就就全部挂掉了
你还没来还没来得及扩容
所以说它限流加过载保护加超时这几个机制一结合
说白了就是要空出时间
给我们人工介入
或者说给我们
给我们的机器介入来去做后续的一些策略
包括像扩缩容重启等等一系列的策略
包括降级
我们刚才讲前面讲的
我们再看一下这几个点 令牌桶
漏桶它都是针对单个节点
无法分布式限流
无法分布式限流也就是说有一种局面
我现在发现达到我整个集群的流量
可能怎么讲
我需要控制住整个我入口的流量
因为假设我是一个bff
我的一个bff进来到我下游可能会放大
比方账号可能会放大两倍三倍
因为你很多场景里面
很多接口可能读账号要读好几次
那么它会产生放大
那么这个时候需要扎
我现在需要闸口
扎BFF层的口去控制它的流量
然后让我下游的服务能够稳定
或者能够让我的那些流量能够处理干净
我就要闸口BFF
那么这个时候
单节点单机版的令牌桶漏桶
它都是单节点的
它无法去闸口全线全局我所有节点的一个流量
比如说有10个节点50个节点
你当然可以说每人配20个
假设我有50个节点每人配20个
我可以每人配20个 我可以这样去配置啊
然后限制的QPS是1000
但是大家知道每个节点之间接收到的流量
有轻微的差异
如果你配20
有些人得到的请求是25
有些人可能得到的是15
它就会得到25的那些人就会报错
你可能会说我可以配大一点
那么你配大一点实际上
你放下的流量就会变多
所以它也达不到效果
所以很不好配单节点
有没有一个办法做分布式的限流
限流无论你有多少个节点
我都是控1000
因为你单机的现在还有一个问题
我这时候扩缩容扩了10个节点
我是不是还要去调这个阈值
每人把总结的总数加起来
把1000除以它再重新去分配
其实很麻烦的这是所以这是第一个问题
那么第二个问题其实是更难解决的是QPS限流
我们很多做法现在目前都是基于QPS来做的
那么QPS限流它有几个问题
不同的请求可能需要数量迥异的资源来处理
你举个例子我们有时候取账号
取一个账号
消耗的CPU和取100个账号
消耗CPU是不一样的
你取100个账号有时候我
我邮件列表里面有100个用户
我点开一封邮件只查一个用户
所以每个请求它成本是不同的
第二就是说某种静态QPS这种限流的阈值
很难去界定你就很难去压测出来的
这个值是很不好去设定
很不好去测量
所以这是QPS现有的一些问题
那么第三个点是什么
叫给每个用户设置限制
这个用户其实是指租户
它不是指我们的C端的用户是指的租户
因为全局过载发生的时候
有可能因为你的账号服务全局
假设你的账号服务整体都过载了
但是你发现突然是某一个
写了有bug的人在那死循环
或者是它写的一个batch的接口
批量请求你的接口它传了个10000
可能是它有问题
导致你账号集群的全局过载
因为你的账号进去是很多个租户才使用
如果那一个人虽然你有自保护
你有自保护或者说你有限流
但是这种情况下
你如果不能针对那一个人进行管控的话
你的影响面仍然是巨大的
你的影响面是巨大的
所以说我们要针对不同的租户要设定限制
然后针对不同的租户限制
其实我说白了还是会超卖
比方说我的总请求是1000
你最终多个租户配出来
每人它200我500
它300可能会超一些
超卖一些其实也好理解
因为说是不是接口之间它还有一些优先级
有些接口重要有些接口不重要像谷歌搜索
谷歌搜索肯定是重要的
谷歌搜索推荐你输一个字
输一个字它上面推荐
可能相对来说是优先级可以低一点
是不是可以优先把它丢弃掉
这也是一个
还有我们刚才说的拒绝请求
仍然需要一定的成本
就是你返回429http状态码是429
叫limit叫什么一个c的
流量一个c的就是超过流量的状态码
它也仍然需要成本
所以基于这些原因啊
我们就会介绍两套机制
一个是分布式限流 一个是熔断
这两个策略结合我们前面讲的超时自适应保护
它最终可以给你的服务带来无比强大的稳定性
分布式限流怎么做我们就很容易想
因为我们其实去搜分布式限流的话
总能搜到一些文章
我们先看看我们正常的一个思路的演进
是怎么样去做的
首先我们假设我们自己去做个限流模块
其实大家最容易想到什么
是不是使用Redis来做限流
使用redis来限流
那么redis这些是请求
那么请求到我们的一个计数的控制模块
这个控制模块会去使用Redis
逐条上报汇总到Reddy iss
它可能使用Regis的incr指令
来一个加一个
然后会根据Incr返回的值
会发现它超过了
就会直接去报错
把流量拒绝掉因为用Redis来做
其实很多分布式限流都是基于Redis来做的
基于Redis的这种做法会有些什么问题呢
单个大流量的接口
就是你单个接口流量超大
因为你一个接口一定是命中到
一个Redis的节点去的
那么你很容易让Redis本身产生一个流量热点
因为你来一个请求你要去Redis做一下计数
你来一个要做一下计数
假设你有10万个那个QPS
是不是意味着我Redis有10万的一个请求量
而且是某一个接口的
肯定是不是特别好的这是第一个
第二个pre-request的这种模式
就来一个请求去更新一下计数这种模式
对性能其实是有一定影响的
这个好理解吧因为你来一个请求
你最终对Redis都放大了一次
每次都要操作Redis
它是一个很高频的网络玩法
所以它会对我们的接口性能产生一定的影响
我们就在想
很多我们搜索网上的文章都是基于release来做的
有没有比较好的一个做法
一条条的去上报指标
使用incr来做统计
感觉不是很好
那是不是可以一次从单个quota升级为
我一次要一批quota要到我本地
一次本来相当于你incr是调一个
我是不是可以一次调incr100个
我本地用完之后我再来去要啊
quota表示的是quota其实可以理解为是速率
可以理解为速率
我们从刚刚一开始每次一条条的去拿
我是不是可以一次拿一批
其实这个也比较容易想到啊
这样的一个做法又有个问题
又有个问题
你肯定是定时去批量去获取quota的
可能基于所谓的一个像心跳一样的定时去获取
那么它是定时的异步的
就不在你的主干逻辑里面
你可以想象一下你的代码不在你的主干逻辑里面
是你后台起的goroutine之类的
它异步的定时的去批量获取quota
那么是不是通过这样的一个机制可以
大大减少请求Redis的频次啊
那么你拉到quota
并不是说比方说我配的限流是1000
我不是说拿10个就掉10次
我可以说拿的是一个份额
拿的是一个速率
也就是说我的QPS总共是1000
一秒钟1000个
我拿的是一个速率
假设我拿的是10秒一秒钟10次请求
假设一秒钟拿100个人
一秒钟拿100个也就是说我拿到的是个速率
不是说我拿到100个
我还要去请求一次只是说
我定时去同步就好了
我这下拿到的是个速率
拿到这个速率以后
我再在本地用令牌桶算法
在本地用拿到的100
用100作为令牌桶算法去构建一个限流的拦截器
用100作为一个拦截器
我就可以每秒钟大概限制住100个请求
它是这样的一个思路
你就拿100
但是这里面就会遇到另外一个问题第二个问题
你每次到底是拿100还是拿50还是拿20
你需要去手动地去设定这种指标
其实是比较繁琐
比较繁琐的
肯定是这肯定是不太好的
因为非常相对来说比较繁琐
你不好去配
你配大了会产生饥饿
你比方说你的限制的是一秒钟1000个请求
你一次配个500
你拿掉之后别人就没法用了
别人就没法用了所以它不合理
这个值很不好设定
它不是特别合理
那怎么做
我们当时的想法是说你可以我们的
代码初始化的时候给一个固定的值
比方说20或者50
当然可以更小因为我们设置的是20
随着我的时间的流逝
我是不是可以基于滑动窗口统计
我过去近期的一个QPS的情况
用近期的QPS的情况
作为我的下次需要去批量获取quota的一个值
也就是说我假设我统计出来我的过去的是50qps
是不是意味着我可能就要拿50QPS就够了
用过去来预测当下的quota
我觉得是比较容易去想到的
那么这样这样解决这个问题以后
还遇到一个什么问题
不公平
去获取过它不公平
就是我们刚才讲的有一个人去获取500
有一个节点获取了500
剩下的人获取不到
有没有可能你1000或者500
剩下只剩500个了
那么这500个可能我还有9个人要去拿
每个人拿到的份额就少得可怜
它不公平
怎么做呢
那么怎么做实际上我们把这个问题总结一下
你可以理解为是我们经常面临给一组用户
一组用户就是一个个的节点
给一组用户划分稀有资源的问题
我们给一批节点去获取集中式的获取份额
这个份额就是我的一千这个份额
那么我们在设计算法的时候
应该是让它们都享有等价的权利来获取资源
它们应该是公平的
但是其中一些用户实际上
只需要比其它用户少的资源
注意我们有些情况是不是比别人要的多
有些情况比别人要的少要的少
是不是它多出来的那些份额可以给别人瓜分掉
那么基于这种思路我们找到一个算法
如何能够比较好的公平的来分配这些资源
一个在我们实际中被广泛使用的一种分享技术
成为叫Max-Min Fairness
就是最大最小公平分享
叫Max-Min Fairness
我们看看它就是这个算法
这个算法从直观上可以公平分享分配给每个用户
想要的可以满足的最小的一个份额
那么没有使用的资源、
会均匀的分配给需要大资源的用户
简单来说你要多少尽可能满足
那么你要的比大家吃大锅饭公平的
这种情况下还要少的情况下
你多出来那些我就瓜分给其它人
这个算法是怎么实现的
第一资源按照需求递增的顺序进行分配
比方说我这个地方递增是什么呢ABCD
A需要两份
B需要2.6
C需要4份
d需要5份
我按照这样的一个排序
我优先给要的最少的用户
优先给满足要的最少的用户
假设我这个地方一共有10个份额
一共有10个份额
那么一共有4个租户
是不是10÷4=2.5啊
也就是意味着如果我们按按照公平的做法
每个人只能拿到2.5分
那么对于A用户它只要两份我优先给它分配
它只要两份
那么A用户者两份的情况下
是不是它给了2.5肯定是多了的
是不是我们可以把它多出来的2.5-2÷3
再加上2.5那么剩下的人
是不是多出来就是2.666
因为你只要两份
那么这个2是怎么来的呢
这个2怎么来的
我们初次是设定的阈值
接下来是用过去的QPS预测出来的
我们继续
到分配B的时候
B也只要2.6
你给它2.666多了
所以把2.666-2.6÷2
所以给C是2.7
给D是2.7
你看虽然D要了5个份额
因为它要5份的话
别人就产生饥饿了
所以说最终它得到实际的是2.7
那么这种是Max-Min Fairness的意思
就是尽可能公平的情况下
来满足多个用户
多个用户多个节点
为什么会这样呢
因为你的节点应该是等质的
你比方你布10个节点布20个节点
它们应该是等质的不会有太大差异
所以它的QPS应该是比较均匀的去分配
尽可能均匀的分配你不能说一个人要100
别的人要不到
所以说它是尽可能均匀的分配
所以分布式限流我们最常用的地方是哪里
最常用的地方就是在BFF层去做闸口
BFF层去做全区的流量控制
当然还有一些场景就是说
我们像账号这种底层的服务
它要做多租户场景的情况下
像我们内部一般怎么做比方说我有账号系统
别人来对接我
别人在对接我的时候
你账号你的账号资源不可能是无限的
万一它调错了调多了怎么办
所以它来申请调我账号的时候
首先会申请一个所谓的权限
我会准许它调
就在我这边ACL会配置服务可以调过来
第二个我其实会配置这个人能调我的账号服务
我大概会给它多少个配额
比方说峰值的时候它跟我说峰值500
那么可能就给它配上500个
一秒钟500个请求
那么这个配置是在账号服务里面配的
所以说当超过我们的时候
账号服务会主动拒绝掉某一个租户
那么有了这个策略以后
你可以针对你账号服务给不同的租户
配定不同的阈值
比方说A服务要五百个请求
B服务只要200个
那么有些极端情况下
有一些人的流量变得特别的夸张
我就可以管控它我让它变的只有一个请求
假设它出bug了我就可以管控它
让它不要影响全局的其它的人
甚至变成0
让它完全不能调用
这也是可以做得到的
那么分布式限流跟我们了解到的像单机限流
那我们看一下就是它们之间的一个变化
就是它们的一个对比
首先是单机限流
那么单机限流它实现简单稳定可靠性能高
因为这是纯本地实现
那么它缺点是什么呢
流量不均匀引发误限制
比方说我们使用令牌桶漏桶你去配的话
节点会变化流量也会变化你去
去手动设定阈值是很麻烦
那么另外就是机器数变化的时候
你要去控全局流量的时候
手动调整容易出错
好处就是说现有实现非常多
动态流控的话动态流控我们也叫自适应请求
它是根据服务的负载情况
动态限流不需要调整额度
那么它缺点是什么呢
需要主动搜集请求的性能数据
像CPU啊load啊成功率啊耗时啊
它有一些有一小丢丢的收集成本
第二就是客户端主动善意的限流什么意思
你虽然会主动拒绝流量
但如果人家无脑的还是继续发送流量过来
你还是会扛不住的
有一天你还是扛不住的
所以客户端要主动善意的限流
后面我们也会讲像移动端或者是对方租户
你的消费方案它应该怎么去做
它应该采用什么样的策略
第三个是什么呢
一般我们是限于接口级别的调用
它支持范围比较小
我们一般是服务做保护的是某一个接口
到全局限流就是我们讲的分布式限流
它的优点是什么
第一
流量不均匀不会触发误限流
因为它是基于过去的一个
QPS去预测你要多少quota
如果你因为是意外的原因
你的节点的确流量比别人多那么一点
它也不会出太大的问题
因为你预测的是准确的话
那么你需要的quota也会多一些
你会比别人多一些
尽可能满足你
第二
机器数变动是无需调整
因为你的全局集群就是不管那个人账号
比方它要来调我账号
它要一秒钟调500次
不管我自己扩缩容多少
或者它扩缩容多少节点
我都不需要去调这个值
第三应用场景很丰富
我们其实内部用分布式限流
这套工程在我们内部其实也用于一些其它的场景
比方说我们甚至用它来做
在Client侧来做MySQL的限流
因为MySQL其实在proxy层也可以做
但现在因为我们proxy研发的进度比较缓慢
或者说我们的中间件还不够完善
所以我们有一些场景是靠
分布式限流来配合DB来做的
包括一些别的资源
所以它场景是非常丰富的
缺点是什么
它实现是比较复杂
就这个中间件比较复杂
目前我们也还没有开源出来
接下来可能我们也是有计划
逐渐是把这些东西放出来的
第二就是说它需要手动配置
因为你要去知道这个业务方
它峰值的时候大概调研的是多少
所以它需要一定的手动的配置
这是几个对比
我们再来了解一下重要接口
因为接口之间它有重要性
每个接口都去配置阈值啊
如果我们按分布式限流这种配法
我们去不是配服务
我们会按接口去配
运营工作极其的繁重
很啰嗦很麻烦
那么最简单的我们又是配置服务级别的quota
那么服务级别它又看起来好像又太粗了
接口又太细了
有没有办法去
根据不同的重要性来设定quota
所以说我们最终又给我们的接口设置了优先级
我们也叫重要性也叫重要性
我们定义了4个种类
第一种叫CRITICAL_PLUS
就是最重要的为最终的这些请求就是
如果拒绝这些请求
会导致非常严重的用户可见的一些问题
就是它的优先级最高
第二个叫重要
生产任务发出的默认请求
我如果什么都不设定的话
默认就是这个等级
那么拒绝这些请求会造成用户可见的问题
但是可能没那么严重
第三类是叫可丢弃的
这些流量容忍某种程度的不可用
这是批量任务发出的
因为我们有时候会做一些定时任务
我们定时任务也会去请求RPC
那么这种它属于优先级比较低
那么这种情况如果被拒绝掉或者失败的话
通常可以过几分钟或者过几小时后
再来重试也是可接受的
那么最后一类叫可丢弃的
这些流量可能经常会遇到部分不可用的情况
偶尔完全不可用也是可接受的
所以你去按接口去配quota很难配
你只按服务似乎也太粗了
因为你有些接口重要性不那么高
那么结合这几个特性
这几个特性其实我们也可以想象一下
你是可能是在BFF
你因为你业务场景是最容易定这些接口优先级的
我们通常是在BFF层来定
但是可能有一些账号服务这种
可能底层也会去定义啊
它一些公共多租户的这种账号服务可能也会定义
那么结合这两个机制
就几个gRPC微服务之间
需要自动的去传递这个重要性
大家知道为什么吗
因为你BFF层认为这个接口很重要
那么它在调下游资源的时候
也会把它染色或者说污染成重要性
同样的一个重要性
我们叫传递重要性
比方说如果后端接受某个请求A
那么在处理过程中发出的请求到B和C
发出的请求B和C到其它的后端
那么请求B和C会使用和A相同的重要性属性
也就是A的重要性会传递到B和C
它必须要有一个继承的关系
那么利用重要性我们会做的几件事情
第一当我们全局配额不足的时候
比方说我一共可以调500个
我全局配额不足的时候优先拒绝低优先级的
优先拒绝低优先级的
也就是说我的负载均衡算法里面其实是
可以做到一个所谓的优先级的算法
让低优先级的被拒绝掉
第二全局配额可以按照重要性去分别设置
以前我去设定服务级别太粗了
我可以按照不同重要性来设定
也是可以支持的
第三过载保护的时候
就是我们前面讲的自适应限流过载保护
低优先级的请求可以优先被拒绝
所以我可以设定档位70%
75%啊或者80%
我认为先要丢弃低优先级的
90%丢弃高优先级的
你可以不是无脑的都丢弃掉
它是可以根据重要性来做丢弃规则的
这也是一个点
这也是非常有效果的
除了重要性分布式限流的话
我们接下来要讲熔断
要讲熔断因为我们看一下这个缺点
我们我们分布式限流里面
我们讲分布式总结优缺点里面有提到一个点
我们做流控
我们做自适应保护也好
或者在服务端做分布式限流也好
如果对方无脑的打持续的很高的流量给你
你仍然会扛不住
所以这个时候需要客户端主动的善意的限流
我们也叫client side throttling
需要在客户端侧叫client side叫客户端侧
来做throttling就是来做节流
那么在客户端侧做节流
也叫熔断
也叫熔断因为有些喜欢叫熔断
其实它本质上是叫client side throttling
就是在客户端做节流的
那么熔断器可以理解为什么呢
像家里的电路
你家里电器过载的时候
是不是就跳闸
你家电器就全部断电了
那不就起到一个保护
如果你持续的高电流的去服务的话
有可能保险丝啊有可能我们的电线会烧掉
可能会着火
所以为了限制操作的持续时间
我们一般是用超时
那么超时可以防止挂起操作
并保证系统可以响应就是fail fast
我们处于高度动态的环境中
几乎不可能确定在哪种情况下
都能正常工作
并且有准确的时间的一个timeline的限制
那么熔断器包括我们刚刚说的限流
限流也无法去解决所有的场景的问题
所以熔断器的引入
它是以我们电器的电子元器件的这种方式来命名的
那么它在分布式系统中
其实也是很重要的一个点
就是在客户端侧去做一个节流
那么熔断最典型的场景是什么呢
当我们服务依赖的资源出现大量错误
我依赖在下游
我的A服务一旦在下游出现了大量的错误
因为大量的错误的时候
你还是去调它
是不是有可能是雪上加霜啊
本来人家能够可能过一会就恢复了
啊结果你还是正常流量发给它
它可能就活不过来了
所以说需要对下游保护
我们讲自适应限流的时候是对自己保护
我们讲熔断的时候实际上是对下游进行保护
因为它是从client side
它是从客户端测去解决的
所以它是对下游进行保护对server进行保护
第二如果某个用户超过了资源配额
类似像我们刚刚讲的你的自适应限流
你的分布式限流你的租户超过了限额
它仍然发超大流量的请求进来
那么后端任务虽然会快速拒绝请求
返回配额不足的一个错误
但是拒绝回复仍然会消耗一定资源
有可能后端我们的服务忙着
不停地发送拒绝请求导致过载
这也是可能常见的 所以它是两个场景
我们再看一下熔断器的一个实现原理
大家可以去看一下Highstreet的源码
有个Java的Highstreet库
它大致的原理
就是相当于熔断器一开始处于一个close的状态
叫熔断器关闭的一个状态
就是就当前是正常的
那么我持续不断的会去统计一些指标
比方像RPC的错误率这些指标
那么当我的服务达到错误率达到一定的情况下
或者说我的保险丝温度超过一定的程度下
我是不是可以什么
让我的熔断器自动打开
自动打开相当于跳闸
打开以后
它就是进入到一个叫fast fail
就fast fail快速失败的fail fast的这种
这种跟超时一样吗fail fast
因为你因为你熔断器打开
因为我本地的请求
我客户端本地发的RPC请求
发都发不出去的
直接在我本地就立马拒绝掉
超时是服务端拒绝掉
超时是你本地客户端拒绝掉
或者是服务端拒绝掉
熔断是直接客户端拒绝掉
不会让你有请求发到服务端
所以它对下游产生一定的保护
那么这个图里面我们看到啊打开熔断以后
其实就是就像电路一样整个跳闸了
然后所以大部分请求都失败
这时候你一般怎么做
你会把你家里的电线拔掉 对
然后把你的电闸重新推上去
是不是有点像我再放一个请求进去啊我去探一探
我去探探路
我去探一下看看我的电路是否恢复了
如果恢复的话
重新把你的什么电视机电冰箱全部打开
表示可以正常工作
所以熔断的整个算法实现其实非常类似
它的做法一般来说默认情况下都是正常的
那么它会有一个滑动的窗口
它会不断持续不断的统计
请求数量成功的请求数量总请求数量
那么你可以让它的阈值是比方QPS大于100
且错误率大于50%
错误率大于50%就会进入熔断
为什么要大于100
因为如果你只有两个请求
错一个就是50%
你单纯用错误率去做那个阈值的话不是特别准
所以说熔断它有个限制
就是说QPS太低的话可能不准
或者说它完全没法熔断
因为你两个请求
错一个50%它无法熔断
那么你进到open状态以后
打开熔断以后
其实就本地直接就报错返回了
那么什么情况下可以放一个请求
它们这个时候打开熔断以后
它有一个变量叫
它是个变量
那么这个变量有点像一个int64
那么你可以用Atomic CAS指令
用当前时间戳对它进行替换
因为只要当前时间戳减去lastTimeStamp
它大于一个阈值
比方说大于200毫秒
大于200毫秒那么我用CAS指令替换以后
是不是CAS指令只有一个人能够成功
那么那一个成功的人它就可以去调
真正的发一个gRPC请求发到下游
如果请求成功
它会把整个计数器全部重置回到一个健康的状态
所以这是它的一个核心原理
那么滑动窗口其实大家也可以去看一下源码
它比较简单
你可以理解为有一个一秒的格子
把1秒的格子划成10份
每一秒每一个格子相当于100毫秒
那么假设当前是第0毫秒
我就统计到第一个d0
我请求了一次
然后它成功了
是不是我只要在格子里面做一个计数
在第0个格子加一个请求成功请求加1
那么假设第300毫秒
我是不是就记第二个格子应该是第二个格子
我去第二个格子去里面做一个计数成功请求加1
那么超过一秒以后
它其实假设是1.1秒
1.1秒它是不是就回到第0个格子了
它相当于是个环形的数组一样
不断重复去使用
为什么这样做呢
这样的话你的精度相当于是以一个格子为精度
那么你1.1秒的时候
实际上第0个格子就过期了
所以它是一个这样统计出来的数据是
相对比较准确的就是比较细粒度
就力度比较细一点
你否则一秒钟一刀切一下肯定也不行
所以这个是个滑动统计的一个实现比较简单
就可以看一下
那么这里面我们讲完熔断原理其实还不够
我们当时还参考了它熔断有个什么问题
它一失败全部请求全部失败
然后放一个请求又全部恢复
特别容易产生一个锯齿
就是一下全没了流量
放一个请求又全部恢复
它却它就是一个锯齿一样
一下好一下坏
所以它失败率从结果来看它失败率特别高
后来我们参考了一下
Google SRE提到的一个公式
我们看一下这个公式大概是这样的
Request表示的是这个变量表示的是总请求数
accepts表示的是什么
是成功请求数量
那么这个公式什么意思
K是一个配置的值
那么这个值越大
意味着我的算法越不激进
越小的话算法越接近
我们先看一个举个demo
假设我这个k=2
意味着request减去两倍的
两倍的成功请求数量减去它
除以request是什么意思呢
我们可以想象一下
如果我成功请求的数量就是我的服务失败率很高
基本上不怎么增加了
就一直在大量报错
那么request减去这个k乘以accepts
是不是意味着这个值不怎么增加
然后这个值无限趋近于无限大
那么这个除法除以request+1
是不是它去求极限等于1
因为你这个值不怎么变化这个值趋近于无穷大
趋近无穷大是不是等于1
所以我们乍一看这个公式 得出来是什么
得出的是一个浮点数0~1的浮点数
那么最终我们用这个浮点数做一个概率
我求一个随机值比它大就放行比它小就拒绝
那么再看一下一种情况
如果我的这块我的accepts也在增加
request也在增加
那么我减出来再除以request+1
我得出的这个概率是不是高一些
说明我这个放行的请求会变得多一点
因为我的accepts我减完之后
我这个值不是特别大
我这个减完之后不是特别大
所以它的意味着它是个丢弃概率
就是丢弃的不需要那么多
所以最终这个算法得出的效果就是说
随着请求数量增加
我的绿色请求也会增加
绿色是成功发行的请求
因为它最终得出的是个丢弃的概率
丢弃概率它会随着你的流量变化会变好
但是尽可能保护我们的服务错误率一定的情况下
多放行一些请求出去
它所以最终从结果来看
它的失败率是要小一些的
就是它放行的请求成功会更多
但是熔断效果也还不错
所以我们后来就用了这个公式来参考
那么我们只需要配置这个K值
这个是我们的代码
大家可以自己看一下
也不是很难
所以它效果还是很不错的
那么除了熔断
我们还有一个小技巧叫Gutter
Gutter这是什么意思呢
这什么意思
我们当时遇到个场景
我们可以讲一下当时遇到什么场景
我们当时是我们是做数据传输的
我现在在做大数据做数据传输
那么它数据传输数据上报以后
一般会先经过我们的Kafka
会经过我们Kafka
我们Kafka然后会传输到HDFS
或者是被我们的Flink这种实时计算引擎去计算
那么我们搭了一套大的Kafka集群
但是我们发现如果我的这一套卡夫卡集群出了故障
我的影响面其实是非常大的
虽然卡夫卡是有很多个broker有很多个节点
但是有些集群级别的故障
可能你这个时候影响会非常大
或者说有时候我的卡夫卡轻微有些抖动
它有一些报错
它有一些轻微的报错
我们比较容易想到的是什么
就是所谓的一套稳定性不够 搭两套
搭一个back up的Kafka
但是如果我们想过没有
你如果搭两套Kafka
你的资源是不是乘以2
你搭小的切过去接管
你搭小的就是你搭个小集群
你流量完全切过去是不是接不住
是不是接不住
那么你付出的成本又很高
如果你搭资源一模一样的话
又成本非常高
所以有没有一个办法
就是你只需要付出大概10%的资源
就能解决部分系统可用性的问题
所以它叫Gutter相当于叫渠道
把你多出来溢出来的那些流量那些负载
那些请求溢出来的那些流量把它排放到一个
只有10%资源的一个小集群把它们消费掉
这是我们当时的一个思路
集群可能会出现一些抖动
可能会短时的一些报错
你把这些溢出的负载或者流量下卸载到
其它的一个小集群就能解决
所以我们叫Gutter 叫gutter kafka
它是一种本质上是一种failover的思路
但是它跟完整的failover
需要翻倍的机器资源思路又不太一样
因为完整的翻倍你需要完全接管流量
我们这个地方不是这个思路
所以我当时出的策略给我们同学它是怎么玩的
它用双熔断算法做的
也就是说我先一开始写kafka A
写不进去就会熔断
因为我去设定熔断用熔断算法
去写设定一个值
那么熔断报错以后
把这些多出来的流量再写到我们的backup
我们backup的Kafka
它那个是个小集群
只用10%的机器去搭建的
那么也写进去
如果报错特别多
我熔断的是不是特别多都切过来
那肯定不行
肯定不能都接管
所以说B Kafka集群也要上一个熔断算法
它扛不住的时候
流量再切回来
所以它是一个优先写A
如果被熔断了就要写B
那么B熔断了呢
那些流量就要自动回写到A
它是这样的一套双熔断的一个算法
这样的话你只需要付出10%的资源
就能解决溢出的这些流量
所以这个思路其实也非常巧妙
我们实际压测下来效果也还不错
我们效果也还不错
那么甲方这些熔断既然是一种服务端级别的
善意的主动的在Client侧的进行节流
那么其实在移动端
也是需要善意的这种客户端流控的
因为大家有看过双11
就是这时候你老婆在
或者你老公在双11抢东西的时候
他可能会去什么
他可能会去秒杀去抢东西
这个时候可能会报错
什么小二报错了让你重试
让你稍等一会重试
然后一个小车让你进度条滑个10秒钟
然后你才能去重试
那么这种机制其实它本质上也是一种
也是一种在客户端在App端去进行一个节流
因为大家知道吗
有一种策略叫有个术语叫Positive feedback
用户总是很积极的重试
因为访问一个不可达的服务
因为你为什么
你用某个功能好好的
不让你用了不让你用了
或者比方你刷B站突然一刷打不开了
看直播看的正嗨
看S9S10的比赛正嗨
突然看不了了
断流了
你是不是会猛刷网页猛刷App
杀掉重来几次
我们以前每次出事故总发现
出事故的时候流量往往会翻倍会增加很多为什么
用户总是积极尝试访问一个不可达的服务
因为他总会去猛刷
这个时候流量会翻倍
我们以前老觉得是以前我们没有经验
就老是每次觉得出事故的时候
流量翻倍是有人在打我们
有人在攻击我们
实际上这些流量是用户重试导致的
所以我们要在App端去限制请求的频次
一般来说App用户主动重试
它也是一种retry
那么这种retry也要做backoff
要做一定的请求退让
这个请求退让可以怎么做
比方说我第一次请求失败sleep3秒钟
第二次请求失败sleep5秒钟
第三次请求失败sleep7秒钟
你可以去做backoff去做一些失败退让
那么最典型经典的一个backoff的算法
叫Jitter backoff
我们看见这个代码其实也非常简单
我从gRPC的代码里面抠出来的
比方第0次重试第0次重试sleep一个baseDelay
可能是一秒钟
那么第二次重试我们看一下
第二次重试我们看一下
在不超过MaxDelay
比方MaxDelay是10秒钟不超过10秒
那么retry大于0的情况下
把它做一个自侦
backoff成一个自增因子
自增因子可能等于2或者等于1.1.5
就比方一秒钟乘以1.5
然后它会乘以一个它的次方
你重试几次就成几个次方
那么把这个值累加以后
如果你的服务一报错
大家移动端一刷一刷
有可能会叠出一个重试的波峰出来
所以说它又会做一些打散
它会乘一个jitter
jitter是一个随机是一个值
把这个jitter乘一个随机的因子加上1
它就相当于把它随机化掉了把它随机化掉了
就基特乘以一个随机因子加上1
最终其实就把整个请求全部打散
避免叠出一个重试的波峰出来
当然还有我们有时候接口报错的时候
你可以在在我们接口里面错误码里面有个叫details
我们可以在接口的details里面
携带一些retry info
在这个里面告诉它一些sleep的策略
那么这样的话你一报错
你的重试才能立马可以通过接口的这些叫hint吧
叫提示信息
通过你的接口提示信息下发到客户端
就不需要客户端去做搞个什么配置那种
它是通过接口可以下放策略出来的
那么你可以立马去做一些管控
这那么你如果不想搞那么复杂
你可以直接如果发现5叉叉4叉叉错误
直接3秒内不允许重复请求
我们之前就做了个保护功能
就是如果刷我们的页面报错了
我们会再立马重新刷
它就会出现一个叫技能冷却中
让你不要刷的太猛
就过一会才能再访问
所以这个也是比较典型的一个
客户端流控的一个场景
那么再看一下几个Case Study
我们之前出现过一次2层缓存穿透
可能是Redis集群故障
然后大量回源导致核心服务故障
因为你缓存一挂
大的情况会miss
miss到然后就会去查数据库
你这个时候数据库有可能就扛不住就挂了
然后这个时候就雪崩了
因为数据库挂就基本上雪崩了
怎么办
很容易想到就是用分布式限流去管控
所有你的微服务发现MySQL的请求数量
因为这样的话
虽然会有一些报错
但是你买MySQL随着慢慢预热
让我的缓存重新构建
最终你的服务是可以恢复正常的
所以对我们底层存储的一些核心组件
一定要做分布式限流这是第一
第二就是说叫异常客户端引起的服务故障
叫query of death
我们之前遇到过一个什么样的租户
我们的稿件服务被一个租户调用
他出了一个bug
然后疯狂在哪发请求
有点像ddos那种就疯狂打我们
这个时候如果你做了分布式限流
并且是我刚刚说的
支持某一个租户配置多少配额那种
你可以把它设定你可以完全把它设成0
让它一个都发不出去
避免被打死
或者说你之前完全没有限流
没有分布式限流策略
它这种无限循环来打你的
你怎么办
你只能干瞪眼
你只能催他打电话
让他赶紧改代码
发布上线
你整个时间周期比较长
这个时候如果只是它一个人有问题
你可以立马在你这边
分布式限流里面加一个
那个租户只能发100个请求
它就被管的死死的
或者说它有可能发个请求
请求一发
一发就可能会把你的服务打挂
因为可能一些bug
或者传递的一些参数什么之类的有问题
可能就它那一个租户有问题
就把它设成0 一个都不让它发
因为你只能这个时候只能让它牺牲掉
其它人放
让大家好好过
这个其实也是比较常见的一个点
第三一定要注意
用户在一个服务不可达的时候
经常会大面积重试
所以你在你的App端一定要埋好这种策略
无论你的策略是做Jitter backoff这种
随机增长的这种算法
或者说你是通过接口报错的时候下发一些策略
都是可以的
这样的话只要限流
自适应保护加超时能够让你的服务
能够持续提供服务
你就有办法应用降级
所以我们下面也是要讲一下降级的一个策略