在微服务架构中,服务与服务之间通过远程调用的方式进行通讯,一旦某个被调用的服务发生了故障,其依赖服务也会发生故障,此时就会发生故障的伸张,终极导致劫难性雪崩效应。服务保护就是为了保证服务的稳固性而出生的一套保护方案。本文则着重先容哀求限流算法
雪崩问题
微服务调用链路中的某个服务故障,引起整个链路中的全部微服务都不可用,这就是雪崩。
产生的原因?
- 微服务相互调用,服务提供者出现故障或阻塞。
- 服务调用者没有做好非常处理,导致自身故障。
- 调用链中的全部服务级联失败,导致整个集群故障
解决问题的思绪?
- 尽量避免服务出现故障或阻塞。
- 保证代码的健壮性;
- 保证网络畅通;
- 能应对较高的并发哀求;服务调用者做好远程调用非常的后备方案,避免故障扩散
服务保护方案
哀求限流
限制访问接口的哀求的并发量,避免服务因流量激增出现故障。
线程隔离
也叫做舱壁模式,模仿船舱隔板的防水原理。通过限定每个业务能利用的线程数目而将故障业务隔离,避免故障扩散。
快速失败
给业务编写一个调用失败时的处理的逻辑,称为fallback。当调用出现故障(比如无线程可用)时,按照失败处理逻辑执行业务并返回,而不是直接抛出非常。
服务熔断
由断路器统计哀求的非常比例或慢调用比例,假如超出阈值则会熔断该业务,则拦截该接口的哀求。
熔断期间,全部哀求快速失败,全都走fallback逻辑。
固定窗口限流算法
固定窗口限流算法(Fixed Window Rate Limiting Algorithm)是一种最简单的限流算法,其原理是在固定时间窗口(单元时间)内限制哀求的数目。该算法将时间分成固定的窗口,并在每个窗口内限制哀求的数目。具体来说,算法将哀求按照时间顺序放入时间窗口中,并计算该时间窗口内的哀求数目,假如哀求数目超出了限制,则拒绝该哀求。
假设单元时间(固定时间窗口)是1秒,限流阀值为3。在单元时间1秒内,每来一个哀求,计数器就加1,假如计数器累加的次数高出限流阀值3,后续的哀求全部拒绝。等到1s竣事后,计数器清0,重新开始计数。如下图:
伪代码实现
- public static Integer counter = 0; //统计请求数
- public static long lastAcquireTime = 0L;
- public static final Long windowUnit = 1000L ; //假设固定时间窗口是1000ms
- public static final Integer threshold = 10; // 窗口阀值是10
-
- /**
- * 固定窗口时间算法
- * @return
- */
- public synchronized boolean fixedWindowsTryAcquire() {
- long currentTime = System.currentTimeMillis(); //获取系统当前时间
- if (currentTime - lastAcquireTime > windowUnit) { //检查是否在时间窗口内
- counter = 0; // 计数器清0
- lastAcquireTime = currentTime; //开启新的时间窗口
- }
- if (counter < threshold) { // 小于阀值
- counter++; //计数统计器加1
- return true;
- }
-
- return false;
- }
复制代码 优缺点
优点:固定窗口算法非常简单,易于实现和明白。
缺点:存在明显的临界问题,比如: 假设限流阀值为5个哀求,单元时间窗口是1s,假如我们在单元时间内的前0.8-1s和1-1.2s,分别并发5个哀求。固然都没有高出阀值,但是假如算0.8-1.2s,则并发数高达10,就已经高出单元时间1s不高出5阀值的定义。
滑动窗口限流算法
滑动窗口限流算法是一种常用的限流算法,用于控制系统对外提供服务的速率,防止系统被过多的哀求压垮。它将单元时间周期分为n个小周期,分别记载每个小周期内接口的访问次数,并且根据时间滑动删除逾期的小周期。它可以解决固定窗口临界值的问题。
用一张图表明滑动窗口算法,如下:假设单元时间还是1s,滑动窗口算法把它划分为5个小周期,也就是滑动窗口(单元时间)被划分为5个小格子。每格表示0.2s。每过0.2s,时间窗口就会往右滑动一格。然后呢,每个小周期,都有自己独立的计数器,假如哀求是0.83s到达的,0.8~1.0s对应的计数器就会加1。
滑动窗口如何解决固定窗口的临界问题?
假设1s内的限流阀值还是5个哀求,0.81.0s内(比如0.9s的时候)来了5个哀求,落在黄色格子里。时间过了1.0s这个点之后,又来5个哀求,落在紫色格子里。假如是固定窗口算法,是不会被限流的,但是滑动窗口的话,每过一个小周期,它会右移一个小格。过了1.0s这个点后,会右移一小格,当前的单元时间段是0.21.2s,这个区域的哀求已经高出限定的5了,已触发限流啦,实际上,紫色格子的哀求都被拒绝啦。
显然,当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
伪代码实现
- /**
- * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
- */
- private int SUB_CYCLE = 10;
-
- /**
- * 每分钟限流请求数
- */
- private int thresholdPerMin = 100;
-
- /**
- * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
- */
- private final TreeMap<Long, Integer> counters = new TreeMap<>();
-
- /**
- * 滑动窗口时间算法实现
- */
- public synchronized boolean slidingWindowsTryAcquire() {
- long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
- int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数
-
- //超过阀值限流
- if (currentWindowNum >= thresholdPerMin) {
- return false;
- }
-
- //计数器+1
- counters.get(currentWindowTime)++;
- return true;
- }
-
- /**
- * 统计当前窗口的请求数
- */
- private synchronized int countCurrentWindow(long currentWindowTime) {
- //计算窗口开始位置
- long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
- int count = 0;
-
- //遍历存储的计数器
- Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
- while (iterator.hasNext()) {
- Map.Entry<Long, Integer> entry = iterator.next();
- // 删除无效过期的子窗口计数器
- if (entry.getKey() < startTime) {
- iterator.remove();
- } else {
- //累加当前窗口的所有计数器之和
- count =count + entry.getValue();
- }
- }
- return count;
- }
复制代码 优缺点
优点:
- 简单易懂
- 精度高(通过调整时间窗口的大小来实现不同的限流效果)
- 可扩展性强(可以非常容易地与其他限流算法团结利用)
缺点:
- 突发流量无法处理(无法应对短时间内的大量哀求,因为一旦到达限流后,哀求都会直接暴力被拒绝。这样就会损失一部分哀求,这其实对于产品来说,并不太友爱),需要合理调整时间窗口大小。
漏斗限流算法
漏桶限流算法(Leaky Bucket Algorithm)是一种流量控制算法,用于控制流入网络的数据速率,以防止网络拥塞。它的头脑是将数据包看作是水滴,漏桶看作是一个固定容量的水桶,数据包像水滴一样从桶的顶部流入桶中,并通过桶底的一个小孔以一定的速率流出,从而限制了数据包的流量。
漏桶限流算法的基本工作原理是:对于每个到来的数据包,都将其加入到漏桶中,并检查漏桶中当前的水量是否高出了漏桶的容量。假如高出了容量,就将多余的数据包丢弃。假如漏桶中另有水,就以一定的速率从桶底输出数据包,保证输出的速率不高出预设的速率,从而达到限流的目的。
- 流入的水滴,可以看作是访问系统的哀求,这个流入速率是不确定的。
- 桶的容量一样平常表示系统所能处理的哀求数。
- 假如桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝哀求)
- 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理哀求。
伪代码实现
- /**
- * LeakyBucket 类表示一个漏桶,
- * 包含了桶的容量和漏桶出水速率等参数,
- * 以及当前桶中的水量和上次漏水时间戳等状态。
- */
- public class LeakyBucket {
- private final long capacity; // 桶的容量
- private final long rate; // 漏桶出水速率
- private long water; // 当前桶中的水量
- private long lastLeakTimestamp; // 上次漏水时间戳
-
- public LeakyBucket(long capacity, long rate) {
- this.capacity = capacity;
- this.rate = rate;
- this.water = 0;
- this.lastLeakTimestamp = System.currentTimeMillis();
- }
-
- /**
- * tryConsume() 方法用于尝试向桶中放入一定量的水,如果桶中还有足够的空间,则返回 true,否则返回 false。
- * @param waterRequested
- * @return
- */
- public synchronized boolean tryConsume(long waterRequested) {
- leak();
- if (water + waterRequested <= capacity) {
- water += waterRequested;
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * 。leak() 方法用于漏水,根据当前时间和上次漏水时间戳计算出应该漏出的水量,然后更新桶中的水量和漏水时间戳等状态。
- */
- private void leak() {
- long now = System.currentTimeMillis();
- long elapsedTime = now - lastLeakTimestamp;
- long leakedWater = elapsedTime * rate / 1000;
- if (leakedWater > 0) {
- water = Math.max(0, water - leakedWater);
- lastLeakTimestamp = now;
- }
- }
- }
- //注意: tryConsume() 和 leak() 方法中,都需要对桶的状态进行同步,以保证线程安全性。
复制代码 优缺点
优点:
- 可以平滑限制哀求的处理速率,避免瞬间哀求过多导致系统崩溃或者雪崩。
- 可以控制哀求的处理速率,使得系统可以适应不同的流量需求,避免过载或者过度闲置。
- 可以通过调整桶的大小和漏出速率来满意不同的限流需求,可以灵活地适应不同的场景。
缺点:
- 需要对哀求进行缓存,会增长服务器的内存消耗。
- 对于流量波动比较大的场景,需要较为灵活的参数配置才能达到较好的效果。
- 但是面对突发流量的时候,漏桶算法还是循规蹈矩地处理哀求。因为流量变突发时,肯定希望系统尽量快点处理哀求,提升用户体验。
令牌桶限流算法
令牌桶算法是一种常用的限流算法,可以用于限制单元时间内哀求的数目。该算法维护一个固定容量的令牌桶,每秒钟会向令牌桶中放入一定数目的令牌。当有哀求到来时,假如令牌桶中有足够的令牌,则哀求被允许通过并从令牌桶中消耗一个令牌,否则哀求被拒绝。
伪代码实现
- /**
- * TokenBucket 类表示一个令牌桶
- */
- public class TokenBucket {
-
- private final int capacity; // 令牌桶容量
- private final int rate; // 令牌生成速率,单位:令牌/秒
- private int tokens; // 当前令牌数量
- private long lastRefillTimestamp; // 上次令牌生成时间戳
-
- /**
- * 构造函数中传入令牌桶的容量和令牌生成速率。
- * @param capacity
- * @param rate
- */
- public TokenBucket(int capacity, int rate) {
- this.capacity = capacity;
- this.rate = rate;
- this.tokens = capacity;
- this.lastRefillTimestamp = System.currentTimeMillis();
- }
-
- /**
- * allowRequest() 方法表示一个请求是否允许通过,该方法使用 synchronized 关键字进行同步,以保证线程安全。
- * @return
- */
- public synchronized boolean allowRequest() {
- refill();
- if (tokens > 0) {
- tokens--;
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * refill() 方法用于生成令牌,其中计算令牌数量的逻辑是按照令牌生成速率每秒钟生成一定数量的令牌,
- * tokens 变量表示当前令牌数量,
- * lastRefillTimestamp 变量表示上次令牌生成的时间戳。
- */
- private void refill() {
- long now = System.currentTimeMillis();
- if (now > lastRefillTimestamp) {
- int generatedTokens = (int) ((now - lastRefillTimestamp) / 1000 * rate);
- tokens = Math.min(tokens + generatedTokens, capacity);
- lastRefillTimestamp = now;
- }
- }
- }
复制代码 优缺点
优点:
- 稳固性高:令牌桶算法可以控制哀求的处理速率,可以使系统的负载变得稳固。
- 精度高:令牌桶算法可以根据实际情况动态调整天生令牌的速率,可以实现较高精度的限流。
- 弹性好:令牌桶算法可以处理突发流量,可以在短时间内提供更多的处理能力,以处理突发流量。
Guava的RateLimiter限流组件,就是基于令牌桶算法实现的。
缺点:
- 实现复杂:相对于固定窗口算法等其他限流算法,令牌桶算法的实现较为复杂。 对短时哀求难以处理:在短时间内有大量哀求到来时,可能会导致令牌桶中的令牌被快速消耗完,从而限流。这种情况下,可以考虑利用漏桶算法。
- 时间精度要求高:令牌桶算法需要在固定的时间间隔内天生令牌,因此要求时间精度较高,假如系统时间不准确,可能会导致限流效果不理想。
总体来说,令牌桶算法具有较高的稳固性和精度,但实现相对复杂,适用于对稳固性和精度要求较高的场景。
单机限流和分布式限流
本质上单机限流和分布式限流的区别其实就在于“阈值” 存放的位置,
单机限流就上面所说的算法直接在单台服务器上实现就好了,而每每我们的服务是集群摆设的。
因此需要多台机器协同提供限流功能。
像上述的计数器或者时间窗口的算法,可以将计数器存放至 Redis 等分布式 K-V 存储中。
比方滑动窗口的每个哀求的时间记载可以利用 Redis的 zset存储,利用 ZREMRANGEBYSCORE删除时间窗口之外的数据,再用 ZCARD 计数。
像令牌桶也可以将令牌数目放到 Redis 中。不过这样的方式等于每一个哀求我们都需要去 Redis 判定一下能不能通过,在性能上有一定的损耗。所以有个优化点就是批量获取,每次取令牌不是一个一取,而是取一批,不够了再去取一批,这样可以减少对 Redis 的哀求。
不过要留意一点,批量获取会导致一定范围内的限流误差。比如你取了10 个此时不用,等下一秒再用,那同一时刻集群机器总处理量可能会高出阈值其实「批量」这个优化点太常见了,不论是 MySQL的批量刷盘,还是Kafka 消息的批量发送还是分布式 ID 的高性能发号,都包含了「批量」的头脑
当然,分布式限流另有一种头脑是平分,假设之前单机限流 500,现在集群摆设了5台,那就让每台继续限流 500呗,即在总的入口做总的限流限制,然后每台机子再自己实现限流.
限流的难点
可以看到,每个限流都有个阈值,这个阈值如何定是个难点。
定大了服务器可能顶不住,定小了就“误杀”了,没有资源利用最大化,对用户体验不好。我能想到的就是限流上线之后先预估个大概的阈值,然后不执行真正的限流操纵,而是采取日志记载方式,对日志进行分析查看限流的效果,然后调整阈值,推算出集群总的处理能力,和每台机子的处理能力(方便扩缩容),然后将线上的流量进行重放,测试真正的限流效果,终极值确定,然后上线。
或者基于TCP拥塞控制的头脑,根据哀求响应在一个时间段的响应时间P90或者P99值来确定此时服务器的健康状态,来进举措态限流。在 Ease Gateway产品中实现了这套算法,有爱好的同砚可以自行搜刮;
其实真实的业务场景很复杂,需要限流的条件和资源很多,每个资源限流要求还不一样
限流组件
一样平常而言,我们不需要自己实现限流算法来达到限流的目的,不管是接入层限流还是细粒度的接口限流,都有现成的轮子利用,其实现也是用了上述我们所说的限流算法。
比如 Google Guava 提供的限流工县类 Ratelimiter ,是基于令牌桶实现的,并目扩展了算法,支持预热功能。
阿里开源的限流框架 sentinel中的匀速排队限流计谋,就采用了漏桶算法。
Nginx 中的限流模块 Limit_reg_zone,采用了漏桶算法,另有 OpenResty 中的 resty.limit.reg 库等等
具体的利用还是很简单的,有爱好的同砚可以自行搜刮,对内部实现感爱好的同砚可以下个源码看看,学习下生产级别的限流是如何实现的。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |