关于java:Spring-Cloud-Gateway-限流实战终于有人写清楚了

4次阅读

共计 24860 个字符,预计需要花费 63 分钟才能阅读完成。

话说在 Spring Cloud Gateway 问世之前,Spring Cloud 的微服务世界里,网关肯定非 Netflix Zuul 莫属。然而因为 Zuul 1.x 存在的一些问题,比方阻塞式的 API,不反对 WebSocket 等,始终被人所诟病,而且 Zuul 降级新版本依赖于 Netflix 公司,通过几次跳票之后,Spring 开源社区决定推出本人的网关组件,代替 Netflix Zuul。

从 18 年 6 月 Spring Cloud 公布的 Finchley 版本开始,Spring Cloud Gateway 逐步锋芒毕露,它基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发,不仅反对响应式和无阻塞式的 API,而且反对 WebSocket,和 Spring 框架严密集成。只管 Zuul 起初也推出了 2.x 版本,在底层应用了异步无阻塞式的 API,大大改善了其性能,然而目前看来 Spring 并没有打算持续集成它的打算。

依据官网的形容,Spring Cloud Gateway 的次要个性如下:

  • Built on Spring Framework 5, Project Reactor and Spring Boot 2.0
  • Able to match routes on any request attribute
  • Predicates and filters are specific to routes
  • Hystrix Circuit Breaker integration
  • Spring Cloud DiscoveryClient integration
  • Easy to write Predicates and Filters
  • Request Rate Limiting
  • Path Rewriting

能够看出 Spring Cloud Gateway 能够很不便的和 Spring Cloud 生态中的其余组件进行集成(比方:断路器和服务发现),而且提供了一套简略易写的 断言 Predicates,有的中央也翻译成 谓词 )和 过滤器 Filters)机制,能够对每个 路由Routes)进行非凡申请解决。

最近在我的项目中应用了 Spring Cloud Gateway,并在它的根底上实现了一些高级个性,如限流和留痕,在网关的应用过程中遇到了不少的挑战,于是趁着我的项目完结,抽点工夫系统地学习并总结下。这篇文章次要学习限流技术,首先我会介绍一些常见的限流场景和限流算法,而后介绍一些对于限流的开源我的项目,学习他人是如何实现限流的,最初介绍我是如何在网关中实现限流的,并分享一些实现过程中的教训和遇到的坑。

一、常见的限流场景

缓存 降级 限流 被称为高并发、分布式系统的三驾马车,网关作为整个分布式系统中的第一道关卡,限流性能天然必不可少。通过限流,能够管制服务申请的速率,从而进步零碎应答突发大流量的能力,让零碎更具弹性。限流有着很多理论的利用场景,比方双十一的秒杀流动,12306 的抢票等。

1.1 限流的对象

通过下面的介绍,咱们对限流的概念可能感觉还是比拟含糊,到底限流限的是什么?顾名思义,限流就是限度流量,但这里的流量是一个比拟抽象的概念。如果思考各种不同的场景,限流是非常复杂的,而且和具体的业务规定密切相关,能够思考如下几种常见的场景:

  • 限度某个接口一分钟内最多申请 100 次
  • 限度某个用户的下载速度最多 100KB/S
  • 限度某个用户同时只能对某个接口发动 5 路申请
  • 限度某个 IP 起源禁止拜访任何申请

从下面的例子能够看出,依据不同的请求者和申请资源,能够组合出不同的限流规定。能够依据请求者的 IP 来进行限流,或者依据申请对应的用户来限流,又或者依据某个特定的申请参数来限流。而限流的对象能够是申请的频率,传输的速率,或者并发量等,其中最常见的两个限流对象是申请频率和并发量,他们对应的限流被称为 申请频率限流 (Request rate limiting)和 并发量限流 (Concurrent requests limiting)。 传输速率限流 在下载场景下比拟罕用,比方一些资源下载站会限度普通用户的下载速度,只有购买会员能力提速,这种限流的做法实际上和申请频率限流相似,只不过一个限度的是申请量的多少,一个限度的是申请数据报文的大小。这篇文章次要介绍申请频率限流和并发量限流。

1.2 限流的解决形式

在零碎中设计限流计划时,有一个问题值得设计者去认真思考,当请求者被限流规定拦挡之后,咱们该如何返回后果。个别咱们有上面三种限流的解决形式:

  • 拒绝服务
  • 排队期待
  • 服务降级

最简略的做法是拒绝服务,间接抛出异样,返回错误信息(比方返回 HTTP 状态码 429 Too Many Requests),或者给前端返回 302 重定向到一个谬误页面,提醒用户资源没有了或稍后再试。然而对于一些比拟重要的接口不能间接回绝,比方秒杀、下单等接口,咱们既不心愿用户申请太快,也不心愿申请失败,这种状况个别会将申请放到一个音讯队列中排队期待,音讯队列能够起到削峰和限流的作用。第三种解决形式是服务降级,当触发限流条件时,间接返回兜底数据,比方查问商品库存的接口,能够默认返回有货。

1.3 限流的架构

针对不同的零碎架构,须要应用不同的限流计划。如下图所示,服务部署的形式个别能够分为单机模式和集群模式:

单机模式的限流非常简单,能够间接基于内存就能够实现,而集群模式的限流必须依赖于某个“中心化”的组件,比方网关或 Redis,从而引出两种不同的限流架构:网关层限流 中间件限流

网关作为整个分布式系统的入口,承当了所有的用户申请,所以在网关中进行限流是最合适不过的。网关层限流有时也被称为 接入层限流。除了咱们应用的 Spring Cloud Gateway,最罕用的网关层组件还有 Nginx,能够通过它的 ngx_http_limit_req_module 模块,应用 limit_conn_zone、limit_req_zone、limit_rate 等指令很容易的实现并发量限流、申请频率限流和传输速率限流。这里不对 Nginx 作过多的阐明,对于这几个指令的详细信息能够 参考 Nginx 的官网文档。

另一种限流架构是 中间件限流,能够将限流的逻辑下沉到服务层。然而集群中的每个服务必须将本人的流量信息对立汇总到某个中央供其余服务读取,一般来说用 Redis 的比拟多,Redis 提供的过期个性和 lua 脚本执行非常适合做限流。除了 Redis 这种中间件,还有很多相似的分布式缓存零碎都能够应用,如 Hazelcast、Apache Ignite、Infinispan 等。

咱们能够更进一步扩大下面的架构,将网关改为集群模式,尽管这还是网关层限流架构,然而因为网关变成了集群模式,所以网关必须依赖于中间件进行限流,这和下面探讨的中间件限流没有区别。

二、常见的限流算法

通过下面的学习,咱们晓得限流能够分为申请频率限流和并发量限流,依据零碎架构的不同,又能够分为网关层限流和分布式限流。在不同的利用场景下,咱们须要采纳不同的限流算法。这一节将介绍一些支流的限流算法。

有一点要留神的是,利用池化技术也能够达到限流的目标,比方线程池或连接池,但这不是本文的重点。

2.1 固定窗口算法(Fixed Window)

固定窗口算法是一种最简略的限流算法,它依据限流的条件,将申请工夫映射到一个工夫窗口,再应用计数器累加拜访次数。譬如限流条件为每分钟 5 次,那么就依照分钟为单位映射工夫窗口,假如一个申请工夫为 11:00:45,工夫窗口就是 11:00:00 ~ 11:00:59,在这个工夫窗口内设定一个计数器,每来一个申请计数器加一,当这个工夫窗口的计数器超过 5 时,就触发限流条件。当申请工夫落在下一个工夫窗口内时(11:01:00 ~ 11:01:59),上一个窗口的计数器生效,以后的计数器清零,从新开始计数。

计数器算法非常容易实现,在单机场景下能够应用 AtomicLong、LongAdder 或 Semaphore 来实现计数,而在分布式场景下能够通过 Redis 的 INCR 和 EXPIRE 等命令并联合 EVAL 或 lua 脚本来实现,Redis 官网提供了几种简略的实现形式。无论是申请频率限流还是并发量限流都能够应用这个算法。

不过这个算法的缺点也比拟显著,那就是存在重大的临界问题。因为每过一个工夫窗口,计数器就会清零,这使得限流成果不够平滑,歹意用户能够利用这个特点绕过咱们的限流规定。如下图所示,咱们的限流条件原本是每分钟 5 次,然而歹意用户在 11:00:00 ~ 11:00:59 这个工夫窗口的后半分钟发动 5 次申请,接下来又在 11:01:00 ~ 11:01:59 这个工夫窗口的前半分钟发动 5 次申请,这样咱们的零碎就在 1 分钟内接受了 10 次申请。

2.2 滑动窗口算法(Rolling Window 或 Sliding Window)

为了解决固定窗口算法的临界问题,能够将工夫窗口划分成更小的工夫窗口,而后随着工夫的滑动删除相应的小窗口,而不是间接滑过一个大窗口,这就是滑动窗口算法。咱们为每个小工夫窗口都设置一个计数器,大工夫窗口的总申请次数就是每个小工夫窗口的计数器的和。如下图所示,咱们的工夫窗口是 5 秒,能够按秒进行划分,将其划分成 5 个小窗口,工夫每过一秒,工夫窗口就滑过一秒:

每次解决申请时,都须要计算所有小工夫窗口的计数器的和,思考到性能问题,划分的小工夫窗口不宜过多,譬如限流条件是每小时 N 个,能够按分钟划分为 60 个窗口,而不是按秒划分成 3600 个。当然如果不思考性能问题,划分粒度越细,限流成果就越平滑。相同,如果划分粒度越粗,限流成果就越不准确,呈现临界问题的可能性也就越大,当划分粒度为 1 时,滑动窗口算法就进化成了固定窗口算法。因为这两种算法都应用了计数器,所以也被称为 计数器算法(Counters)。

进一步思考咱们发现,如果划分粒度最粗,也就是只有一个工夫窗口时,滑动窗口算法进化成了固定窗口算法;那如果咱们把划分粒度调到最细,又会如何呢?那么怎样才能让划分的工夫窗口最细呢?工夫窗口细到肯定境地时,意味着每个工夫窗口中只能包容一个申请,这样咱们能够省略计数器,只记录每个申请的工夫,而后统计一段时间内的申请数有多少个即可。具体的实现能够参考 Redis sorted set 技巧 和 Sliding window log 算法。

2.3 漏桶算法(Leaky Bucket)

除了计数器算法,另一个很天然的限流思路是将所有的申请缓存到一个队列中,而后按某个固定的速度缓缓解决,这其实就是 漏桶算法(Leaky Bucket)。漏桶算法假如将申请装到一个桶中,桶的容量为 M,当桶满时,申请被抛弃。在桶的底部有一个洞,桶中的申请像水一样按固定的速度(每秒 r 个)漏出来。咱们用上面这个形象的图来示意漏桶算法:

桶的下面是个水龙头,咱们的申请从水龙头流到桶中,水龙头流出的水速不定,有时快有时慢,这种忽快忽慢的流量叫做 Bursty flow。如果桶中的水满了,多余的水就会溢出去,相当于申请被抛弃。从桶底部漏出的水速是固定不变的,能够看出漏桶算法能够平滑申请的速率。

漏桶算法能够通过一个队列来实现,如下图所示:

当申请达到时,不间接解决申请,而是将其放入一个队列,而后另一个线程以固定的速率从队列中读取申请并解决,从而达到限流的目标。留神的是这个队列能够有不同的实现形式,比方设置申请的存活工夫,或将队列革新成 PriorityQueue,依据申请的优先级排序而不是先进先出。当然队列也有满的时候,如果队列曾经满了,那么申请只能被抛弃了。漏桶算法有一个缺点,在解决突发流量时效率很低,于是人们又想出了上面的令牌桶算法。

2.4 令牌桶算法(Token Bucket)

令牌桶算法 (Token Bucket)是目前利用最宽泛的一种限流算法,它的根本思维由两局部组成: 生成令牌 生产令牌

  • 生成令牌:假如有一个装令牌的桶,最多能装 M 个,而后按某个固定的速度(每秒 r 个)往桶中放入令牌,桶满时不再放入;
  • 生产令牌:咱们的每次申请都须要从桶中拿一个令牌能力放行,当桶中没有令牌时即触发限流,这时能够将申请放入一个缓冲队列中排队期待,或者间接回绝;

令牌桶算法的图示如下:

在下面的图中,咱们将申请放在一个缓冲队列中,能够看出这一部分的逻辑和漏桶算法简直截然不同,只不过在解决申请上,一个是以固定速率解决,一个是从桶中获取令牌后才解决。

认真思考就会发现,令牌桶算法有一个很要害的问题,就是桶大小的设置,正是这个参数能够让令牌桶算法具备解决突发流量的能力。譬如将桶大小设置为 100,生成令牌的速度设置为每秒 10 个,那么在零碎闲暇一段时间的之后(桶中令牌始终没有生产,缓缓的会被装满),忽然来了 50 个申请,这时零碎能够间接按每秒 50 个的速度解决,随着桶中的令牌很快用完,处理速度又会缓缓降下来,和生成令牌速度趋于统一。这是令牌桶算法和漏桶算法最大的区别,漏桶算法无论来了多少申请,只会始终以每秒 10 个的速度进行解决。当然,解决突发流量尽管进步了零碎性能,但也给零碎带来了肯定的压力,如果桶大小设置不合理,突发的大流量可能会间接压垮零碎。

通过上面对令牌桶的原理剖析,个别会有两种不同的实现形式。第一种形式是启动一个外部线程,一直的往桶中增加令牌,解决申请时从桶中获取令牌,和下面图中的解决逻辑一样。第二种形式不依赖于外部线程,而是在每次解决申请之前先实时计算出要填充的令牌数并填充,而后再从桶中获取令牌。上面是第二种形式的一种经典实现,其中 capacity 示意令牌桶大小,refillTokensPerOneMillis 示意填充速度,每毫秒填充多少个,availableTokens 示意令牌桶中还剩多少个令牌,lastRefillTimestamp 示意上一次填充工夫。

 1public class TokenBucket {
 2
 3    private final long capacity;
 4    private final double refillTokensPerOneMillis;
 5    private double availableTokens;
 6    private long lastRefillTimestamp;
 7
 8    public TokenBucket(long capacity, long refillTokens, long refillPeriodMillis) {
 9        this.capacity = capacity;
10        this.refillTokensPerOneMillis = (double) refillTokens / (double) refillPeriodMillis;
11        this.availableTokens = capacity;
12        this.lastRefillTimestamp = System.currentTimeMillis();
13    }
14
15    synchronized public boolean tryConsume(int numberTokens) {16        refill();
17        if (availableTokens < numberTokens) {
18            return false;
19        } else {
20            availableTokens -= numberTokens;
21            return true;
22        }
23    }
24
25    private void refill() {26        long currentTimeMillis = System.currentTimeMillis();
27        if (currentTimeMillis > lastRefillTimestamp) {
28            long millisSinceLastRefill = currentTimeMillis - lastRefillTimestamp;
29            double refill = millisSinceLastRefill * refillTokensPerOneMillis;
30            this.availableTokens = Math.min(capacity, availableTokens + refill);
31            this.lastRefillTimestamp = currentTimeMillis;
32        }
33    }
34}

能够像上面这样创立一个令牌桶(桶大小为 100,且每秒生成 100 个令牌):

1TokenBucket limiter = new TokenBucket(100, 100, 1000);

从下面的代码片段能够看出,令牌桶算法的实现非常简单也十分高效,仅仅通过几个变量的运算就实现了残缺的限流性能。外围逻辑在于 refill() 这个办法,在每次生产令牌时,计算以后工夫和上一次填充的时间差,并依据填充速度计算出应该填充多少令牌。在从新填充令牌后,再判断申请的令牌数是否足够,如果不够,返回 false,如果足够,则减去令牌数,并返回 true。

在理论的利用中,往往不会间接应用这种原始的令牌桶算法,个别会在它的根底上作一些改良,比方,填充速率反对动静调整,令牌总数反对透支,基于 Redis 反对分布式限流等,不过总体来说还是合乎令牌桶算法的整体框架,咱们在前面学习一些开源我的项目时对此会有更深的领会。

三、一些开源我的项目

有很多开源我的项目中都实现了限流的性能,这一节通过一些开源我的项目的学习,理解限流是如何实现的。

3.1 Guava 的 RateLimiter

Google Guava 是一个弱小的外围库,蕴含了很多有用的工具类,例如:汇合、缓存、并发库、字符串解决、I/O 等等。其中在并发库中,Guava 提供了两个和限流相干的类:RateLimiter 和 SmoothRateLimiter。Guava 的 RateLimiter 基于令牌桶算法实现,不过在传统的令牌桶算法根底上做了点改良,反对两种不同的限流形式:平滑突发限流 (SmoothBursty)和 平滑预热限流(SmoothWarmingUp)。

上面的办法能够创立一个平滑突发限流器(SmoothBursty):

1RateLimiter limiter = RateLimiter.create(5);

RateLimiter.create(5) 示意这个限流器容量为 5,并且每秒生成 5 个令牌,也就是每隔 200 毫秒生成一个。咱们能够应用 limiter.acquire() 生产令牌,如果桶中令牌足够,返回 0,如果令牌有余,则阻塞期待,并返回期待的工夫。咱们间断申请几次:

1System.out.println(limiter.acquire());
2System.out.println(limiter.acquire());
3System.out.println(limiter.acquire());
4System.out.println(limiter.acquire());

输入后果如下:

10.0
20.198239
30.196083
40.200609

能够看出限流器创立之后,初始会有一个令牌,而后每隔 200 毫秒生成一个令牌,所以第一次申请间接返回 0,前面的申请都会阻塞大概 200 毫秒。另外,SmoothBursty 还具备应答突发的能力,而且 还容许生产将来的令牌,比方上面的例子:

1RateLimiter limiter = RateLimiter.create(5);
2System.out.println(limiter.acquire(10));
3System.out.println(limiter.acquire(1));
4System.out.println(limiter.acquire(1));

会失去相似上面的输入:

10.0
21.997428
30.192273
40.200616

限流器创立之后,初始令牌只有一个,然而咱们申请 10 个令牌居然也通过了,只不过看前面申请发现,第二次申请花了 2 秒左右的工夫把后面的透支的令牌给补上了。

Guava 反对的另一种限流形式是平滑预热限流器(SmoothWarmingUp),能够通过上面的办法创立:

1RateLimiter limiter = RateLimiter.create(2, 3, TimeUnit.SECONDS);
2System.out.println(limiter.acquire(1));
3System.out.println(limiter.acquire(1));
4System.out.println(limiter.acquire(1));
5System.out.println(limiter.acquire(1));
6System.out.println(limiter.acquire(1));

