31. 干货系列从零用Rust编写正反向代理,HTTP限流的实现(limit_req)
wmproxy
wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
HTTP限流
HTTP限流是在HTTP请求处理过程中,对请求进行限制的一种技术手段。其目的是防止系统过载,保护系统的稳定性和可用性。HTTP限流可以基于不同的策略和方法,例如基于时间窗口、令牌桶、漏桶等。
常见的HTTP限流方法包括:
- 基于时间窗口:这种方法将一段时间划分为若干个时间窗口,每个时间窗口内只允许一定数量的请求通过。例如,每秒只允许20个请求通过。
- 令牌桶:令牌桶算法允许突发流量,只要有令牌就可以处理请求,当没有令牌时,请求就被拒绝。这种方法适用于处理突发流量的情况。和时间窗口结合时,如果当前时间段已经有20个请求,此时触发令牌桶
brust,将当前的流量进行延时处理。 - 漏桶:漏桶算法不允许突发流量,无论何时都只能按照一定的速率处理请求。这种方法适用于处理稳定流量的情况。
在进行HTTP限流时,需要考虑系统的实际情况和需求,选择合适的限流策略和方法。同时,还需要对系统的性能和负载进行充分的测试和评估,以确保系统的稳定性和可用性。
方案选择
在此项目中,选择的是基于时间窗口及令牌桶做组合使用进行限制,以下做个例子,配置
limit="rate=10r/s brust=10"
效果将是每秒钟限制10条请求,可以允许突发的10个令牌桶做一秒的延时,在下一秒允许通行。
participant C
participant S
C->>S: 第一秒请求数据10条
Note right of S: 当前记录请求10条
S->>C: 返回成功
C->>S: 第一秒继续请求数据1条
Note right of S: 当前记录请求10条+1条令牌桶
S->>C: 延时一秒再进行后续处理返回成功
C->>S: 第一秒继续请求数据9条
Note right of S: 当前记录请求10条+10条令牌桶
S->>C: 延时一秒再进行后续处理返回成功
C->>S: 第一秒继续请求数据(1条-N条)
Note right of S: 当前记录当前秒已满,返回拒绝
S->>C: 直接返回错误,频率过快,已超时
C-->>S: 第二秒请求数据1条
Note right of S: 清除第一秒的记录10条<br>10条令牌桶转化第二秒10条记录<br>故当前为请求数10条+1条令牌桶
S->>C: 延时一秒过时行后续处理返回成功
C-->>S: 第三秒请求数据1条
Note right of S: 清除第二秒的记录10条<br>1条令牌桶转化第三秒1条记录<br>故当前为请求数1条
S->>C: 直接返回成功
以上是时序加令牌的请求数据和返回情况
限流配置
类似于nginx中的limit_req配置,分为limit_req_zone及limit_req两部分,可分为两个类,一个为zone,一个为关联到zone名称的具体项目
#[derive(Debug, Clone)]
pub struct LimitReqZone {
/// 键值的匹配方式
pub key: String,
/// IP个数
pub limit: u64,
/// 周期内可以通行的数据
pub nums: u64,
/// 每个周期的时间
pub per: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LimitReq {
zone: String,
burst: u64,
}
然后在http的根目录下配置当前的zone空间,为一个HashMap结构,可以配置多种zone结构
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpConfig {
// ...
#[serde_as(as = "HashMap<_, DisplayFromStr>")]
#[serde(default = "HashMap::new")]
pub limit_req_zone: HashMap<String, LimitReqZone>,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommonConfig {
// ...
#[serde_as(as = "Option<DisplayFromStr>")]
pub limit_req: Option<LimitReq>,
}
因为并不是任何的请求都要进行限流,所以此处为Option,如果子级未配置,父级有配置,子级将会应用父级的配置。
以下展示在toml格式的配置
# 反向代理相关,七层协议为http及https
[http]
[http.limit_req_zone]
limit = "{client_ip} limit=10m rate=10r/s"
less = "{client_ip} limit=10m rate=10r/min"
# 反向代理中的具体服务,可配置多个多组
[[http.server]]
bind_addr = "0.0.0.0:82"
server_name = "soft.wm-proxy.com"
limit_req = "zone=limit brust=10"
# 按请求路径进行rule匹配,可匹配method,看具体的处理的内容如文件服务或者负载均衡
[[http.server.location]]
limit_req = "zone=less brust=1"
rule = "/root"
file_server = { browse = true }
[[http.server.location]]
rule = "/api"
file_server = { browse = true }
这样子就可以实现api不同的进行不同的限速方案,可以实现更好的通用效果。
配置解析
- LimitReqZone解析
需要将"{client_ip} limit=10m rate=10r/s"转成LimitReqZone结构,此处我们用的是FromStr接口,用空格分割,第一个字段为key,后续用=做分割,得取相应的值
impl FromStr for LimitReqZone {
type Err = ProxyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v = s.split(" ").collect::<Vec<&str>>();
let key = v[0].to_string();
let mut limit = 0;
let mut nums = 0;
let mut per = Duration::new(0, 0);
for idx in 1..v.len() {
let key_value = v[idx].split("=").map(|k| k.trim()).collect::<Vec<&str>>();
if key_value.len() <= 1 {
return Err(ProxyError::Extension("未知的LimitReq"));
}
match key_value[0] {
"limit" => {
let s = ConfigSize::from_str(key_value[1])?;
limit = s.0;
}
"rate" => {
let rate_key = key_value[1]
.split("/")
.map(|k| k.trim())
.collect::<Vec<&str>>();
if rate_key.len() == 1 {
return Err(ProxyError::Extension("未知的LimitReq"));
}
let rate = rate_key[0].trim_end_matches("r");
nums = rate
.parse::<u64>()
.map_err(|_e| ProxyError::Extension("parse error"))?;
let s = ConfigDuration::from_str(rate_key[1])?;
per = s.0;
}
_ => {
return Err(ProxyError::Extension("未知的LimitReq"));
}
}
}
Ok(LimitReqZone::new(key, limit, nums, per))
}
}
- LimitReq解析
需要将"zone=less brust=1"转成LimitReq结构,此处我们用的是FromStr接口,用空格分割,将每个值用=做分割,得取相应的值
impl FromStr for LimitReq {
type Err = ProxyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v = s.split(" ").collect::<Vec<&str>>();
let mut zone = String::new();
let mut brust = 0;
for idx in 0..v.len() {
let key_value = v[idx].split("=").map(|k| k.trim()).collect::<Vec<&str>>();
if key_value.len() <= 1 {
return Err(ProxyError::Extension("未知的LimitReq"));
}
match key_value[0] {
"zone" => {
zone = key_value[1].to_string();
}
"brust" => {
brust = key_value[1]
.parse::<u64>()
.map_err(|_e| ProxyError::Extension("parse error"))?;
}
_ => {
return Err(ProxyError::Extension("未知的LimitReq"));
}
}
}
Ok(LimitReq::new(zone, brust))
}
}
限制实现
首先我们配置一个静态可访问的全局变量,因为所有的线程操作都需要汇总到此时判定是否合格
每个命名空间里,都将存储不超过规格数据的IP,如果超过将直接返回失败
pub struct LimitReqData {
/// 记录所有的ip数据的限制情况
ips: HashMap<String, InnerLimit>,
/// IP个数
limit: u64,
/// 周期内可以通行的数据
nums: u64,
/// 每个周期的时间
per: Duration,
/// 最后清理IP的时间
last_remove: Instant,
}
全局静态数据
lazy_static! {
static ref GLOABL_LIMIT_REQ: RwLock<HashMap<&'static str, LimitReqData>> =
RwLock::new(HashMap::new());
}
返回结果
#[derive(Debug)]
pub enum LimitResult {
Ok,
Refuse,
Delay(Duration),
}
所以的判断是否通过,我们将通过以下函数返回相应的结果,从而使外部的函数可以进行相应的处理。
impl LimitReqData {
pub fn recv_new_req(key: &str, ip: &String, burst: u64) -> ProtResult<LimitResult> {
let mut write = GLOBAL_LIMIT_REQ
.write()
.map_err(|_| ProtError::Extension("unlock error"))?;
if !write.contains_key(&*key) {
return Ok(LimitResult::Ok);
}
write.get_mut(key).unwrap().inner_recv_new_req(ip, burst)
}
}
小结
我们通过全局共享数据,需要加锁获取该数据,来判定整体的KEY的流量情况,可能是IP,可能是IP+Cookie等,来灵活的针对用户限流还是针对IP限流或者其它的业务情况进行合理的安排。
点击 [关注],[在看],[点赞] 是对作者最大的支持
31. 干货系列从零用Rust编写正反向代理,HTTP限流的实现(limit_req)的更多相关文章
- (转)Spring Boot干货系列:(七)默认日志logback配置解析
转:http://tengj.top/2017/04/05/springboot7/ 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候,是带着下面几个问题来查资料的, ...
- (转)Spring Boot干货系列:(四)开发Web应用之Thymeleaf篇
转:http://tengj.top/2017/03/13/springboot4/ 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boot对Web开发的支持. 正文 Sp ...
- 【转】Spring Boot干货系列:(一)优雅的入门篇
转自Spring Boot干货系列:(一)优雅的入门篇 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做 ...
- Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用
Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用 原创 2017-04-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章介绍了一些基础,但都是静 ...
- Spring Boot干货系列:(七)默认日志框架配置
Spring Boot干货系列:(七)默认日志框架配置 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候, ...
- Spring Boot干货系列:(五)开发Web应用JSP篇
Spring Boot干货系列:(五)开发Web应用JSP篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 上一篇介绍了Spring Boot中使用Thymeleaf模板引擎,今天 ...
- Spring Boot干货系列:(四)Thymeleaf篇
Spring Boot干货系列:(四)Thymeleaf篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boo ...
- Spring Boot干货系列:(一)优雅的入门篇
Spring Boot干货系列:(一)优雅的入门篇 2017-02-26 嘟嘟MD 嘟爷java超神学堂 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社 ...
- Spring Boot干货系列:(十二)Spring Boot使用单元测试(转)
前言这次来介绍下Spring Boot中对单元测试的整合使用,本篇会通过以下4点来介绍,基本满足日常需求 Service层单元测试 Controller层单元测试 新断言assertThat使用 单元 ...
- 【WEB API项目实战干货系列】- 导航篇(十足干货分享)
在今天移动互联网的时代,作为攻城师的我们,谁不想着只写一套API就可以让我们的Web, Android APP, IOS APP, iPad APP, Hybired APP, H5 Web共用共同的 ...
随机推荐
- Nep2023的wp
0x00 闲言碎语 2023.8.14 记录11-13的紧张刺激.46名结赛. 非常高兴能够参加NepCTF2023,以一个初出茅庐的新人的身份参加.ctf的乐趣在于学习和探索,同时我也有想证明自己的 ...
- papricice
2023-07-14 题目 题目传送门 题目大意 给定一个 \(n\) 个点的树,这 \(n\) 个点编号为 \(1\) 到 \(n\). 现在要选择断掉两条边,会形成三个连通块,假设这三个连通块内的 ...
- 【干货】华为云图数据库GES技术演进
本文分享自华为云社区<[干货]华为云图数据库GES技术演进>,作者: Chenyi. 1 背景 大规模图数据无处不在,图查询.分析和表示学习已成为大数据和AI的核心部分之一.特别是知识图谱 ...
- 如何分析 JVM 内存瓶颈浅谈
背景: 当操作系统内存出现瓶颈时,我们便会重点排查那个应用占用内存过大.对于更深一步分析内存的使用,就进一步去了解内存结构,应用程序使用情况,以及内存如何分配.如何回收,这样你才能更好地确定内存的问题 ...
- 算法笔记_python
目录 算法 概念 时间复杂度 空间复杂度 递归原理 顺序查找 二分查找 列表排序 LowB 三人组 冒泡排序 选择排序 插入排序 NB三人组 快速排序 堆排序 归并排序 NB三人组小结 总结 其他排序 ...
- 华为云ECS上搭建Hadoop集群环境启动时报错“java.net.BindException: Cannot assign requested address”问题的解决
启动时使用: ./sbin/start-all.sh 1 报错: java.net.BindException: Problem binding to [test7972:9000] java.net ...
- 二叉树(binary tree)
二叉树(binary tree) 二叉树(Binary Tree)是一种常见的树状数据结构,它由一组节点组成,每个节点最多有两个子节点,分别称为左子节点和右子节点.二叉树具有以下特点: 每个节点最多有 ...
- DAY005_异或运算
运算规则 二进制:相同为0 相异为1 十进制:相同为0 任何数字和0异或都是它本身 不利用额外变量交换两个数 数组中一种数字出现了奇数次,其他数都出现了偶数次,怎么得到这个出现了奇数次的数 将所有的数 ...
- 【译】ASP.NET Core在 .NET Core 3.1 Preview 1中的更新
.NET Core 3.1 Preview 1现在可用.此版本主要侧重于错误修复,但同时也包含一些新功能. 这是此版本的ASP.NET Core的新增功能: 对Razor components的部分类 ...
- 开发app软件成本计算参考
目录 1. 设计成本 2. 前端开发成本 3. 后端开发成本 4. 测试成本 5. 上架试运营成本 app软件开发已成为人们生活中不可或缺的一部分.无论是在娱乐.通讯.信息.健康等方面,都有数不清的a ...