11. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 实现健康检查

项目 ++wmproxy++

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

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

健康检查的意义

健康检查维持着系统的稳定运行, 极大的加速着服务的响应时间, 并保证服务器不会把消息包转发到不能响应的服务器上, 从而使系统快速稳定的运转

在LINUX系统中,系统默认TCP建立连接超时时间为127秒。通常网络不可达或者网络连接被拒绝或者网络连接超时需要耗时的时长较长。此时会超成服务器的响应时间变长很多,而且重复发起不可达的连接尝试也在耗着大量的IO资源。

当健康检查介入后,如果短时间内多次建立连接失败,则暂时判定该地址不可达,状态设置为不可达。如果此时接收到该地址的请求时直接返回错误。大大提高了响应的时间。

所以健康检查是必不可少的存在。

如何实现

由于健康状态需要调用的地方可能在任意处需要发起连接的地方,如果通过参数透传也会涉及到多线程的数据共用,如Arc<Mutex<Data>>,取用的时候也是要通过锁共用,且编码的复杂度和理解成本急剧升高,所以此处健康检查选用的是多线程共用的静态处理变量。

Rust中的静态变量

在Rust中,全局变量可以分为两种:

  • 编译期初始化的全局变量
  • 运行期初始化的全局变量

编译期初始化的全局变量有:

const创建的常量,如 const MAX_ID:usize=usize::MAX/2;
static创建的静态变量,如 static mut REQUEST_RECV:usize=0;

运行期初始化的全局变量有lazy_static用于懒初始化。例如:

lazy_static! {
static ref HEALTH_CHECK: RwLock<HealthCheck> = RwLock::new(HealthCheck::new(60, 3, 2));
}

此外还有

  • 实现你自己的运行时初始化:std::sync::Once + static mut T
  • 单线程运行时初始化的特殊情况:thread_local

我们此处维持一个HealthCheck的全局变量,因为程序是多线程,用thread_local,无法共用其它线程的检测,不条例预期,所以此处用读写锁来保证全局变量的正确性,读写锁的特点是允许存在多个读,但如果获取写必须保证唯一。

源码解析,暂时不做主动性的健康检查

接下来我们看HealthCheck的定义

pub struct HealthCheck {
/// 健康检查的重置时间, 失败超过该时间会重新检查, 统一单位秒
fail_timeout: usize,
/// 最大失败次数, 一定时间内超过该次数认为不可访问
max_fails: usize,
/// 最小上线次数, 到达这个次数被认为存活
min_rises: usize,
/// 记录跟地址相关的信息
health_map: HashMap<SocketAddr, HealthRecord>,
} /// 每个SocketAddr的记录值
struct HealthRecord {
/// 最后的记录时间
last_record: Instant,
/// 失败的恢复时间
fail_timeout: Duration,
/// 当前连续失败的次数
fall_times: usize,
/// 当前连续成功的次数
rise_times: usize,
/// 当前的状态
failed: bool,
}

主要有最后记录时间,失败次数,成功次数,最大失败惩罚时间等元素组成

我们通过函数is_fall_down判定是否是异常状态,未检查前默认为正常状态,超出一定时间后,解除异常状态。

/// 检测状态是否能连接
pub fn is_fall_down(addr: &SocketAddr) -> bool {
// 只读,获取读锁
if let Ok(h) = HEALTH_CHECK.read() {
if !h.health_map.contains_key(addr) {
return false;
}
let value = h.health_map.get(&addr).unwrap();
if Instant::now().duration_since(value.last_record) > value.fail_timeout {
return false;
}
h.health_map[addr].failed
} else {
false
}
}

如果连接TCP失败则调用add_fall_down将该地址失败连接次数+1,如果失败次数达到最大失败次数将状态置为不可用。