第一个参数还是每秒创立的令牌数量,这里是每秒 2 个,也就是每 500 毫秒生成一个,前面的参数示意从冷启动速率过渡到均匀速率的工夫距离,也就是所谓的热身工夫距离(warm up period)。咱们看下输入后果:

10.0
21.329289
30.994375
40.662888
50.501287

第一个申请还是立刻失去令牌,然而前面的申请和下面平滑突发限流就齐全不一样了,按理来说 500 毫秒就会生成一个令牌,然而咱们发现第二个申请却等了 1.3s,而不是 0.5s,前面第三个和第四个申请也等了一段时间。不过能够看出,等待时间在缓缓的靠近 0.5s,直到第五个申请等待时间才开始变得失常。从第一个申请到第五个申请,这两头的工夫距离就是热身阶段,能够算出热身的工夫就是咱们设置的 3 秒。

3.2 Bucket4j

Bucket4j 是一个基于令牌桶算法实现的弱小的限流库,它不仅反对单机限流,还反对通过诸如 Hazelcast、Ignite、Coherence、Infinispan 或其余兼容 JCache API (JSR 107) 标准的分布式缓存实现分布式限流。

在应用 Bucket4j 之前,咱们有必要先理解 Bucket4j 中的几个外围概念:

  • Bucket
  • Bandwidth
  • Refill

Bucket 接口代表了令牌桶的具体实现,也是咱们操作的入口。它提供了诸如 tryConsume 和 tryConsumeAndReturnRemaining 这样的办法供咱们生产令牌。能够通过上面的构造方法来创立 Bucket:

1Bucket bucket = Bucket4j.builder().addLimit(limit).build();
2if(bucket.tryConsume(1)) {3    System.out.println("ok");
4} else {5    System.out.println("error");
6}

Bandwidth 的意思是带宽,能够了解为限流的规定。Bucket4j 提供了两种办法来创立 Bandwidth:simple 和 classic。上面是 simple 形式创立的 Bandwidth,示意桶大小为 10,填充速度为每分钟 10 个令牌:

1Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));

simple 形式桶大小和填充速度是一样的,classic 形式更灵便一点,能够自定义填充速度,上面的例子示意桶大小为 10,填充速度为每分钟 5 个令牌:

1Refill filler = Refill.greedy(5, Duration.ofMinutes(1));
2Bandwidth limit = Bandwidth.classic(10, filler);

其中,Refill 用于填充令牌桶,能够通过它定义填充速度,Bucket4j 有两种填充令牌的策略:距离策略(intervally)和 贪心策略(greedy)。在下面的例子中咱们应用的是贪心策略,如果应用距离策略能够像上面这样创立 Refill:

1Refill filler = Refill.intervally(5, Duration.ofMinutes(1));

所谓距离策略指的是每隔一段时间,一次性的填充所有令牌,比方下面的例子,会每隔一分钟,填充 5 个令牌,如下所示:

而贪心策略会尽可能贪心的填充令牌,同样是下面的例子,会将一分钟划分成 5 个更小的工夫单元,每隔 12 秒,填充 1 个令牌,如下所示:

在理解了 Bucket4j 中的几个外围概念之后,咱们再来看看官网介绍的一些个性:

  • 基于令牌桶算法
  • 高性能,无锁实现
  • 不存在精度问题,所有计算都是基于整型的
  • 反对通过合乎 JCache API 标准的分布式缓存零碎实现分布式限流
  • 反对为每个 Bucket 设置多个 Bandwidth
  • 反对同步和异步 API
  • 反对可插拔的监听 API,用于集成监控和日志
  • 不仅能够用于限流,还能够用于简略的调度

Bucket4j 提供了丰盛的文档,举荐在应用 Bucket4j 之前,先把官网文档中的 根本用法 和 高级个性 仔细阅读一遍。另外,对于 Bucket4j 的应用,举荐这篇文章 Rate limiting Spring MVC endpoints with bucket4j,这篇文章具体的解说了如何在 Spring MVC 中应用拦截器和 Bucket4j 打造业务无侵入的限流计划,另外还解说了如何应用 Hazelcast 实现分布式限流;另外,Rate Limiting a Spring API Using Bucket4j 这篇文章也是一份很好的入门教程,介绍了 Bucket4j 的基础知识,在文章的最初还提供了 Spring Boot Starter 的集成形式,联合 Spring Boot Actuator 很容易将限流指标集成到监控零碎中。

和 Guava 的限流器相比,Bucket4j 的性能显然要更胜一筹,毕竟 Guava 的目标只是用作通用工具类,而不是用于限流的。应用 Bucket4j 基本上能够满足咱们的大多数要求,不仅反对单机限流和分布式限流,而且能够很好的集成监控,搭配 Prometheus 和 Grafana 几乎完满。值得一提的是,有很多开源我的项目譬如 JHipster API Gateway 就是应用 Bucket4j 来实现限流的。

Bucket4j 惟一有余的中央是它只反对申请频率限流,不反对并发量限流,另外还有一点,尽管 Bucket4j 反对分布式限流,但它是基于 Hazelcast 这样的分布式缓存零碎实现的,不能应用 Redis,这在很多应用 Redis 作缓存的我的项目中就很不爽,所以咱们还须要在开源的世界里持续摸索。

3.3 Resilience4j

Resilience4j 是一款轻量级、易使用的高可用框架。用过 Spring Cloud 晚期版本的同学必定都听过 Netflix Hystrix,Resilience4j 的设计灵感就来自于它。自从 Hystrix 进行保护之后,官网也举荐大家应用 Resilience4j 来代替 Hystrix。

Resilience4j 的底层采纳 Vavr,这是一个十分轻量级的 Java 函数式库,使得 Resilience4j 非常适合函数式编程。Resilience4j 以装璜器模式提供对函数式接口或 lambda 表达式的封装,提供了一波高可用机制:重试(Retry)熔断(Circuit Breaker)限流(Rate Limiter)限时(Timer Limiter)隔离(Bulkhead)缓存(Caceh)降级(Fallback)。咱们重点关注这里的两个性能:限流(Rate Limiter)和 隔离(Bulkhead),Rate Limiter 是申请频率限流,Bulkhead 是并发量限流。

