在一个高并发系统中对流量的把控是非常重要的,当巨大的流量直接请求到我们的服务器上没多久就可能造成接口不可用,不处理的话甚至会造成整个应用不可用。
【限流的作用】
由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。
限流 (Ratelimiting) 指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒, 对超过限制的请求则进行快速失败或丢弃。
限流可以应对:
· 热点业务带来的突发请求;
· 调用方 bug 导致的突发请求;
· 恶意攻击请求。
因此,对于公开的接口最好采取限流措施。
【为什么要分布式限流】
当应用为单点应用时,只要应用进行了限流,那么应用所依赖的各种服务也都得到了保护。
但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。
而如果实现了分布式限流,那么就可以方便地控制整个服务集群的请求限制,且由于整个集群的请求数量得到了限制,因此服务依赖的各种资源也得到了限流的保护。
【限流的算法】
实现限流有很多办法,在程序中时通常是根据每秒处理的事务数 (Transactionpersecond) 来衡量接口的流量。
本文介绍几种最常用的限流算法:
· 固定窗口计数器;
· 滑动窗口计数器;
· 漏桶;
· 令牌桶。
1、固定窗口计数器算法
固定窗口计数器算法概念如下:
· 将时间划分为多个窗口;
· 在每个窗口内每有一次请求就将计数器加一;
· 如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃当时间到达下一个窗口时,计数器重置。
固定窗口计数器是最为简单的算法,但这个算法有时会让通过请求量允许为限制的两倍。考虑如下情况:限制 1 秒内最多通过 5 个请求,在第一个窗口的最后半秒内通过了 5 个请求,第二个窗口的前半秒内又通过了 5 个请求。这样看来就是在 1 秒内通过了 10 个请求。
2、滑动窗口计数器算法
滑动窗口计数器算法概念如下:
· 将时间划分为多个区间;
· 在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间;
· 每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间;
· 如果当前窗口内区间的请求计数总和超过了限制数量,则本窗口内所有的请求都被丢弃。
滑动窗口计数器是通过将窗口再细分,并且按照时间 " 滑动 ",这种算法避免了固定窗口计数器带来的双倍突发请求,但时间区间的精度越高,算法所需的空间容量就越大。
3、漏桶算法
漏桶算法概念如下:
· 将每个请求视作 " 水滴 " 放入 " 漏桶 " 进行存储;
· “漏桶 " 以固定速率向外 " 漏 " 出请求来执行如果 " 漏桶 " 空了则停止 " 漏水”;
· 如果 " 漏桶 " 满了则多余的 " 水滴 " 会被直接丢弃。
漏桶算法多使用队列实现,服务的请求会存到队列中,服务的提供方则按照固定的速率从队列中取出请求并执行,过多的请求则放在队列中排队或直接拒绝。
漏桶算法的缺陷也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。
4、令牌桶算法
令牌桶算法概念如下:
· 令牌以固定速率生成;
· 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行;
· 如果桶空了,那么尝试取令牌的请求会被直接丢弃。
令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。
【分布式限流概述】
分布式限流需要解决什么问题呢?我想至少有下面几个:
1.动态规则:比如限流的QPS我们希望可以动态修改,限流的功能可以随时开启、关闭,限流的规则可以跟随业务进行动态变更等。
2.集群限流:比如对Spring Cloud微服务架构中的某服务下的所有实例进行统一限流,以控制后续访问数据库的流量。
3.熔断降级:比如在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
可选的其它几个功能,诸如实时监控数据、网关流控、热点参数限流、系统自适应限流、黑白名单控制、注解支持等,这些功能其实可以非常方便的进行扩展。
【分布式限流方案】
分布式限流的思想我列举下面三个方案:
1.Redis令牌桶
这种方案是最简单的一种集群限流思想。在本地限流中,我们使用Long的原子类作令牌桶,当实例数量超过1,我们就考虑将Redis用作公共内存区域,进行读写。涉及到的并发控制,也可以使用Redis实现分布式锁。
方案的缺点显而易见,每取一次令牌都会进行一次网络开销,而网络开销起码是毫秒级,所以这种方案支持的并发量是非常有限的。
2.QPS统一分配
这种方案的思想是将集群限流最大程度的本地化。
举个例子,我们有两台服务器实例,对应的是同一个应用程序(Application.name相同),程序中设置的QPS为100,将应用程序与同一个控制台程序进行连接,控制台端依据应用的实例数量将QPS进行均分,动态设置每个实例的QPS为50,若是遇到两个服务器的配置并不相同,在负载均衡层的就已经根据服务器的优劣对流量进行分配,例如一台分配70%流量,另一台分配30%的流量。面对这种情况,控制台也可以对其实行加权分配QPS的策略。
客观来说,这是一种集群限流的实现方案,但依旧存在不小的问题。该模式的分配比例是建立在大数据流量下的趋势进行分配,实际情况中可能并不是严格的五五分或三七分,误差不可控,极容易出现用户连续访问某一台服务器遇到请求驳回而另一台服务器此刻空闲流量充足的尴尬情况。
3.发票服务器
这种方案的思想是建立在Redis令牌桶方案的基础之上的。如何解决每次取令牌都伴随一次网络开销,该方案的解决方法是建立一层控制端,利用该控制端与Redis令牌桶进行交互,只有当客户端的剩余令牌数不足时,客户端才向该控制层取令牌并且每次取一批。
这种思想类似于Java集合框架的数组扩容,设置一个阈值,只有当超过该临界值时,才会触发异步调用。其余存取令牌的操作与本地限流无二。虽然该方案依旧存在误差,但误差最大也就一批次令牌数而已。
【结语】
作为如此重要的功能,在 Java 中自然有很多实现限流的类库,例如 Google 的开源项目 guava 提供了 RateLimiter 类,实现了单点的令牌桶限流。
而分布式限流常用的则有 Hystrix、resilience4j、Sentinel 等框架,但这些框架都需引入第三方的类库,对于国企等一些保守的企业,引入外部类库都需要经过层层审批,较为麻烦。
分布式限流本质上是一个集群并发问题,而 Redis 作为一个应用广泛的中间件,又拥有单进程单线程的特性,天然可以解决分布式集群的并发问题。本文简单介绍一个通过 Redis 实现单次请求判断限流的功能。
登录后可发表评论
05月30日 11:56
05月23日 12:03
05月16日 12:06
05月13日 23:57
05月12日 15:48
05月02日 12:04
04月04日 19:34
04月04日 13:56
03月14日 16:07
03月09日 14:30