18. 从零开始编写一个类nginx工具, 主动式健康检查源码实现
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个请求失败,如果是重要的数据,我们会丢失很多重要数据。
  如果此时客户端拥有重试机制,那么客户端在失败的时候会发起重试,而且系统可能会反复的分配到那台不可达的系统,将会造成短时间内请求数激增,可能引发系统的雪崩。
  所以此时我们主动知道目标端的系统稳定性极其重要。
网络访问示意图
以下是没有主动健康检查
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,节点的宕机会对系统的稳定性产生较大的影响
以下是主动健康检查,它保证了访问后端服务器组均是正常的状态
客户端->>代理服务器: 请求数据(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::timeout和- future做组合,等超时的时候直接按错误处理
部分实现源码
主要源码定义在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工具, 主动式健康检查源码实现的更多相关文章
- 从零开始编写一个BitTorrent下载器
		从零开始编写一个BitTorrent下载器 BT协议 简介 BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P).曾经风靡一时,由于它引起了巨大的流量 ... 
- 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。
		22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ... 
- 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 ... 
- 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的顺序输出,如果传入的是一个字符串,就将字符串反序输出。
		namespace test2 { class Program { /// <summary> /// 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的 ... 
- 二、 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)
		请指教交流! package com.it.hxs.c01; import java.util.Stack; /* 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek) */ ... 
- 如何编写一个SQL注入工具
		0x01 前言 一直在思考如何编写一个自动化注入工具,这款工具不用太复杂,但是可以用最简单.最直接的方式来获取数据库信息,根据自定义构造的payload来绕过防护,这样子就可以. 0x02 SQL注 ... 
- 从零开始编写一个vue插件
		title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ... 
- 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法
		作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ... 
- 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】
		目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ... 
- nginx限流&健康检查
		Nginx原生限流模块: ngx_http_limit_conn_module模块 根据前端请求域名或ip生成一个key,对于每个key对应的网络连接数进行限制. 配置如下: http模块 ser ... 
随机推荐
- tomcat Filter内存马
			idea调试的时候加入源代码 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId> ... 
- 当cmd运行python无法显示中文报错 SyntaxError: Non-UTF-8 code starting with  时
			报错图片: 解决方法: 在python的脚本开头加上 再运行后 
- (占坑编辑中)hexo个人博客主页添加百度搜索资源平台
			hexo个人博客主页添加百度搜索资源平台 目的是在百度搜你的网站,可以搜到 配置过程 添加效果: 我的个人博客主页,欢迎访问 我的CSDN主页,欢迎访问 我的简书主页,欢迎访问 我的GitHub主页, ... 
- 聊聊Spring注解@Transactional失效的那些事
			一.前言 emm,又又又踩坑啦.这次的需求主要是对逾期计算的需求任务进行优化,现有的计算任务运行时间太长了.简单描述下此次的问题:在项目中进行多个数据库执行操作时,我们期望的是将其整个封装成一个事务, ... 
- mysql中使用sql语句统计日志计算每天的访问量
			日志建表语句: CREATE TABLE `syslog` ( `syslogid` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) ... 
- 行行AI人才直播第14期:【国内第二波人工智能进入者、连续创业者】土豆《土豆利用GPT成功融资两次的提示词和故事》
			行行AI人才(海南行行智能科技有限公司)是博客园和顺顺智慧共同运营的AI行业人才全生命周期服务平台. 此刻,ChatGPT的火热程度已经无需多言.一时间,追逐大模型成了国内AI行业的标准动作,&quo ... 
- 但因热爱,愿迎万难,OpenTiny 社区增加一枚前端程序媛贡献者🎉
			我们非常高兴地宣布,OpenTiny Vue Playground 正式上线! 链接:https://opentiny.github.io/tiny-vue-playground/ 在此非常感谢 xi ... 
- 【译】All-In-One Search 在 Visual Studio 17.6 中可用
			一体化搜索体验是在17.2预览版中首次引入的,从那以后我们一直在改进它的质量.新的搜索将代码和特性搜索功能合并到一个 UI 中,因此您可以在一个地方找到所需的东西.实时结果和结果预览加速了这个过程,让 ... 
- 【Unity3D】素描特效
			1 非真实渲染  法线贴图和凹凸映射中讲述了普通光照的渲染原理,实现的效果比较贴近真实世界(照相写实主义,Photorealism),非真实渲染(Non-Photorealism Rendering ... 
- 如何在工作中利用Prompt高效使用ChatGPT?
			导读 AI 不是来替代你的,是来帮助你更好工作.用better prompt使用chatgpt,替换搜索引擎,让你了解如何在工作中利用Prompt高效使用ChatGPT. 01背景 现在 GPT 已经 ... 