Resilience4j 提供了两种限流的实现:SemaphoreBasedRateLimiterAtomicRateLimiterSemaphoreBasedRateLimiter 基于信号量实现,用户的每次申请都会申请一个信号量,并记录申请的工夫,申请通过则容许申请,申请失败则限流,另外有一个外部线程会定期扫描过期的信号量并开释,很显然这是令牌桶的算法。AtomicRateLimiter 和下面的经典实现相似,不须要额定的线程,在解决每次申请时,依据间隔上次申请的工夫和生成令牌的速度主动填充。对于这二者的区别能够参考文章 Rate Limiter Internals in Resilience4j。

Resilience4j 也提供了两种隔离的实现:SemaphoreBulkheadThreadPoolBulkhead,通过信号量或线程池管制申请的并发数,具体的用法参考官网文档,这里不再赘述。

上面是一个同时应用限流和隔离的例子:

 1// 创立一个 Bulkhead,最大并发量为 150
 2BulkheadConfig bulkheadConfig = BulkheadConfig.custom()
 3    .maxConcurrentCalls(150)
 4    .maxWaitTime(100)
 5    .build();
 6Bulkhead bulkhead = Bulkhead.of("backendName", bulkheadConfig);
 7
 8// 创立一个 RateLimiter,每秒容许一次申请
 9RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
10    .timeoutDuration(Duration.ofMillis(100))
11    .limitRefreshPeriod(Duration.ofSeconds(1))
12    .limitForPeriod(1)
13    .build();
14RateLimiter rateLimiter = RateLimiter.of("backendName", rateLimiterConfig);
15
16// 应用 Bulkhead 和 RateLimiter 装璜业务逻辑
17Supplier<String> supplier = () -> backendService.doSomething();
18Supplier<String> decoratedSupplier = Decorators.ofSupplier(supplier)
19  .withBulkhead(bulkhead)
20  .withRateLimiter(rateLimiter)
21  .decorate();
22
23// 调用业务逻辑
24Try<String> try = Try.ofSupplier(decoratedSupplier);
25assertThat(try.isSuccess()).isTrue();

Resilience4j 在性能个性上比 Bucket4j 弱小不少,而且还反对并发量限流。不过最大的遗憾是,Resilience4j 不反对分布式限流。

3.4 其余

网上还有很多限流相干的开源我的项目,不可能一一介绍,这里列出来的只是冰山之一角:

  • https://github.com/mokies/rat…
  • https://github.com/wangzheng0…
  • https://github.com/wukq/rate-…
  • https://github.com/marcosbarb…
  • https://github.com/onblog/Sno…
  • https://gitee.com/zhanghaiyan…
  • https://github.com/Netflix/co…

能够看出,限流技术在理论我的项目中利用十分宽泛,大家对实现本人的限流算法乐此不疲,新算法和新实现层出不穷。然而找来找去,目前还没有找到一款开源我的项目齐全满足我的需要。

我的需要其实很简略,须要同时满足两种不同的限流场景:申请频率限流和并发量限流,并且能同时满足两种不同的限流架构:单机限流和分布式限流。上面咱们就开始在 Spring Cloud Gateway 中实现这几种限流,通过后面介绍的那些我的项目,咱们舍短取长,基本上都能用比拟成熟的技术实现,只不过对于最初一种状况,分布式并发量限流,网上没有搜到现成的解决方案,在和共事探讨了几个早晨之后,想出一种新型的基于双窗口滑动的限流算法,我在这里抛砖引玉,欢送大家批评指正,如果大家有更好的办法,也欢送探讨。

四、在网关中实现限流

在文章一开始介绍 Spring Cloud Gateway 的个性时,咱们留神到其中有一条 Request Rate Limiting,阐明网关自带了限流的性能,然而 Spring Cloud Gateway 自带的限流有很多限度,譬如不反对单机限流,不反对并发量限流,而且它的申请频率限流也是不尽人意,这些都须要咱们本人入手来解决。

4.1 实现单机申请频率限流

Spring Cloud Gateway 中定义了对于限流的一个接口 RateLimiter,如下:

1public interface RateLimiter<C> extends StatefulConfigurable<C> {2    Mono<RateLimiter.Response> isAllowed(String routeId, String id);
3}

这个接口就一个办法 isAllowed,第一个参数 routeId 示意申请路由的 ID,依据 routeId 能够获取限流相干的配置,第二个参数 id 示意要限流的对象的惟一标识,能够是用户名,也能够是 IP,或者其余的能够从 ServerWebExchange 中失去的信息。咱们看下 RequestRateLimiterGatewayFilterFactory 中对 isAllowed 的调用逻辑:

 1@Override
 2public GatewayFilter apply(Config config) {
 3    // 从配置中失去 KeyResolver
 4    KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);
 5    // 从配置中失去 RateLimiter
 6    RateLimiter<Object> limiter = getOrDefault(config.rateLimiter,
 7            defaultRateLimiter);
 8    boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
 9    HttpStatusHolder emptyKeyStatus = HttpStatusHolder
10            .parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));
11
12    return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY)
13            .flatMap(key -> {14                // 通过 KeyResolver 失去 key,作为惟一标识 id 传入 isAllowed() 办法
15                if (EMPTY_KEY.equals(key)) {16                    if (denyEmpty) {17                        setResponseStatus(exchange, emptyKeyStatus);
18                        return exchange.getResponse().setComplete();
19                    }
20                    return chain.filter(exchange);
21                }
22                // 获取以后路由 ID,作为 routeId 参数传入 isAllowed() 办法
23                String routeId = config.getRouteId();
24                if (routeId == null) {
25                    Route route = exchange
26                            .getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
27                    routeId = route.getId();
28                }
29                return limiter.isAllowed(routeId, key).flatMap(response -> {
30
31                    for (Map.Entry<String, String> header : response.getHeaders()
32                            .entrySet()) {33                        exchange.getResponse().getHeaders().add(header.getKey(),
34                                header.getValue());
35                    }
36                    // 申请容许,间接走到下一个 filter
37                    if (response.isAllowed()) {38                        return chain.filter(exchange);
39                    }
40                    // 申请被限流,返回设置的 HTTP 状态码(默认是 429)41                    setResponseStatus(exchange, config.getStatusCode());
42                    return exchange.getResponse().setComplete();
43                });
44            });
45}

从下面的的逻辑能够看出,通过实现 KeyResolver 接口的 resolve 办法就能够自定义要限流的对象了。

1public interface KeyResolver {2    Mono<String> resolve(ServerWebExchange exchange);
3}

