高并发场景下常见的限流算法及方案介绍
来源 | OSCHINA 社区
作者 | 京东云开发者—京东科技 康志兴
原文链接:https://my.oschina.net/u/4090830/blog/8367252
应用场景
极致的优化,就是将硬件使用率提高到 100%,但永远不会超过 100%
常用限流算法
1. 计数器
2. 漏桶
3. 令牌桶
漏桶和令牌桶算法的区别:
限流方案
一、容器限流
1. Tomcat
maxThreads
是 Tomcat 的最大线程数,当请求的并发大于 maxThreads
时,请求就会排队执行 (排队数设置:accept-count),这样就完成了限流的目的。<Connectorport="8080"protocol="HTTP/1.1"
connectionTimeout="20000"
maxThreads="150"
redirectPort="8443"/>
2. Nginx
控制速率 我们需要使用 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 的。 并发连接数 利用 limit_conn_zone
和 limit_conn
两个指令即可控制并发数,示例配置如下 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;
}
二、服务端限流
1. Semaphore
Semaphore sp = new Semaphore(3);
sp.require(); // 阻塞获取
System.out.println("执行业务逻辑");
sp.release();
2. RateLimiter
create()
创建一个桶,然后通过 acquire()
或者 tryAcquire()
获取令牌:RateLimiter rateLimiter = RateLimiter.create(5); // 初始化令牌桶,每秒往桶里存放5个令牌
rateLimiter.acquire(); // 自旋阻塞获取令牌,返回阻塞的时间,单位为秒
rateLimiter.tryAcquire(); // 获取令牌,返回布尔结果,超过超时时间(默认为0,单位为毫秒)则返回失败
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 初始化时桶是空的,直接从空桶获取5个令牌
* 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
线程池:每个 command 运行在一个线程中,限流是通过线程池的大小来控制的 信号量:command 是运行在调用线程中,但是通过信号量的容量来进行限流
// HelloWorldHystrixCommand要使用Hystrix功能
public classHelloWorldHystrixCommandextendsHystrixCommand{
private final String name;
publicHelloWorldHystrixCommand(String name){
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
// 如果继承的是HystrixObservableCommand,要重写Observable construct()
@Override
protectedStringrun(){
return "Hello " + name;
}
}
调用该 command:
String result = new HelloWorldHystrixCommand("HLX").execute();
System.out.println(result); // 打印出Hello HLX
4. Sentinel
@SentinelResource(String name)
或者手动调用 SphU.entry(String name)
方法开启流控。@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
三、分布式下限流方案
1. Tair 通过 incr 方法实现简单窗口
incr()
自增方法来计数并与阈值进行大小比较。public boolean tryAcquire(String key) {
// 以秒为单位构建tair的key
String wrappedKey = wrapKey(key);
// 每次请求+1,初始值为0,key的有效期设置5s
Result<Integer> result = tairManager.incr(NAMESPACE, wrappedKey, 1, 0, 5);
return result.isSuccess() && result.getValue() <= threshold;
}
private String wrapKey(String key) {
long sec = System.currentTimeMillis() / 1000L;
return key + ":" + sec;
}
【备注】incr 方法的参数说明
// 方法定义:
Result incr(int namespace, Serializable key,int value,int defaultValue,int expireTime)
/* 参数含义:
namespace - 申请时分配的 namespace
key - key 列表,不超过 1k
value - 增加量
defaultValue - 第一次调用 incr 时的 key 的 count 初始值,第一次返回的值为 defaultValue + value。
expireTime - 数据过期时间,单位为秒,可设相对时间或绝对时间(Unix 时间戳)。
*/
2. Redis 通过 lua 脚本实现简单窗口
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 脚本实现令牌桶
实现思路是获取令牌后,用 SET 记录 “请求时间” 和 “剩余 token 数量”。
每次请求令牌时,通过这两个参数和请求的时间、流速等参数进行计算,返回是否获取令牌成功。
获取令牌 lua 脚本:
local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token')
local last_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_rate
if current_token == nil then
current_token = max_token
last_time = current_time
else
local past_time = current_time-last_time
local reverse_token = math.floor(past_time/reverse_time)
current_token = current_token+reverse_token
last_time = reverse_time*reverse_token+last_time
if current_token>max_token then
current_token = max_token
end
end
local result = 0
if(current_token>0) then
result = 1
current_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
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
往期推荐
Ambient:Rust编写的高性能多人游戏引擎
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦
微信扫码关注该文公众号作者
戳这里提交新闻线索和高质量文章给我们。
来源: qq
点击查看作者最近其他文章