一文读懂限流算法及方案介绍

2023-05-28 0 355

译者:天猫信息技术 康志兴

应用领域情景

现代网络许多业务情景,比如说直降、付款、查阅商品详细情况,最大特点是高mammalian,而常常他们的控制系统不能忍受这么大的网络流量,而后产生了许多的应付措施:CDN、消息堆栈、多层内存、跨县多活。

但是不管怎样强化,即便由硬体的力学优点决定了他们控制系统操控性的下限,如果私自转交所有允诺,常常造成暴风雪。

这时开闭TNUMBERAP就有所作为了,管制允诺数,加速失利,保证控制系统满阻抗又不超载。

无与伦比的强化,是将硬件使用量提高到100%,但总有一天不会少于100%

常见开闭演算法

1. 算数器

直接算数,单纯暴力行为,举个范例:

比如说开闭预设为1半小时内10次,所以每天接到允诺就算数加一,并推论这一半小时内算数是否小于下限10,没超过下限就回到获得成功,不然回到失利。

这个演算法的优点是在天数临界值会有较大一瞬间网络流量。

继续下面的范例,平庸状况下,允诺慢速步入,控制系统慢速处理允诺:

一文读懂限流算法及方案介绍

但实际情况中,允诺常常不是慢速步入,假定第n半小时59分59秒的时候突然步入10个允诺,全部允诺获得成功,抵达下一个天数区段时创下算数。所以第n+1半小时刚开始又打进10个允诺,等同于一瞬间步入20个允诺,的确不合乎“1半小时10次”的准则,这种现像叫作“反弹球现像”。

一文读懂限流算法及方案介绍

为解决这个问题,算数器演算法经过强化后,产生了滑动窗口演算法:

他们将天数间隔均匀分隔,比如说将一分钟分为6个10秒,每一个10秒内单独算数,总的数量管制为这6个10秒的总和,他们把这6个10秒成为“窗口”。

所以每过10秒,窗口往前滑动一步,数量管制变为新的6个10秒的总和,如图所示:

一文读懂限流算法及方案介绍

所以如果在临界时,接到10个允诺(图中灰色格子),在下一个天数段来临时,橙色部分又步入10个允诺,但窗口内包含灰色部分,所以已经抵达允诺上线,不再转交新的允诺。

这是滑动窗口演算法。

但是滑动窗口仍然有缺陷,为了保证慢速,他们要划分尽可能多的格子,而格子越多,每一个格子能够转交的允诺数就越少,这样就管制了控制系统一瞬间处理能力。

2. 漏桶

一文读懂限流算法及方案介绍

漏桶演算法其实也很单纯,假定他们有一个固定容量的桶,流速(控制系统处理能力)固定,如果一段天数水龙头水流太大,水就溢出了(允诺被抛弃了)。

用编程的语言来说,每天允诺进来都放入一个先进先出的堆栈中,堆栈满了,则直接回到失利。另外有一个线程池固定间隔不断地从这个堆栈中拉取允诺。

消息堆栈、jdk的线程池,都有类似的设计。

3. 令牌桶

令牌桶演算法比漏桶演算法稍显复杂。

首先,他们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以一个固定的速率往桶里填充,直抵达到桶的容量,多余的令牌将会被丢弃。每当一个允诺过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,允诺无法通过。

一文读懂限流算法及方案介绍

漏桶和令牌桶演算法的区别:

漏桶的特点是消费能力固定,当允诺量超出消费能力时,提供一定的冗余能力,把允诺内存下来慢速消费。优点是对下游保护更好。

令牌桶遇到激增网络流量会更从容,只要存在令牌,则可以通通消费掉。适合有突发特征的网络流量,如直降情景。

开闭计划

一、容器开闭

1. Tomcat

tomcat能够配置连接器的最大线程数属性,该属性maxThreads是Tomcat的最大线程数,当允诺的mammalian小于maxThreads时,请求就会排队执行(排队数设置:accept-count),这样就完成了开闭的目的。