比方上面的 HostAddrKeyResolver 能够依据 IP 来限流:

 1public interface KeyResolver {2    Mono<String> resolve(ServerWebExchange exchange);
 3}
 4 比方上面的 HostAddrKeyResolver 能够依据 IP 来限流:5public class HostAddrKeyResolver implements KeyResolver {
 6    @Override
 7    public Mono<String> resolve(ServerWebExchange exchange) {8        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
 9    }
10}

咱们持续看 Spring Cloud Gateway 的代码发现,RateLimiter 接口只提供了一个实现类 RedisRateLimiter:

很显然是基于 Redis 实现的限流,虽说通过 Redis 也能够实现单机限流,然而总感觉有些大材小用,而且对于那些没有 Redis 的环境很不敌对。所以,咱们要实现真正的本地限流。

咱们从 Spring Cloud Gateway 的 pull request 中发现了一个新个性 Feature/local-rate-limiter,而且看提交记录,这个新个性很有可能会合并到 3.0.0 版本中。咱们无妨来看下这个 local-rate-limiter 的实现:LocalRateLimiter.java,能够看出它是基于 Resilience4 有意思的是,这个类 还有一个晚期版本,是基于 Bucket4j 实现的:

 1public Mono<Response> isAllowed(String routeId, String id) {2    Config routeConfig = loadConfiguration(routeId);
 3
 4    // How many requests per second do you want a user to be allowed to do?
 5    int replenishRate = routeConfig.getReplenishRate();
 6
 7    // How many seconds for a token refresh?
 8    int refreshPeriod = routeConfig.getRefreshPeriod();
 9
10    // How many tokens are requested per request?
11    int requestedTokens = routeConfig.getRequestedTokens();
12
13    final io.github.resilience4j.ratelimiter.RateLimiter rateLimiter = RateLimiterRegistry
14            .ofDefaults()
15            .rateLimiter(id, createRateLimiterConfig(refreshPeriod, replenishRate));
16
17    final boolean allowed = rateLimiter.acquirePermission(requestedTokens);
18    final Long tokensLeft = (long) rateLimiter.getMetrics().getAvailablePermissions();
19
20    Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
21    return Mono.just(response);
22}

有意思的是,这个类 还有一个晚期版本,是基于 Bucket4j 实现的:

 1public Mono<Response> isAllowed(String routeId, String id) {
 2
 3    Config routeConfig = loadConfiguration(routeId);
 4
 5    // How many requests per second do you want a user to be allowed to do?
 6    int replenishRate = routeConfig.getReplenishRate();
 7
 8    // How much bursting do you want to allow?
 9    int burstCapacity = routeConfig.getBurstCapacity();
10
11    // How many tokens are requested per request?
12    int requestedTokens = routeConfig.getRequestedTokens();
13
14    final Bucket bucket = bucketMap.computeIfAbsent(id,
15            (key) -> createBucket(replenishRate, burstCapacity));
16
17    final boolean allowed = bucket.tryConsume(requestedTokens);
18
19    Response response = new Response(allowed,
20            getHeaders(routeConfig, bucket.getAvailableTokens()));
21    return Mono.just(response);
22}

实现形式都是相似的,在上面对 Bucket4j 和 Resilience4j 曾经作了比拟具体的介绍,这里不再赘述。不过从这里也能够看出 Spring 生态圈对 Resilience4j 是比拟看好的,咱们也能够将其引入到咱们的我的项目中。

4.2 实现分布式申请频率限流

下面介绍了如何实现单机申请频率限流,接下来再看下分布式申请频率限流。这个就比较简单了,因为下面说了,Spring Cloud Gateway 自带了一个限流实现,就是 RedisRateLimiter,能够用于分布式限流。它的实现原理仍然是基于令牌桶算法的,不过实现逻辑是放在一段 lua 脚本中的,咱们能够在 src/main/resources/META-INF/scripts 目录下找到该脚本文件 request_rate_limiter.lua:

 1local tokens_key = KEYS[1]
 2local timestamp_key = KEYS[2]
 3
 4local rate = tonumber(ARGV[1])
 5local capacity = tonumber(ARGV[2])
 6local now = tonumber(ARGV[3])
 7local requested = tonumber(ARGV[4])
 8
 9local fill_time = capacity/rate
10local ttl = math.floor(fill_time*2)
11
12local last_tokens = tonumber(redis.call("get", tokens_key))
13if last_tokens == nil then
14  last_tokens = capacity
15end
16
17local last_refreshed = tonumber(redis.call("get", timestamp_key))
18if last_refreshed == nil then
19  last_refreshed = 0
20end
21
22local delta = math.max(0, now-last_refreshed)
23local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
24local allowed = filled_tokens >= requested
25local new_tokens = filled_tokens
26local allowed_num = 0
27if allowed then
28  new_tokens = filled_tokens - requested
29  allowed_num = 1
30end
31
32if ttl > 0 then
33  redis.call("setex", tokens_key, ttl, new_tokens)
34  redis.call("setex", timestamp_key, ttl, now)
35end
36
37return {allowed_num, new_tokens}

这段代码和下面介绍令牌桶算法时用 Java 实现的那段经典代码简直是一样的。这里应用 lua 脚本,次要是利用了 Redis 的单线程个性,以及执行 lua 脚本的原子性,防止了并发拜访时可能呈现申请量超出下限的景象。设想目前令牌桶中还剩 1 个令牌,此时有两个申请同时到来,判断令牌是否足够也是同时的,两个申请都认为还剩 1 个令牌,于是两个申请都被容许了。

有两种形式来配置 Spring Cloud Gateway 自带的限流。第一种形式是通过配置文件,比方上面所示的代码,能够对某个 route 进行限流:

 1spring:
 2  cloud:
 3    gateway:
 4      routes:
 5      - id: test
 6        uri: http://httpbin.org:80/get
 7        filters:
 8        - name: RequestRateLimiter
 9          args:
10            key-resolver: '#{@hostAddrKeyResolver}'
11            redis-rate-limiter.replenishRate: 1
12            redis-rate-limiter.burstCapacity: 3

其中,key-resolver 应用 SpEL 表达式 #{@beanName} 从 Spring 容器中获取 hostAddrKeyResolver 对象,burstCapacity 示意令牌桶的大小,replenishRate 示意每秒往桶中填充多少个令牌,也就是填充速度。

