再写一篇tps限流
再写一篇tps限流
各种限流算法的称呼
网上有很多文章介绍限流算法,但是对于这些算法的称呼与描述也是有点难以理解。不管那么多了。我先按我理解的维度梳理一下。
主要维度是:是正向计数还是反向计数。是定点(时间点)重置当前计数器还是每次接口调用时按量调整当前还剩的可用请求数。
通俗理解
正向计数且定点(时间点)重置的流程
+-------------------------+
| init value = 0 |
+-----------+-------------+
|
|
|
+---------------------v----------------------+
| when request arrived |
+------+ detect lastReqTime+interval > currentTime +------+
| | | |
Y +--------------------------------------------+ |
| N
| |
+-----v-----------------------+ +---------------v----------+
| reset value = 0 | | |
+-----------------------------+ | detect value > thresold |
| +-----+ +----+
| | +--------------------------+ |
| N |
| | |
| v |
| +---------+---------+ |
| | | Y
| | value = value + 1 | |
| | | |
| +--------+----------+ |
| | |
| +-------v---------+ +-----------v-----+
+------------------->+ return true | | return false |
+-----------------+ +-----------------+
几个参数解释下:
- value:当前时间段有多少请求进来了,即计数器的值
- interval: 每次刷新计数器的时间间隔
- lastReqTime: 上次请求进来的时间点
- currentTime:当前时间点
- thresold:时间间隔(interval)内请求数的最大阈值
这样设计,如果你的interval设置成1秒钟,thresold设置成1000,那么意思就是每秒限制1000请求数的流控。 相当于tps=1000限流。
反向计数
反向计数计数初始化value时不是初始化成0,而是初始化成thresold(你的限制请求数量的阈值)。
然后每次请求进来的时候 value不是+1而是-1,reset value的时候也是重置成thresold。
定点重置的lua脚本
-- 资源唯一标识
local key = KEYS[1]
-- 时间窗口内最大并发数
local max_permits = tonumber(KEYS[2])
-- 窗口的间隔时间
local interval_milliseconds = tonumber(KEYS[3])
-- 获取的并发数
local permits = tonumber(ARGV[1])
local current_permits = tonumber(redis.call("get", key) or 0)
-- 如果超过了最大并发数,返回false
if (current_permits + permits > max_permits) then
return false
else
-- 增加并发计数
redis.call("incrby", key, permits)
-- 如果key中保存的并发计数为0,说明当前是一个新的时间窗口,它的过期时间设置为窗口的过期时间
if (current_permits == 0) then
redis.call("pexpire", key, interval_milliseconds)
end
return true
end
定点重置存在的问题
不论是正向计数还是反向计数,定点重置都存在一个问题:
0r 1000r 1000r 0r
0s 0.8s 1.0s 1.2 2.0s
+----------------+-----+-----+----------------+------->timeline
^ ^ ^ ^ ^
| | | | |
| | | | |
| | | | |
| | | | |
+ + + + +
假设按上图时间线描述,0-0.8s系统没有收到请求,0.8-1.0s系统收到了1000个请求,1.0-1.2s系统又收到了1000个请求,1.2-2.0s系统收到了0个请求。这种场景其实是能通过上面的定点重置的流控的,但是实际在0.8s-1.2s这0.4s时间内tps达到了2000/(1.2-0.8)=5000的量,没能达到真正意义上的tps限流的述求。
每次接口调用时按量调整当前还剩的可用请求数且反向计数
这里先解释一下按量,按量就是:
假设上一次接口调用到这次接口调用间隔是2s,然后我们是1000tps限流,那么此时按量调整就是2sx1000r/s= +2000r。 也就是此时可用请求数按量加2000个。
整体逻辑比较复杂,先用java代码描述下:
-- key
String key = "流控实例id串"; // 通用代码,可以支持多个流控实例,控制不同的服务
-- 最大存储的令牌数
int max_permits = 10000;
-- 每秒钟产生的令牌数
int permits_per_second = 1000; // tps阈值
-- 请求的令牌数
int required_permits = 1; // 请求数流控每次1个请求,如果是流量流控,可以从外面把这个传进来
// 存储流控逻辑中的数据用, 在redis中用hset代替。 [流控实例id串--> [流程过程中需要的参数-->参数值]]
Map<String, Map<String, Object>> storeMap = new ConcurrentHashMap<String, Map<String, Object>>();
-- 下次请求可以获取令牌的起始时间,初始值为0
long next_free_ticket_micros = storeMap.get(key).get("next_free_ticket_micros") , default: 0 // 取不到就用默认0
-- 当前时间
long now_micros = System.currentTimeMillis();
-- 查询获取令牌是否超时
if (ARGV[1] != null) {
-- 获取令牌的超时时间
long timeout_micros = ARGV[1];
long micros_to_wait = next_free_ticket_micros - now_micros;
if (micros_to_wait > timeout_micros) {
return micros_to_wait
}
}
-- 当前存储的令牌数
long stored_permits = storeMap.get(key).get("stored_permits") , default: 0 // 取不到就用默认0
-- 添加令牌的时间间隔
float stable_interval_micros = 1000 / permits_per_second;
-- 补充令牌
if (now_micros > next_free_ticket_micros) {
/**
* 当前时间 到 下次请求可以获取令牌的起始时间 之间差多少毫秒 就补 多少毫秒/产生单个可用令牌的毫秒数
* 比如 1000tps 则产生1个令牌要1毫秒,假设 上面差50毫秒,那么就可以有50个新令牌可以用
*
**/
long new_permits = (now_micros - next_free_ticket_micros) / stable_interval_micros;
stored_permits = math.min(max_permits, stored_permits + new_permits); // 取最大令牌数 与 存储令牌数+新可用令牌数 小的一个
next_free_ticket_micros = now_micros; // 将当前时间更新为next_free_ticket_micros ,因为有新令牌能用了嘛
}
-- 消耗令牌
long moment_available = next_free_ticket_micros;
long stored_permits_to_spend = math.min(required_permits, stored_permits); // 将要花掉多少令牌, 请求数控制的是1 取小的是因为不能超过可用令牌数
long fresh_permits = required_permits - stored_permits_to_spend; // 这次用掉的,要减掉
long wait_micros = fresh_permits * stable_interval_micros; // fresh_permits > 0 表示申请的令牌不够,则需要等,乘以每个令牌需要的产生时间,就是要等多久
// redis.replicate_commands() // 在redis脚本中调用time会有问题的规避
storeMap.get(key).put("stored_permits", stored_permits - stored_permits_to_spend);
storeMap.get(key).put("next_free_ticket_micros", next_free_ticket_micros + wait_micros);
// redis.call('expire', key, 10) // 每隔10s刷新一下这个流控实例
-- 返回需要等待的时间长度
return moment_available - now_micros;
最后贴一下redis的lua脚本:
-- key
local key = KEYS[1]
-- 最大存储的令牌数
local max_permits = tonumber(KEYS[2])
-- 每秒钟产生的令牌数
local permits_per_second = tonumber(KEYS[3])
-- 请求的令牌数
local required_permits = tonumber(ARGV[1])
-- 下次请求可以获取令牌的起始时间
local next_free_ticket_micros = tonumber(redis.call('hget', key, 'next_free_ticket_micros') or 0)
-- 当前时间
local time = redis.call('time')
local now_micros = tonumber(time[1]) * 1000000 + tonumber(time[2])
-- 查询获取令牌是否超时
if (ARGV[2] ~= nil) then
-- 获取令牌的超时时间
local timeout_micros = tonumber(ARGV[2])
local micros_to_wait = next_free_ticket_micros - now_micros
if (micros_to_wait > timeout_micros) then
return micros_to_wait
end
end
-- 当前存储的令牌数
local stored_permits = tonumber(redis.call('hget', key, 'stored_permits') or 0)
-- 添加令牌的时间间隔
local stable_interval_micros = 1000000 / permits_per_second
-- 补充令牌
if (now_micros > next_free_ticket_micros) then
local new_permits = (now_micros - next_free_ticket_micros) / stable_interval_micros
stored_permits = math.min(max_permits, stored_permits + new_permits)
next_free_ticket_micros = now_micros
end
-- 消耗令牌
local moment_available = next_free_ticket_micros
local stored_permits_to_spend = math.min(required_permits, stored_permits)
local fresh_permits = required_permits - stored_permits_to_spend;
local wait_micros = fresh_permits * stable_interval_micros
redis.replicate_commands()
redis.call('hset', key, 'stored_permits', stored_permits - stored_permits_to_spend)
redis.call('hset', key, 'next_free_ticket_micros', next_free_ticket_micros + wait_micros)
redis.call('expire', key, 10)
-- 返回需要等待的时间长度
return moment_available - now_micros
再写一篇tps限流的更多相关文章
- TPS限流
限流是高可用服务需要具备的能力之一 ,粗暴简单的就像我们之前做的并发数控制.好一点的有tps限流,可用令牌桶等算法实现.<亿级流量网站架构核心技术>一书P67限流详解也有讲.dubbo提供 ...
- 再写一篇ubuntu服务器的环境配置文
三年前写过一篇,但是环境和三年前比已经发生了比较大的变化,于是重新写一篇,自己以后再次配置也比较方便.我个人而言并没有觉得centos比ubuntu好用多少,所以继续选用ubuntu. 一.硬盘分区 ...
- yii验证系统学习记录,基于yiicms(一)写的太长了,再写一篇(二)
项目地址:https://gitee.com/templi/yiicms 感谢七觞酒大神的付出,和免费分享.当然也感谢yii2的开发团队们. 项目已经安全完毕,不知道后台密码,这种背景下,后台无法进去 ...
- SpringBoot 如何进行限流?老鸟们都这么玩的!
大家好,我是飘渺.SpringBoot老鸟系列的文章已经写了四篇,每篇的阅读反响都还不错,那今天继续给大家带来老鸟系列的第五篇,来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算 ...
- dubbo是如何控制并发数和限流的?
ExecuteLimitFilter ExecuteLimitFilter ,在服务提供者,通过 的 "executes" 统一配置项开启: 表示每服务的每方法最大可并行执行请求数 ...
- 【.NET Core项目实战-统一认证平台】第七章 网关篇-自定义客户端限流
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上增加自定义客户端授权功能,从设计到编码实现,一步一步详细讲解,相信大家也掌握了自定义中间件的开发技巧了,本篇我们 ...
- Spring Cloud Alibaba | Sentinel: 服务限流基础篇
目录 Spring Cloud Alibaba | Sentinel: 服务限流基础篇 1. 简介 2. 定义资源 2.1 主流框架的默认适配 2.2 抛出异常的方式定义资源 2.3 返回布尔值方式定 ...
- Spring Cloud Alibaba | Sentinel: 服务限流高级篇
目录 Spring Cloud Alibaba | Sentinel: 服务限流高级篇 1. 熔断降级 1.1 降级策略 2. 热点参数限流 2.1 项目依赖 2.2 热点参数规则 3. 系统自适应限 ...
- .net core使用ocelot---第四篇 限流熔断
简介 .net core使用ocelot---第一篇 简单使用 .net core使用ocelot---第二篇 身份验证 .net core使用ocelot---第三篇 日志记录 前几篇文章我们陆续介 ...
随机推荐
- 软件测试Lab 1 Junit and Eclemma
首先安装eclipse 然后下载hamcrest-core-1.3.jar,下载地址:http://mvnrepository.com/artifact/org.hamcrest/hamcrest-c ...
- 【Java/Android性能优5】 Android ImageCache图片缓存,使用简单,支持预取,支持多种缓存算法,支持不同网络类型,扩展性强
本文转自:http://www.trinea.cn/android/android-imagecache/ 主要介绍一个支持图片自动预取.支持多种缓存算法.支持二级缓存.支持数据保存和恢复的图片缓存的 ...
- OSS基本概念介绍
存储空间(Bucket): 存储空间是用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间. 可以设置和修改存储空间属性用来控制地域.访问权限.生命周期等,这些属性设置直接作用于该存 ...
- hdu-1179 Ollivanders: Makers of Fine Wands since 382 BC.---二分图匹配模板
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1179 题目大意: 有n个人要去买魔杖,有m根魔杖(和哈利波特去买魔杖的时候一样,是由魔杖选人).接下 ...
- php 单例模式笔记
<?php /** * 单例模式1. 它们必须拥有一个构造函数,并且必须被标记为private2. 它们拥有一个保存类的实例的静态成员变量3. 它们拥有一个访问这个实例的公共的静态方法单例类不能 ...
- 通过cmd查看环境变量名对应的环境变量值
在VS环境中通常要添加路径,不过基本都是按照往上提供的方法添加变量名形如:$(VC_IncludePath),但是如何通过cmd命令找到真正的路径呢 未完待续……
- java编程基础——栈压入和弹出序列
题目描述 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序.假设压入栈的所有数字均不相等.例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压 ...
- pm2 服务器命令
1..配置日志文件路径 命令:pm2 start /home/admin/node/fotonIp/bin/www --name ip -i 4 -o "/app/node/logs ...
- 第五篇:selenium调用IE问题(Protected Mode settings are not the same for all zones)
代码信息: driver = webdriver.Ie()driver.get('http://www.baidu.com') 问题描述: raise exception_class(message, ...
- maven引入dubbo包后启动报错
启动后报错内容为: Caused by: org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exce ...