<Connector port=“8080” protocol=“HTTP/1.1” connectionTimeout=“20000” maxThreads=“150” redirectPort=“8443” />

2. Nginx

Nginx 提供了两种开闭手段:一是控制速率,二是控制mammalian连接数。

控制速率他们需要使用 limit_req_zone配置来管制单位天数内的允诺数,即速率管制,示例配置如下:limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;第一个参数:$binary_remote_addr 表示通过remote_addr这个标识来做管制,“binary_”的目的是缩写内存占用量,是管制同一客户端ip地址。第二个参数:zone=mylimit:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息。第三个参数:rate=2r/s表示允许相同标识的客户端的访问频次,这里管制的是每秒2次,还可以有比如说30r/m的。mammalian连接数利用 limit_conn_zonelimit_conn 两个指令即可控制mammalian数,示例配置如下limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn_zone $server_name zone=perserver:10m; server { … limit_conn perip 10; # 管制同一个客户端ip limit_conn perserver 100; }

只有当 request header 被后端处理后,这个连接才进行算数

二、服务端开闭

1. Semaphore

JUC包中提供的信号量工具,它的内部维护了一个

单纯样例:

Semaphore sp = new Semaphore(3); sp.require(); // 阻塞 System.out.println(“执行业务逻辑”); sp.release();

2. RateLimiter

Guava中基于令牌桶实现的一个开闭工具,使用非常单纯,通过方法create()创建一个桶,然后通过acquire()或者tryAcquire()

RateLimiter rateLimiter = RateLimiter.create(5); // 初始化令牌桶,每秒往桶里存放5个令牌 rateLimiter.acquire(); rateLimiter.tryAcquire(); 天数(默认为0,单位为毫秒)则回到失利

RateLimiter在实现时,允许暴增允诺的突发情况存在。

举个范例,他们有一个速率为每秒5个令牌的RateLimiter:

牌,所以会在下一次补充令牌的时候回到结果

下一个令牌的时候直接回到,而预支令牌所需的补充天数会在下一次允诺时进行补偿

public void testSmoothBursty() { RateLimiter r = RateLimiter.create(5); for (int i = 0; i++ < 2; ) { System.out.println(“get 5 tokens: ” + r.acquire(5) + “s”); System.out.println(“get 1 tokens: ” + r.acquire(1) + “s”); System.out.println(“get 1 tokens: ” + r.acquire(1) + “s”); System.out.println(“get 1 tokens: ” + r.acquire(1) + “s”); System.out.println(“end”); } } /** * 控制台输出 * get 5 tokens: 0.0s * get 1 tokens: 0.998068s 滞后效应,需要替前一个允诺进行等待 * get 1 tokens: 0.196288s * get 1 tokens: 0.200391s * end * get 5 tokens: 0.195756s * get 1 tokens: 0.995625s 滞后效应,需要替前一个允诺进行等待 * get 1 tokens: 0.194603s * get 1 tokens: 0.196866s * end */

3. Hystrix

Netflix开源的TNUMBERAP组件,支持两种资源隔离策略:THREAD(默认)或者SEMAPHORE

线程池:每个command运行在一个线程中,开闭是通过线程池的大小来控制的信号量:command是运行在调用线程中,但是通过信号量的容量来进行开闭

线程池策略对每一个资源创建一个线程池以进行网络流量管控,优点是资源隔离彻底,优点是容易造成资源碎片化。

使用样例:

// HelloWorldHystrixCommand要使用Hystrix功能 public class HelloWorldHystrixCommand extends HystrixCommand { private final String name; public HelloWorldHystrixCommand(String name) { super(HystrixCommandGroupKey.Factory.asKey(“ExampleGroup”)); this.name = name; }// 如果继承的是HystrixObservableCommand,要重写Observable construct() @Override protected String run() { return “Hello “ + name; } }

调用该command:

String result = new HelloWorldHystrixCommand(“HLX”).execute(); System.out.println(result); // 打印出Hello HLX

Hystrix已经在2018年停止开发,官方推荐替代项目Resilience4j

更多使用介绍可查看:HystrixTNUMBERAP器的使用

4. Sentinel

阿里开源的开闭TNUMBERAP组件,底层统计采用滑动窗口演算法,开闭方面有两种使用方式:API调用和注解,内部采插槽链来统计和执行校验准则。

通过为方法增加注解@SentinelResource(String name)或者手动调用SphU.entry(String name)方法开启流控。

使用API手动调用流控示例:

@Test public void testRule() { // 配置准则. initFlowRules(); int count = 0; while (true) { try (Entry entry = SphU.entry(“HelloWorld”)) {// 被保护的逻辑 System.out.println(“run “ + ++count + ” times”); } catch (BlockException ex) { // 处理被流控的逻辑 System.out.println(“blocked after “ + count); break; } } } // 输出结果: // run 1 times // run 2 times // run 3 times

关于Sentinel的详细介绍可查看:Sentinel-分布式控制系统的网络流量哨兵

三、分布式下开闭计划

线上环境下,如果对共用资源(如数据库、下游服务)做统一网络流量管制,所以单机开闭显然不能满足,而需要分布式流控计划。

分布式开闭主要采取中心控制系统网络流量管控的计划,由一个中心控制系统统一管控网络流量配额。

这种计划的优点是中心控制系统的可靠性,所以一般需要备用计划,在中心控制系统不可用时,退化为单机流控。

1. Tair通过incr方法实现单纯窗口

实现方式是使用incr()自增方法来算数并与阈值进行大小比较。

public boolean tryAcquire(String key) { // 以秒为单位构建tair的key String wrappedKey = wrapKey(key); // 每天允诺+1,初始值为0,key的有效期设置5sResult<Integer> result = tairManager.incr(NAMESPACE, wrappedKey,1, 0, 5); returnresult.isSuccess() && result.getValue() <= threshold; }private String wrapKey(String key) { long sec = System.currentTimeMillis() / 1000L; returnkey +“:” + sec; }

【备注】incr方法的参数说明

// 方法定义: Result incr(int namespace, Serializable key, int value, intdefaultValue,int expireTime) /* 参数含义: namespace – 申请时分配的 namespace key – key 列表,不少于 1k value – 增加量 defaultValue – 第一次调用 incr 时的 key 的 count 初始值,第一次回到的值为 defaultValue + value。 expireTime – 数据过期天数,单位为秒,可设相对天数或绝对天数(Unix 天数戳)。 */

2. Redis通过lua脚本实现单纯窗口

与Tair实现方式类似,不过redis的incr()方法不能原子性的设置过期天数,所以需要使用lua脚本,在第一次调用回到1时,设置下过期天数为1秒。

local current current = redis.call(“incr”,KEYS[1]) if tonumber(current) == 1 then redis.call(“expire”,KEYS[1],1) end return current

3. Redis通过lua脚本实现令牌桶

每天允诺令牌时,通过这两个参数和允诺的天数、流速等参数进行计算,返

local ratelimit_info = redis.pcall(HMGET,KEYS[1],last_time,current_token) locallast_time = ratelimit_info[1] local current_token = tonumber(ratelimit_info[2]) local max_token = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) local reverse_time = 1000/token_rateif current_token == nil then current_token = max_token last_time = current_time else localpast_time = current_time-last_timelocal reverse_token = math.floor(past_time/reverse_time) current_token = current_token+reverse_token last_time = reverse_time*reverse_token+last_timeifcurrent_token>max_tokenthen current_token = max_token end end local result = 0 if(current_token>0) then result = 1current_token = current_token-1 end redis.call(HMSET,KEYS[1],last_time,last_time,current_token,current_token) redis.call(pexpire,KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time)))return result

初始化令牌桶lua脚本:

local result=1 redis.pcall(“HMSET”,KEYS[1],“last_mill_second”,ARGV[1],“curr_permits”,ARGV[2],“max_burst”,ARGV[3],“rate”,ARGV[4]) return result

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务