第二种形式是通过上面的代码来配置:

 1@Bean
 2public RouteLocator myRoutes(RouteLocatorBuilder builder) {3  return builder.routes()
 4    .route(p -> p
 5      .path("/get")
 6      .filters(filter -> filter.requestRateLimiter()
 7        .rateLimiter(RedisRateLimiter.class, rl -> rl.setBurstCapacity(3).setReplenishRate(1)).and())
 8      .uri("http://httpbin.org:80"))
 9    .build();
10}

这样就能够对某个 route 进行限流了。然而这里有一点要留神,Spring Cloud Gateway 自带的限流器有一个很大的坑,replenishRate 不反对设置小数,也就是说往桶中填充的 token 的速度起码为每秒 1 个,所以,如果我的限流规定是每分钟 10 个申请(按理说应该每 6 秒填充一次,或每秒填充 1/6 个 token),这种状况 Spring Cloud Gateway 就没法正确的限流。网上也有人提了 issue,support greater than a second resolution for the rate limiter,但还没有失去解决。

4.3 实现单机并发量限流

下面学习 Resilience4j 的时候,咱们提到了 Resilience4j 的一个性能个性,叫 隔离(Bulkhead)。Bulkhead 这个单词的意思是船的舱壁,利用舱壁能够将不同的船舱隔离起来,这样如果一个船舱破损进水,那么只损失这一个船舱,其它船舱能够不受影响。借鉴造船行业的教训,这种模式也被引入到软件行业,咱们把它叫做 舱壁模式(Bulkhead pattern)。舱壁模式个别用于服务隔离,对于一些比拟重要的系统资源,如 CPU、内存、连接数等,能够为每个服务设置各自的资源限度,避免某个异样的服务把零碎的所有资源都消耗掉。这种服务隔离的思维同样能够用来做并发量限流。

正如前文所述,Resilience4j 提供了两种 Bulkhead 的实现:SemaphoreBulkhead 和 ThreadPoolBulkhead,这也正是舱壁模式常见的两种实现计划:一种是带计数的信号量,一种是固定大小的线程池。思考到多线程场景下的线程切换老本,默认举荐应用信号量。

在操作系统根底课程中,咱们学习过两个名词:互斥量(Mutex)信号量(Semaphores)。互斥量用于线程的互斥,它和临界区有点类似,只有领有互斥对象的线程才有拜访资源的权限,因为互斥对象只有一个,因而任何状况下只会有一个线程在拜访此共享资源,从而保障了多线程能够平安的拜访和操作共享资源。而信号量是用于线程的同步,这是由荷兰科学家 E.W.Dijkstra 提出的概念,它和互斥量不同,信号容许多个线程同时应用共享资源,然而它同时设定了访问共享资源的线程最大数目,从而能够进行并发量管制。

上面是应用信号量限度并发拜访的一个简略例子:

 1public class SemaphoreTest {
 2
 3    private static ExecutorService threadPool = Executors.newFixedThreadPool(100);
 4    private static Semaphore semaphore = new Semaphore(10);
 5
 6    public static void main(String[] args) {7        for (int i = 0; i < 100; i++) {8            threadPool.execute(new Runnable() {
 9                @Override
10                public void run() {
11                    try {12                        semaphore.acquire();
13                        System.out.println("Request processing ...");
14                        semaphore.release();
15                    } catch (InterruptedException e) {16                        e.printStack();
17                    }
18                }
19            });
20        }
21        threadPool.shutdown();
22    }
23}

这里咱们创立了 100 个线程同时执行,然而因为信号量计数为 10,所以同时只能有 10 个线程在解决申请。说到计数,实际上,在 Java 里除了 Semaphore 还有很多类也能够用作计数,比方 AtomicLong 或 LongAdder,这在并发量限流中十分常见,只是无奈提供像信号量那样的阻塞能力:

 1public class AtomicLongTest {
 2
 3    private static ExecutorService threadPool = Executors.newFixedThreadPool(100);
 4    private static AtomicLong atomic = new AtomicLong();
 5
 6    public static void main(String[] args) {7        for (int i = 0; i < 100; i++) {8            threadPool.execute(new Runnable() {
 9                @Override
10                public void run() {
11                    try {12                        if(atomic.incrementAndGet() > 10) {13                            System.out.println("Request rejected ...");
14                            return;
15                        }
16                        System.out.println("Request processing ...");
17                        atomic.decrementAndGet();
18                    } catch (InterruptedException e) {19                        e.printStack();
20                    }
21                }
22            });
23        }
24        threadPool.shutdown();
25    }
26}

4.4 实现分布式并发量限流

通过在单机实现并发量限流,咱们把握了几种罕用的伎俩:信号量、线程池、计数器,这些都是单机上的概念。那么略微拓展下,如果能实现分布式信号量、分布式线程池、分布式计数器,那么实现分布式并发量限流不就大海捞针了吗?

对于分布式线程池,是我本人杜撰的词,在网上并没有找到相似的概念,比拟靠近的概念是资源调度和散发,然而又感觉不像,这里间接疏忽吧。

对于分布式信号量,还真有这样的货色,比方 Apache Ignite 就提供了 IgniteSemaphore 用于创立分布式信号量,它的应用形式和 Semaphore 十分相似。应用 Redis 的 ZSet 也能够实现分布式信号量,比方 这篇博客介绍的办法,还有《Redis in Action》这本电子书中也提到了这样的例子,教你如何实现 Counting semaphores。另外,Redisson 也实现了基于 Redis 的分布式信号量 RSemaphore,用法也和 Semaphore 相似。应用分布式信号量能够很容易实现分布式并发量限流,实现形式和下面的单机并发量限流简直是一样的。

最初,对于分布式计数器,实现计划也是多种多样。比方应用 Redis 的 INCR 就很容易实现,更有甚者,应用 MySQL 数据库也能够实现。只不过应用计数器要留神操作的原子性,每次申请时都要通过这三步操作:取计数器以后的值、判断是否超过阈值,超过则回绝、将计数器的值自增。这其实和信号量的 P 操作是一样的,而开释就对应 V 操作。

所以,利用分布式信号量和计数器就能够实现并发量限流了吗?问题当然没有这么简略。实际上,下面通过信号量和计数器实现单机并发量限流的代码片段有一个重大 BUG:

1semaphore.acquire();
2System.out.println("Request processing ...");
3semaphore.release();

设想一下如果在解决申请时出现异常了会怎么样?很显然,信号量被该线程获取了,然而却永远不会开释,如果申请异样多了,这将导致信号量被占满,最初一个申请也进不来。在单机场景下,这个问题能够很容易解决,加一个 finally 就行了:

1try {2    semaphore.acquire();
3    System.out.println("Request processing ...");
4} catch (InterruptedException e) {5    e.printStack();
6} finally {7    semaphore.release();
8}

因为无论呈现何种异样,finally 中的代码肯定会执行,这样就保障了信号量肯定会被开释。然而在分布式系统中,就不是加一个 finally 这么简略了。这是因为在分布式系统中可能存在的异样不肯定是可被捕捉的代码异样,还有可能是服务解体或者不可预知的零碎宕机,就算是失常的服务重启也可能导致分布式信号量无奈开释。

对于这个问题,我和几个共事间断探讨了几个早晨,想出了两种解决办法:第一种办法是应用带 TTL 的计数器,第二种办法是基于双窗口滑动的一种比拟 tricky 的算法。

第一种办法比拟容易了解,咱们为每个申请赋予一个惟一 ID,并在 Redis 里写入一个键值对,key 为 requests_xxx(xxx 为申请 ID),value 为 1,并给这个 key 设置一个 TTL(如果你的利用中存在耗时十分长的申请,譬如对于一些 WebSockket 申请可能会继续几个小时,还须要开一个线程定期去刷新这个 key 的 TTL)。而后在判断并发量时,应用 KEYS 命令查问 requests_* 结尾的 key 的个数,就能够晓得以后一共有多少个申请,如果超过并发量下限则拒绝请求。这种办法能够很好的应答服务解体或重启的问题,因为每个 key 都设置了 TTL,所以通过一段时间后,这些 key 就会主动隐没,就不会呈现信号量占满不开释的状况了。然而这里应用 KEYS 命令查问申请个数是一个十分低效的做法,在申请量比拟多的状况下,网关的性能会受到重大影响。咱们能够把 KEYS 命令换成 SCAN,性能会失去些许晋升,但总体来说成果还是很不现实的。

针对第一种办法,咱们能够进一步优化,不必为每个申请写一个键值对,而是为每个分布式系统中的每个实例赋予一个惟一 ID,并在 Redis 里写一个键值对,key 为 instances_xxx(xxx 为实例 ID),value 为这个实例以后的并发量。同样的,咱们为这个 key 设置一个 TTL,并且开启一个线程定期去刷新这个 TTL。每承受一个申请后,计数器加一,申请完结,计数器减一,这和单机场景下的解决形式一样,只不过在判断并发量时,还是须要应用 KEYS 或 SCAN 获取所有的实例,并计算出并发量的总和。不过因为实例个数是无限的,性能比之前的做法有了显著的晋升。

第二种办法我称之为 双窗口滑动算法,联合了 TTL 计数器和滑动窗口算法。咱们按分钟来设置一个工夫窗口,在 Redis 里对应 202009051130 这样的一个 key,value 为计数器,示意申请的数量。当承受一个申请后,在以后的工夫窗口中加一,当申请完结,在以后的工夫窗口中减一,留神,承受申请和申请完结的工夫窗口可能不是同一个。另外,咱们还须要一个本地列表来记录以后实例正在解决的所有申请和申请对应的工夫窗口,并通过一个小于工夫窗口的定时线程(如 30 秒)来迁徙过期的申请,所谓过期,指的是申请的工夫窗口和以后工夫窗口不统一。那么具体如何迁徙呢?咱们首先须要统计列表中一共有多少申请过期了,而后将列表中的过期申请工夫更新为以后工夫窗口,并从 Redis 中上一个工夫窗口挪动相应数量到以后工夫窗口,也就是上一个工夫窗口减 X,以后工夫窗口加 X。因为迁徙线程定期执行,所以过期的申请总是会被挪动到以后窗口,最终 Redis 中只有以后工夫窗口和上个工夫窗口这两个工夫窗口中有数据,再早一点的窗口工夫中的数据会被往后迁徙,所以能够给这个 key 设置一个 3 分钟或 5 分钟的 TTL。判断并发量时,因为只有两个 key,只须要应用 MGET 获取两个值相加即可。上面的流程图详细描述了算法的运行过程:

其中有几个须要留神的细节:

  1. 申请完结时,间接在 Redis 中以后工夫窗口减一即可,就算是正数也没关系。申请列表中的该申请不必急着删除,能够打上完结标记,在迁徙线程中对立删除(当然,如果申请的开始工夫和完结工夫在同一个窗口,能够间接删除);
  2. 迁徙的工夫距离要小于工夫窗口,个别设置为 30s;
  3. Redis 中的 key 肯定要设置 TTL,工夫至多为 2 个工夫窗口,个别设置为 3 分钟;
  4. 迁徙过程波及到“从上一个工夫窗口减”和“在以后工夫窗口加”两个操作,要留神操作的原子性;
  5. 获取以后并发量能够通过 MGET 一次性读取两个工夫窗口的值,不必 GET 两次;
  6. 获取并发量和判断并发量是否超限,这个过程也要留神操作的原子性。

总结

网关作为微服务架构中的重要一环,充当着一夫当关万夫莫开的角色,所以对网关服务的稳定性要求和性能要求都十分高。为保障网关服务的稳定性,一代又一代的程序员们前仆后继,想出了十八般武艺:限流、熔断、隔离、缓存、降级、等等等等。这篇文章从限流动手,具体介绍了限流的场景和算法,以及源码实现和可能踩到的坑。只管限流只是网关的一个十分小的性能,但却影响到网关的方方面面,在零碎架构的设计中至关重要。

尽管我试着从不同的角度心愿把限流介绍的更齐全,但究竟是管中窥豹,只见一斑,还有很多的内容没有介绍到,比方阿里开源的 Sentinel 组件也能够用于限流,因为篇幅无限未能开展。另外前文提到的 Netflix 不再保护 Hystrix 我的项目,这是因为他们把精力放到另一个限流我的项目 concurrency-limits 上了,这个我的项目的指标是打造一款自适应的,极具弹性的限流组件,它借鉴了 TCP 拥塞管制的算法(TCP congestion control algorithm),实现零碎的主动限流,感兴趣的同学能够去它的我的项目主页理解更多内容。

本文篇幅较长,不免疏漏,如有问题,还望不吝赐教。

起源:https://www.aneasystone.com/a…

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 别在再满屏的 if/ else 了,试试策略模式,真香!!

3. 卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅公布,光明模式太炸了!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0