wmproxy

wmproxy将用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,后续将实现websocket代理, 内外网穿透等, 会将实现过程分享出来, 感兴趣的可以一起造个轮子法

项目地址

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

为什么我们需要主动

  主动可以让我们掌握好系统的稳定性,假设我们有一条连接不可达,连接超时的判定是5秒,需要检测失败3次才认定为失败,那么此时从我们开始检测,到判定失败需要耗时15秒。

  如果此时我们是个高并发的系统,每秒的QPS是1000,我们有三个地址判定,那么此时我们有1/3的失败概率。那么在15秒内,我们会收到15000个请求,会造成5000个请求失败,如果是重要的数据,我们会丢失很多重要数据。

  如果此时客户端拥有重试机制,那么客户端在失败的时候会发起重试,而且系统可能会反复的分配到那台不可达的系统,将会造成短时间内请求数激增,可能引发系统的雪崩。

  所以此时我们主动知道目标端的系统稳定性极其重要。

网络访问示意图

以下是没有主动健康检查

sequenceDiagram
participant 客户端
participant 代理服务器
客户端->>代理服务器: 请求数据(0.5s)
代理服务器->>后端1: 连接并请求数据(5s)失败
Note right of 后端1: 机器宕机不可达

代理服务器-->>客户端: 返回失败0.5s(总耗时6s)
客户端->>代理服务器: 重新请求数据(0.5s)
代理服务器->>后端2: 请求数据成功(0.2s)
后端2-->>代理服务器: 返回数据成功(0.2s)
代理服务器-->> 客户端: 返回数据成功0.5s(总耗时1.4s)

如果出错的时候,一个请求的平均时长可能会达到(1.4s + 5s) / 2 = (3.2s),比正常访问多了(3.2 - 1.4) = 1.8s,节点的宕机会对系统的稳定性产生较大的影响

以下是主动健康检查,它保证了访问后端服务器组均是正常的状态

sequenceDiagram
客户端->>代理服务器: 请求数据(0.5s)
loop 健康检查
代理服务器->>服务器组(只访问1): 定时请求,保证存活,1检查成功,2检查失败
end
Note right of 服务器组(只访问1): 处理客户端数据
代理服务器 -->> 服务器组(只访问1): 请求数据(0.2s)
服务器组(只访问1) -->> 代理服务器: 返回数据成功(0.2s)
代理服务器-->>客户端: 返回数据成功(0.5s)(总耗时1.4s)

服务器2出错的时候,主动检查已经检查出服务器2不可用,负载均衡的时候选择已经把服务器2摘除,所以系统的平均耗时1.4s,系统依然保持稳定

健康检查的种类

在目前的系统中有以下两分类:

  • HTTP 请求特定的方法及路径,判断返回是否得到预期的status或者body
  • TCP 仅只能测试连通性,如果能连接表示正常,会出现能连接但无服务的情况

健康检查的准备

我们需要从配置中读出所有的需要健康检查的类型,即需要去重,把同一个指向的地址过滤掉

配置有可能被重新加载,所以我们需要预留发送配置的方式(或者后续类似nginx用新开进程的方式则不需要),此处做一个预留。

  • 如何去重

    像这种简单级别的去重通常用HashSet复杂度为O(1)或者用简单的Vec复杂度为O(n),以SocketAddr的为键值,判断是否有重复的数据。

  • 如何保证不影响主线程

    把健康请求的方法移到异步函数,用tokio::spawn中处理,在健康检查的情况下保证不影响其它数据处理

  • 如果同时处理多个地址的健康检查

    每一次健康检查都会在一个异步函数中执行,在我们调用完请求后,我们会对当前该异步进行tokio::time::sleep以让出当前CPU。

  • 如何按指定间隔时间请求

    因为每一次健康请求都是在异步函数中,我们不确认之前的异步是否完成,所以我们在每次请求前都记录last_request,我们在请求前调用HealthCheck::check_can_request判断当前是否可以发送请求来保证间隔时间内不多次请求造成服务器的压力。

  • 超时连接判定处理

    利用tokio::time::timeoutfuture做组合,等超时的时候直接按错误处理

部分实现源码

主要源码定义在check/active.rs中,主要的定义两个类

/// 单项健康检查
#[derive(Debug, Clone)]
pub struct OneHealth {
/// 主动检查地址
pub addr: SocketAddr,
/// 主动检查方法, 有http/https/tcp等
pub method: String,
/// 每次检查间隔
pub interval: Duration,
/// 最后一次记录时间
pub last_record: Instant,
}
/// 主动式健康检查
pub struct ActiveHealth {
/// 所有的健康列表
pub healths: Vec<OneHealth>,
/// 接收健康列表,当配置变更时重新载入
pub receiver: Receiver<Vec<OneHealth>>,
}

我们在配置的时候获取所有需要主动检查的数据

/// 获取所有待健康检查的列表
pub fn get_health_check(&self) -> Vec<OneHealth> {
let mut result = vec![];
let mut already: HashSet<SocketAddr> = HashSet::new();
if let Some(proxy) = &self.proxy {
// ...
} if let Some(http) = &self.http {
// ...
}
result
}

主要的检查源码,所有的最终信息都落在HealthCheck中的静态变量里:

pub async fn do_check(&self) -> ProxyResult<()> {
// 防止短时间内健康检查的连接过多, 做一定的超时处理, 或者等上一条消息处理完毕
if !HealthCheck::check_can_request(&self.addr, self.interval) {
return Ok(())
}
if self.method.eq_ignore_ascii_case("http") {
match tokio::time::timeout(self.interval + Duration::from_secs(1), self.connect_http()).await {
Ok(r) => match r {
Ok(r) => {
if r.status().is_server_error() {
log::trace!("主动健康检查:HTTP:{}, 返回失败:{}", self.addr, r.status());
HealthCheck::add_fall_down(self.addr);
} else {
HealthCheck::add_rise_up(self.addr);
}
}
Err(e) => {
log::trace!("主动健康检查:HTTP:{}, 发生错误:{:?}", self.addr, e);
HealthCheck::add_fall_down(self.addr);
}
},
Err(e) => {
log::trace!("主动健康检查:HTTP:{}, 发生超时:{:?}", self.addr, e);
HealthCheck::add_fall_down(self.addr);
},
}
} else {
match tokio::time::timeout(Duration::from_secs(3), self.connect_http()).await {
Ok(r) => {
match r {
Ok(_) => {
HealthCheck::add_rise_up(self.addr);
}
Err(e) => {
log::trace!("主动健康检查:TCP:{}, 发生错误:{:?}", self.addr, e);
HealthCheck::add_fall_down(self.addr);
}
}
}
Err(e) => {
log::trace!("主动健康检查:TCP:{}, 发生超时:{:?}", self.addr, e);
HealthCheck::add_fall_down(self.addr);
}
}
}
Ok(())
}

结语

主动检查可以及时的更早的发现系统中不稳定的因素,是系统稳定性的基石,也可以通过更早的发现因素来通知运维介入,我们的目的是使系统更稳定,更健壮,处理延时更少。

点击 [关注][在看][点赞] 是对作者最大的支持

18. 从零开始编写一个类nginx工具, 主动式健康检查源码实现的更多相关文章

  1. 从零开始编写一个BitTorrent下载器

    从零开始编写一个BitTorrent下载器 BT协议 简介 BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P).曾经风靡一时,由于它引起了巨大的流量 ...

  2. 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。

    22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...

  3. 35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n); (2)编写一个类:ClassA来实现接口InterfaceA,实现int method(int n)接口方 法时,要求计算1到n的和; (3)编写另一个类:ClassB来实现接口InterfaceA,实现int method(int n)接口 方法时,要求计算n的阶乘(n

      35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n): (2)编写一个类:ClassA来实现接口InterfaceA,实现in ...

  4. 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的顺序输出,如果传入的是一个字符串,就将字符串反序输出。

    namespace test2 { class Program { /// <summary> /// 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的 ...

  5. 二、 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)

    请指教交流! package com.it.hxs.c01; import java.util.Stack; /* 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek) */ ...

  6. 如何编写一个SQL注入工具

    0x01  前言 一直在思考如何编写一个自动化注入工具,这款工具不用太复杂,但是可以用最简单.最直接的方式来获取数据库信息,根据自定义构造的payload来绕过防护,这样子就可以. 0x02 SQL注 ...

  7. 从零开始编写一个vue插件

    title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...

  8. 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法

    作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ...

  9. 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

    目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...

  10. nginx限流&健康检查

    Nginx原生限流模块: ngx_http_limit_conn_module模块 根据前端请求域名或ip生成一个key,对于每个key对应的网络连接数进行限制. 配置如下: http模块   ser ...

随机推荐

  1. 力扣 (LeetCode)算法入门——Day1

    704. 二分查找 题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1. ...

  2. 谷歌语法的基础知识&FOFA

    谷歌语法 谷歌语法基础符号: "xxx":表示完全匹配,即关键字不能分开,顺序也不能变 +:"xxx"+www.baidu.com  搜索xxx与baidu.c ...

  3. 前端Vue组件之仿京东拼多多领取优惠券弹出框popup 可用于电商商品详情领券场景使用

    随着技术的发展,开发的复杂度也越来越高,传统开发方式将一个系统做成了整块应用,经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改,造成牵一发而动全身.通过组件化开发,可以有效 ...

  4. Centos7中搭建Redis6集群操作步骤

    目录 下载安装包 解压安装装包 安装依赖 安装 创建目录 设置配置文件 创建启动服务 制作启动文件 启动并验证Redis 开放防火墙端口 创建集群 集群其他操作 注意 下载安装包 # 进入软件下载目录 ...

  5. ASP.NET MVC4 学习笔记-3

    创建一个简单的数据录入程序--Create a Simple Data-Entry Application 在这篇博客中,我们将通过创建一个简单的数据录入程序来探索MVC的其他特点.在这一节中我们要跟 ...

  6. base64详解

    base64详解 前置知识 位与字节 二进制系统中,每个0或1就是一个位(bit,比特),也叫存储单元,位是数据存储的最小单位. 其中8bit就称为一个字节(Byte). 1B=8位 位运算 与运算: ...

  7. 我学到的一下vue使用技巧

    这两天学到的vue使用技巧 v-if , 当封装组件的时候,用到的props,最外层最好加个v-if,防止出现cannot read property of undefined 这样的错误,如果pro ...

  8. 文件上传的multipart/form-data属性,你理解了吗

    form表单经常用于前端发送请求,比如:用户填写信息.选择数据.上传文件,对于不同的场景,上传数据的格式也会有些区别. action action 表示该请求的 url 地址,定义在form上,请求的 ...

  9. 不要再傻傻分不清 hash、 chunkhash 和 contenthash 啦

    hash.contenthash 和 chunkhash 是通过散列函数处理之后,生成的一串字符,可用于区分文件. 文件名不带哈希值 webpack.config.js 文件中,output 中定义输 ...

  10. shell命令-lsof

    前言 lsof是系统管理常用命令,其名指的是list open files,列出打开的文件,而在linux系统,一切皆文件. centos7安装:yum install -y lsof 获取网络信息 ...