/// 失败时调用
pub fn add_fall_down(addr: SocketAddr) {
// 需要写入,获取写入锁
if let Ok(mut h) = HEALTH_CHECK.write() {
if !h.health_map.contains_key(&addr) {
let mut health = HealthRecord::new(h.fail_timeout);
health.fall_times = 1;
h.health_map.insert(addr, health);
} else {
let max_fails = h.max_fails;
let value = h.health_map.get_mut(&addr).unwrap();
// 超出最大的失败时长,重新计算状态
if Instant::now().duration_since(value.last_record) > value.fail_timeout {
value.clear_status();
}
value.last_record = Instant::now();
value.fall_times += 1;
value.rise_times = 0; if value.fall_times >= max_fails {
value.failed = true;
}
}
}
}

如果连接TCP成功则调用add_rise_up将该地址成功连接次数+1,如果成功次数达到最小次数将状态置为不可用。

/// 成功时调用
pub fn add_rise_up(addr: SocketAddr) {
// 需要写入,获取写入锁
if let Ok(mut h) = HEALTH_CHECK.write() {
if !h.health_map.contains_key(&addr) {
let mut health = HealthRecord::new(h.fail_timeout);
health.rise_times = 1;
h.health_map.insert(addr, health);
} else {
let min_rises = h.min_rises;
let value = h.health_map.get_mut(&addr).unwrap();
// 超出最大的失败时长,重新计算状态
if Instant::now().duration_since(value.last_record) > value.fail_timeout {
value.clear_status();
}
value.last_record = Instant::now();
value.rise_times += 1;
value.fall_times = 0; if value.rise_times >= min_rises {
value.failed = false;
}
}
}
}

接下来我们将TcpStream::connect函数统一替换成HealthCheck::connect外部修改几乎为0,可实现开启健康检查,后续还会有主动式的健康检查。

pub async fn connect<A>(addr: &A) -> io::Result<TcpStream>
where
A: ToSocketAddrs,
{
let addrs = addr.to_socket_addrs()?;
let mut last_err = None; for addr in addrs {
// 健康检查失败,直接返回错误
if Self::is_fall_down(&addr) {
last_err = Some(io::Error::new(io::ErrorKind::Other, "health check falldown"));
} else {
match TcpStream::connect(&addr).await {
Ok(stream) =>
{
Self::add_rise_up(addr);
return Ok(stream)
},
Err(e) => {
Self::add_fall_down(addr);
last_err = Some(e)
},
}
}
} Err(last_err.unwrap_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"could not resolve to any address",
)
}))
}

效果

在前三次请求的时候,将花费5秒左右才抛出拒绝链接的错误

connect server Err(Os { code: 10061, kind: ConnectionRefused, message: "由于目标计算机积极拒绝,无
法连接。" })

可以发现三次之后,将会快速的抛出错误,达成健康检查的目标

connect server Err(Custom { kind: Other, error: "health check falldown" })

此时被动式的健康检查已完成,后续按需要的话将按需看是否实现主动式的健康检查。

11. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 实现健康检查的更多相关文章

  1. 借助FRP反向代理实现内网穿透

    一.frp 是什么? frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP.UDP.HTTP.HTTPS 等多种协议.可以将内网服务以安全.便捷的方式通过具有公网 IP 节点的中转暴露到公 ...

  2. 分享一个内网穿透工具frp

    首先简单介绍一下内网穿透: 内网穿透:通过公网,访问局域网里的IP地址与端口,这需要将局域网里的电脑端口映射到公网的端口上:这就需要用到反向代理,即在公网服务器上必须运行一个服务程序,然后在局域网中需 ...

  3. 代理内网上网-iptables

    代理内网上网-iptables 1.1 环境说明 主机A:(能上网) ip:内172.16.1.7/24 外10.0.0.7/24 系统CentOS 6.9 主机B:(不能上网) ip:内172.16 ...

  4. 【代理】内网穿透工具 frp&frps

    frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内网穿透,对外网提供服务,支持 tcp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发. ### frp 的作 ...

  5. frp实现基于反向代理的内网穿透

    个人博客主页: xzajyjs.cn frp是什么 简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP.TCP.UDP等众多协议 ...

  6. 【新晋开源项目】内网穿透神器[中微子代理] 加入 Dromara 开源社区

    1.关于作者 dromara开源组织成员,dromara/neutrino-proxy项目作者 名称:傲世孤尘.雨韵诗泽 名言: 扎根土壤,心向太阳.积蓄能量,绽放微光. 拘浊酒邀明月,借赤日暖苍穹. ...

  7. [原创]K8飞刀20150725 支持SOCKS5代理(内网渗透)

    工具: K8飞刀编译: 自己查壳组织: K8搞基大队[K8team]作者: K8拉登哥哥博客: http://qqhack8.blog.163.com发布: 2015/7/26 3:41:11 简介: ...

  8. iptables之NAT代理-内网访问外网

    1.前言 本文使用NAT功能:内网服务器,想上网又不想被攻击. 工作原理:内网主机向公网发送数据包时,由于目的主机跟源主机不在同一网段,所以数据包暂时发往内网默认网关处理,而本网段的主机对此数据包不做 ...

  9. Mysql-proxy代理内网数据库

    Mysql-proxy 参考:https://segmentfault.com/q/1010000000394160 情景分析:首先您需要正在使用UCloud云主机(uhoust)以及云数据库(udb ...

  10. ssh后门反向代理实现内网穿透

    如图所示,内网主机ginger 无公网IP地址,防火墙只允许ginger连接blackbox.example.com主机 假如你是ginger的管理员root,你想要用tech主机连接ginger主机 ...

随机推荐

  1. 大语言模型(LLM)在文本分类、语言生成和文本摘要中的应用

    目录 大语言模型(LLM)在文本分类.语言生成和文本摘要中的应用 引言 文本分类.语言生成和文本摘要是人工智能领域中的重要任务,涉及到自然语言处理.机器学习和深度学习等领域.本文将介绍大语言模型(LL ...

  2. Maven进阶

    前言 在项目开发的过程中,我们通常要使用到外部依赖的组件,同时也会使用某些插件来帮助我们管理项目.例如,我们访问数据库的时候需要使用到jdbc组件,我们可以下载对应的jar包去加载到我们的应用中.在我 ...

  3. ASL芯片CS5466方案设计|集睿致远CS5466代理商|Type-c转HDMI电路原理

    CS5466作为ASL集睿致远新推出的高性能Type-C to HDMI2.1协议转换器,可以通过HDMI输出端口作为TMDS或FRL发射机进行操作. CS5466适配于多个配件市场和现实应用主板,例 ...

  4. Sentieon | 每周文献-Tumor Sequencing-第三期

    肿瘤测序系列文章-1 标题(英文):The relationship between genetic characteristics and clinical characteristics and ...

  5. ISP-长短曝光融合生成HDR图像

    1.高动态范围图像相关 图像的动态范围是指一幅图像中量化的最大亮度与最小噪声的比值.高动态范围HDR(high dynamic range)图像,能够完整表示真实场景中跨度很大的动态范围.采用普通CM ...

  6. Unity自定义类使用携程--自身不继承MonoBehaviour

    [TOC] 参考: https://www.jianshu.com/p/67f498cb839b 话不多说,直接上代码 1 using System.Collections; 2 using Unit ...

  7. quarkus实战之三:开发模式(Development mode)

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 前文咱们曾提到过几种启动方式,有一种用mav ...

  8. Blazor前后端框架Known-V1.2.6

    V1.2.6 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行. Gitee: https://gitee.com/known/Known Gith ...

  9. Powe AutoMate: 条件判断语句

    大纲 学习使用条件判断语句 使用条件判断 功能描述 判断用户输入的年龄,并显示对应的信息 使用if 判断是否是未成年人: 使用else if 判断大于18岁,小于28岁的人群 运行效果 使用else ...

  10. Vue Cli起别名

    vue cli 3的写法 module.exports = { configureWebpack: { resolve:{ extensions:[], alias:{ 'assets':'@/ass ...