术语俗话 --- 什么是类DDoS雪崩

此类故障的通用故障现象总结
此类故障可以概括为:
网络抖动诱发的类 DDoS 服务雪崩故障。
它不是单点突然宕机,而是从轻微网络异常开始,被客户端重试、连接堆积、服务端资源占用逐步放大,最终导致业务系统大面积不可用。
一、用户侧现象
| 阶段 | 典型表现 |
|---|---|
| 初期 | HIS 打开变慢、登录慢、偶发卡顿 |
| 发展期 | 操作转圈、查询慢、保存慢、偶尔提示连接失败 |
| 扩散期 | 部分客户端登录失败,重登后有时能进 |
| 高峰期 | 大量客户端同时无法登录或登录后无响应 |
| 严重期 | 报数据库连接失败、服务器连接失败、业务中断 |
| 恢复期 | 重启服务后短暂恢复,但仍有零星卡顿 |
二、网络侧现象
| 指标 | 现象 |
|---|---|
| ping | 偶发 timeout,丢包率升高 |
| TCP连接 | 出现 SYN_NO_REPLY、timeout、RST |
| TCP重传 | 明显升高,说明丢包或响应延迟严重 |
| 网卡流量 | 高峰期接近瓶颈,后期可能突然下降 |
| 网关/防火墙 | 可能出现延迟升高、会话数升高、接口拥塞 |
| 交换机端口 | 可能出现 drop、discard、CRC、pause frame |
特别注意:
流量突然下降不一定代表恢复,也可能说明服务端已经无法响应,客户端请求发不进来了。
三、服务端现象
| 指标 | 现象 |
|---|---|
| 业务端口 5522 | Established 连接数快速升高 |
| 5522 CLOSE_WAIT | 明显增多,说明连接释放异常 |
| 5522 SYN_RECEIVED | 增多,说明建连阶段出现积压 |
| 1432 数据库端口 | 连接数随后升高,被业务层传导拖高 |
| CPU | 不一定很高,可能中低负载也会故障 |
| 内存 | 不一定耗尽 |
| 磁盘 | 可能短时队列升高,但通常不是第一原因 |
| 服务状态 | 可能仍在运行,但已经无法正常响应业务 |
四、数据库侧现象
| 现象 | 含义 |
|---|---|
| 1432 连接数升高 | HIS业务服务大量占用数据库连接 |
| SQL连接等待 | 请求堆积传导到数据库 |
| 查询变慢 | 数据库被动承压 |
| 客户端报数据库连接失败 | 不一定是数据库首发故障,可能是业务层雪崩传导 |
判断重点:
如果 5522 先异常、1432 后升高,多数是业务雪崩传导;不是数据库一开始坏了。
五、故障演化特征
网络轻微抖动
↓
5522业务端口响应变慢
↓
客户端自动重试/用户反复登录
↓
5522连接数快速增加
↓
TCP重传升高、CLOSE_WAIT堆积
↓
HIS服务处理能力下降
↓
1432数据库连接被同步拉高
↓
客户端报连接失败/数据库失败
↓
服务进入类DDoS雪崩状态
六、最典型的几个特征
1. 先慢后断
一开始不是完全不能用,而是:
慢、卡、偶发失败、刷新后恢复。
2. 局部先异常,随后扩散
先是少数客户端反馈,随后扩散到多台机器,最后大面积不可用。
3. 业务端口先堵
HIS 业务入口 5522 通常先出现异常,数据库 1432 是后续被拖高。
4. 重试会放大故障
用户越反复登录、刷新、重试,连接数越高,系统越容易被拖垮。
5. CPU、内存不一定满
这类故障不一定表现为 CPU 100% 或内存耗尽,而是表现为:
TCP重传高、连接数高、CLOSE_WAIT高、端口建连失败。
6. 流量骤降可能是假恢复
当服务端完全处理不了请求时,客户端连接不上,流量反而会下降。
这时不是恢复,而是:
请求进不来,服务已经接近不可用。
一句话总结
此类故障的通用现象是:先出现网络抖动和业务端口响应变慢,随后客户端重试导致连接堆积,服务端连接释放异常,数据库连接被传导拉高,最终形成类似 DDoS 效果的服务雪崩,表现为从“偶发慢”逐步发展为“大面积登录失败和业务中断”。
现有工具监测HIS雪崩问题的完整梳理
先说结论
text
没有一个现成工具能开箱即用地监测这类问题
但是:
用现有工具组合,可以覆盖80%的监测需求
剩下20%(四层联动评分)需要少量定制脚本
一、按监测层分:现有工具能覆盖什么
第一层:链路层监测
Smokeping(最推荐)
text
这个工具专门干这件事
官网:https://oss.oetiker.ch/smokeping/
监测什么:
- 连续ping,画出丢包率和延迟趋势图
- 能看到抖动(jitter)
- 能看到丢包突然升高的时间点
对应本次故障:
- 监测 192.168.8.249 的ping质量
- 监测 192.168.8.254 网关的ping质量
- 两个曲线对比,判断是链路问题还是服务器问题
部署方式:
docker run -d \
--name smokeping \
-p 8080:80 \
-e PUID=1000 \
-e PGID=1000 \
lscr.io/linuxserver/smokeping:latest
配置示例(添加HIS监测目标):
*** Targets ***
probe = FPing
+ HIS
menu = HIS服务器监测
title = HIS网络质量
++ HISServer
menu = HIS主服务器
title = 192.168.8.249 网络质量
host = 192.168.8.249
++ Gateway
menu = 网关
title = 192.168.8.254 网关质量
host = 192.168.8.254
优点:
- 免费开源
- 图形直观,一眼看出抖动时间点
- 历史数据保留,方便复盘
缺点:
- 只监测ping,不监测TCP端口状态
- 不能自动告警(需要配合其他工具)
LibreNMS / Zabbix(网络设备监测)
text
用于监测交换机、防火墙的接口流量和丢包
LibreNMS:
- 自动发现网络设备
- SNMP采集交换机接口drop/discard
- 带宽利用率趋势图
- 内置告警规则
Zabbix:
- 更通用的监控平台
- 支持SNMP监测交换机
- 支持自定义监测项
对应本次故障:
- 监测接入交换机的HIS服务器连接端口
- 看端口的in/out流量、drop计数
- 防火墙的CPU、会话数、NAT表
第二层:端口层监测(5522)
Zabbix(最常用)
text
内置TCP端口监测模板
配置示例:
监测项:net.tcp.service[tcp,192.168.8.249,5522]
- 返回1:端口可连接
- 返回0:端口不可连接
监测项:net.tcp.service.perf[tcp,192.168.8.249,5522]
- 返回TCP连接建立耗时(毫秒)
告警规则:
- 连续3次返回0:触发告警
- 连接时间 > 3000ms:触发告警
缺点:
- 只能探测端口通不通
- 不能看连接状态(Established/CLOSE_WAIT等)
- 看不到连接数量
Prometheus + Blackbox Exporter(推荐组合)
text
Blackbox Exporter专门做探测类监测
支持的探测:
- TCP连接探测
- HTTP探测
- ICMP探测
- DNS探测
配置示例(监测5522端口):
# blackbox.yml
modules:
tcp_connect_5522:
prober: tcp
timeout: 5s
tcp:
preferred_ip_protocol: ip4
# prometheus.yml
scrape_configs:
- job_name: 'his_port_probe'
metrics_path: /probe
params:
module: [tcp_connect_5522]
static_configs:
- targets:
- 192.168.8.249:5522
- 192.168.8.249:1432
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- target_label: __address__
replacement: localhost:9115 # Blackbox Exporter地址
Prometheus采集到的指标:
- probe_success:探测是否成功(0/1)
- probe_duration_seconds:探测耗时
- probe_failed_due_to_regex:是否匹配预期响应
Grafana告警规则:
probe_success{job="his_port_probe",instance="192.168.8.249:5522"} == 0
持续60秒 → 触发告警
优点:
- 免费开源,社区成熟
- Grafana画图非常好看
- 告警灵活
缺点:
- 同样只探测通不通
- 看不到连接状态细节
Netdata(实时性最好)
text
安装在HIS服务器本机
自动采集:
- 网络连接状态(TCP状态统计)
- 本机所有端口的连接数
- TCP重传统计
- 网卡流量
对应本次故障:
- 能看到5522端口的连接数变化
- 能看到CLOSE_WAIT、TIME_WAIT等状态数量
- 实时更新(1秒粒度)
安装命令:
bash <(curl -Ss https://my-netdata.io/kickstart.sh)
访问:http://192.168.8.249:19999
能看到的数据:
net.tcpconns(TCP连接数按状态)
- established
- syn_sent
- syn_recv
- close_wait
- time_wait
等等
缺点:
- 显示的是全机器所有端口的TCP状态汇总
- 不能只看5522端口的状态
- 默认不支持只看某个端口的CLOSE_WAIT
第三层:数据库层监测(1432)
SQL Server自带工具
text
SQL Server Management Studio (SSMS)
内置活动监视器:
- 当前用户连接数
- 活跃请求
- 等待任务
- 数据文件I/O
查询语句(可以定时执行并记录):
-- 查当前连接数
SELECT COUNT(*) as connections
FROM sys.dm_exec_sessions
WHERE is_user_process = 1
-- 查阻塞情况
SELECT
blocking_session_id,
session_id,
wait_type,
wait_time,
SUBSTRING(st.text, (r.statement_start_offset/2)+1, 100) as sql_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE blocking_session_id != 0
-- 查长时间运行的查询
SELECT
session_id,
DATEDIFF(SECOND, start_time, GETDATE()) as elapsed_sec,
status,
command
FROM sys.dm_exec_requests
WHERE DATEDIFF(SECOND, start_time, GETDATE()) > 30
Prometheus + SQL Server Exporter
text
开源项目:
https://github.com/burningalchemist/sql_exporter
功能:
- 采集SQL Server自定义查询结果
- 推送到Prometheus
- Grafana展示
配置示例:
# sql_exporter.yml
jobs:
- name: sql_server_his
interval: '15s'
connections:
- 'sqlserver://monitor:password@192.168.8.249:1432'
queries:
- name: user_connections
help: "SQL Server用户连接数"
values: [connections]
query: |
SELECT COUNT(*) as connections
FROM sys.dm_exec_sessions
WHERE is_user_process = 1
- name: blocked_processes
help: "被阻塞的进程数"
values: [blocked]
query: |
SELECT COUNT(*) as blocked
FROM sys.dm_exec_requests
WHERE blocking_session_id != 0
- name: long_running_queries
help: "超过30秒的查询数"
values: [count]
query: |
SELECT COUNT(*) as count
FROM sys.dm_exec_requests
WHERE DATEDIFF(SECOND, start_time, GETDATE()) > 30
Grafana告警:
sql_server_his_blocked_processes > 5 → 触发告警
Zabbix SQL Server模板
text
Zabbix官方提供SQL Server监测模板
模板名:Template DB Microsoft SQL Server
监测项包括:
- User connections(用户连接数)
- Batch requests/sec
- SQL compilations/sec
- Buffer cache hit ratio
- Page faults/sec
- Lock waits/sec
直接导入模板就能用,不需要写代码
第四层:客户端层监测
现有工具覆盖较弱
text
这一层现有工具基本无法直接监测:
- 登录失败次数:需要HIS应用日志支持
- 客户端重试行为:需要应用层埋点
部分可用方案:
方案1:分析HIS应用日志
如果HIS记录了登录日志,用ELK或简单脚本分析
方案2:Windows事件日志
如果HIS用Windows认证,可以从安全事件日志抓登录失败
wevtutil qe Security /q:"*[System[(EventID=4625)]]"
方案3:网络侧监测(被动)
用Wireshark/tcpdump抓包分析登录请求频率
二、推荐组合方案(低成本可落地)
方案A:最简版(1周内可部署)
text
工具选择:
┌─────────────────────────────────────────┐
│ Smokeping → 链路抖动监测 │
│ Zabbix → TCP端口探测 + 告警 │
│ Netdata → 服务器本机TCP状态 │
│ SSMS活动监视器→ 数据库人工查看 │
└─────────────────────────────────────────┘
覆盖率:链路层✅ 端口探测✅ 服务器状态✅ 数据库❌(手工)
缺点:
- 不能看5522的CLOSE_WAIT具体数量
- 没有四层联动评分
- 数据库层需要人工查看
方案B:推荐版(2-3周可部署)
text
工具选择:
┌─────────────────────────────────────────────────────┐
│ Smokeping → 链路抖动可视化 │
│ Prometheus → 数据采集中心 │
│ + Blackbox Exp → TCP端口探测(5522/1432) │
│ + Node Exporter → 服务器系统指标 │
│ + SQL Exporter → SQL Server连接数/阻塞 │
│ Grafana → 统一展示 + 告警规则 │
│ 自定义脚本(20行) → 5522/1432连接状态细节 │
└─────────────────────────────────────────────────────┘
覆盖率:链路层✅ 端口探测✅ 连接状态✅ 数据库✅
这个方案的核心补充脚本:
Bash
#!/bin/bash
# 部署在HIS服务器上,每10秒采集一次
# 输出格式供Prometheus的textfile采集器读取
OUTPUT_FILE="/var/lib/node_exporter/textfile_collector/his_conn.prom"
collect_port_stats() {
local port=$1
local label=$2
# 统计各状态连接数
established=$(ss -tn state established "( dport = :$port or sport = :$port )" 2>/dev/null | grep -c .)
close_wait=$(ss -tn state close-wait "( dport = :$port or sport = :$port )" 2>/dev/null | grep -c .)
syn_recv=$(ss -tn state syn-recv "( dport = :$port or sport = :$port )" 2>/dev/null | grep -c .)
time_wait=$(ss -tn state time-wait "( dport = :$port or sport = :$port )" 2>/dev/null | grep -c .)
# 统计连接来源IP数量
unique_ips=$(ss -tn state established "( sport = :$port )" 2>/dev/null | awk 'NR>1{print $5}' | cut -d: -f1 | sort -u | wc -l)
echo "his_port_established{port=\"$port\",service=\"$label\"} $established"
echo "his_port_close_wait{port=\"$port\",service=\"$label\"} $close_wait"
echo "his_port_syn_recv{port=\"$port\",service=\"$label\"} $syn_recv"
echo "his_port_time_wait{port=\"$port\",service=\"$label\"} $time_wait"
echo "his_port_unique_source_ips{port=\"$port\",service=\"$label\"} $unique_ips"
}
# 采集TCP重传统计
collect_tcp_retransmit() {
retrans=$(cat /proc/net/snmp | grep Tcp: | tail -1 | awk '{print $13}')
echo "his_tcp_retransmit_total $retrans"
}
# 写入结果
{
echo "# HELP his_port_established HIS端口Established连接数"
echo "# TYPE his_port_established gauge"
collect_port_stats 5522 "his_business"
collect_port_stats 1432 "his_database"
echo ""
echo "# HELP his_tcp_retransmit_total TCP重传累计次数"
echo "# TYPE his_tcp_retransmit_total counter"
collect_tcp_retransmit
} > "$OUTPUT_FILE"
text
这个脚本配合Node Exporter的textfile采集器
就能把5522/1432的连接状态细节推送到Prometheus
然后在Grafana里画图和告警
方案C:完整版(对应上一个回答的自研系统)
text
在方案B基础上增加:
- 四层联动风险评分
- 自动基线学习
- 雪崩风险预警
- 处置建议推送
适合:对HIS稳定性要求极高的三甲医院
三、工具对比总表
text
┌──────────────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ 工具 │链路层│端口探│连接 │数据库│联动 │部署 │
│ │ │测 │状态 │监测 │评分 │难度 │
├──────────────┼──────┼──────┼──────┼──────┼──────┼──────┤
│ Smokeping │ ✅ │ ❌ │ ❌ │ ❌ │ ❌ │ 简单 │
│ Zabbix │ ✅ │ ✅ │ ❌ │ ✅ │ ❌ │ 中等 │
│ Prometheus │ ✅ │ ✅ │ ✅* │ ✅ │ ❌ │ 中等 │
│ +Grafana │ │ │ │ │ │ │
│ Netdata │ ✅ │ ❌ │ ✅ │ ❌ │ ❌ │ 简单 │
│ LibreNMS │ ✅ │ ✅ │ ❌ │ ❌ │ ❌ │ 中等 │
│ PRTG(商业) │ ✅ │ ✅ │ ❌ │ ✅ │ ❌ │ 简单 │
│ 自研脚本补充 │ - │ - │ ✅ │ - │ - │ 简单 │
│ 自研系统 │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ 复杂 │
└──────────────┴──────┴──────┴──────┴──────┴──────┴──────┘
✅* = 需要配合自定义脚本补充端口级连接状态
四、最终建议
text
如果只有1个运维人员,时间有限:
→ 用 Smokeping + Zabbix + 20行脚本
→ 1周内搞定,覆盖最核心的监测需求
如果有2-3人,有一定技术能力:
→ 用 Prometheus + Grafana + Blackbox + SQL Exporter + 自定义脚本
→ 2-3周搞定,覆盖率达到80%,告警灵活
如果对稳定性要求极高,有开发资源:
→ 在上面基础上加自研四层评分引擎
→ 把现成工具的数据汇聚进来做联动分析
→ 不需要从零开始,现成工具负责采集,自研只负责分析和评分
核心观点:现有工具负责采集,自研只补充"联动分析和评分"这一层,这是最省力的落地方式。
gpt
HIS类DDoS雪崩监测系统 - 完整软件研发方案
一、项目概述
1.1 项目定位
text
项目名称:HIS-Guardian(HIS雪崩早期预警监测平台)
研发周期:16周
技术路线:Python + Go + Vue3 + InfluxDB + Redis + MySQL
部署方式:内网独立部署,不依赖外网
核心价值:在HIS服务雪崩发生前15-30分钟发出预警
1.2 系统架构总览
text
┌─────────────────────────────────────────────────────────────┐
│ HIS-Guardian 架构图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 链路层 │ │ 业务入口 │ │ 数据库层 │ │ 客户端层 │ │
│ │ 采集器 │ │ 采集器 │ │ 采集器 │ │ 采集器 │ │
│ │(Go) │ │(Go) │ │(Go) │ │(Python) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────▼──────────────▼──────────────▼──────────────▼─────┐ │
│ │ 数据总线 (Redis Stream) │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼───────────────────────────────┐ │
│ │ 分析引擎 (Python) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 风险评分 │ │ 趋势分析 │ │ 关联分析 │ │ 预警决策 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────┐ ┌──────────▼──────┐ ┌──────────────────┐ │
│ │InfluxDB │◄─│ 存储路由层 │─►│ MySQL │ │
│ │(时序数据)│ └─────────────────┘ │(配置/告警/事件) │ │
│ └──────────┘ └──────────────────┘ │
│ │ │
│ ┌────────────────────────▼───────────────────────────────┐ │
│ │ 告警分发层 │ │
│ │ 短信 │ 钉钉/企微 │ 邮件 │ 声光报警 │ 大屏 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼───────────────────────────────┐ │
│ │ Vue3 前端展示层 │ │
│ │ 实时大屏 │ 趋势图 │ 告警管理 │ 配置管理 │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
二、目录结构设计
text
his-guardian/
├── collector/ # 数据采集层 (Go)
│ ├── cmd/
│ │ └── main.go
│ ├── internal/
│ │ ├── link/ # 链路层采集
│ │ │ ├── ping_collector.go
│ │ │ ├── snmp_collector.go
│ │ │ └── netcard_collector.go
│ │ ├── port/ # 端口层采集
│ │ │ ├── tcp_probe.go
│ │ │ ├── conn_state.go
│ │ │ └── port_analyzer.go
│ │ ├── database/ # 数据库层采集
│ │ │ ├── mssql_collector.go
│ │ │ └── conn_monitor.go
│ │ ├── client/ # 客户端层采集
│ │ │ └── login_collector.go
│ │ └── publisher/ # 数据发布
│ │ └── redis_publisher.go
│ ├── config/
│ │ └── config.yaml
│ └── go.mod
│
├── analyzer/ # 分析引擎 (Python)
│ ├── main.py
│ ├── core/
│ │ ├── risk_scorer.py # 风险评分引擎
│ │ ├── trend_analyzer.py # 趋势分析
│ │ ├── correlation.py # 关联分析
│ │ ├── alert_decision.py # 预警决策
│ │ └── baseline_manager.py # 基线管理
│ ├── models/
│ │ ├── metric.py
│ │ ├── alert.py
│ │ └── risk_score.py
│ ├── storage/
│ │ ├── influx_writer.py
│ │ ├── mysql_writer.py
│ │ └── redis_reader.py
│ ├── notifier/
│ │ ├── sms_sender.py
│ │ ├── dingtalk_sender.py
│ │ ├── email_sender.py
│ │ └── webhook_sender.py
│ └── requirements.txt
│
├── api/ # API服务 (Python FastAPI)
│ ├── main.py
│ ├── routers/
│ │ ├── metrics.py
│ │ ├── alerts.py
│ │ ├── config.py
│ │ └── dashboard.py
│ ├── schemas/
│ │ └── response.py
│ └── requirements.txt
│
├── frontend/ # 前端 (Vue3)
│ ├── src/
│ │ ├── views/
│ │ │ ├── Dashboard.vue # 实时大屏
│ │ │ ├── Metrics.vue # 指标详情
│ │ │ ├── Alerts.vue # 告警管理
│ │ │ └── Config.vue # 系统配置
│ │ ├── components/
│ │ │ ├── RiskGauge.vue # 风险仪表盘
│ │ │ ├── MetricChart.vue # 指标图表
│ │ │ ├── AlertList.vue # 告警列表
│ │ │ └── TopologyMap.vue # 拓扑图
│ │ ├── api/
│ │ │ └── index.js
│ │ └── store/
│ │ └── index.js
│ └── package.json
│
├── database/
│ ├── migrations/
│ │ ├── 001_create_tables.sql
│ │ └── 002_init_config.sql
│ └── influx_init.sh
│
├── deploy/
│ ├── docker-compose.yml
│ ├── nginx.conf
│ └── systemd/
│ ├── his-collector.service
│ ├── his-analyzer.service
│ └── his-api.service
│
└── docs/
├── deploy.md
└── api.md
三、核心代码实现
3.1 采集层 - 链路层采集器(Go)
Go
// collector/internal/link/ping_collector.go
package link
import (
"context"
"fmt"
"math"
"net"
"sync"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
// PingResult 单次Ping结果
type PingResult struct {
Target string
Timestamp time.Time
Success bool
RTT float64 // 毫秒
PacketLoss float64 // 百分比
Seq int
}
// PingStats 统计结果(每个采集周期输出一次)
type PingStats struct {
Target string
Timestamp time.Time
Sent int
Received int
PacketLoss float64
MinRTT float64
MaxRTT float64
AvgRTT float64
StddevRTT float64
Jitter float64 // 抖动:相邻RTT差值均值
}
// PingCollector 链路探测采集器
type PingCollector struct {
targets []string
interval time.Duration // 探测间隔
count int // 每次统计的包数
timeout time.Duration
resultChan chan PingStats
mu sync.Mutex
}
// NewPingCollector 创建采集器
func NewPingCollector(targets []string, interval time.Duration, count int) *PingCollector {
return &PingCollector{
targets: targets,
interval: interval,
count: count,
timeout: time.Second * 3,
resultChan: make(chan PingStats, 100),
}
}
// Start 启动采集
func (p *PingCollector) Start(ctx context.Context) <-chan PingStats {
for _, target := range p.targets {
go p.collectTarget(ctx, target)
}
return p.resultChan
}
// collectTarget 对单个目标持续探测
func (p *PingCollector) collectTarget(ctx context.Context, target string) {
ticker := time.NewTicker(p.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
stats := p.doPing(target)
select {
case p.resultChan <- stats:
default:
// 队列满了丢弃,不阻塞采集
}
}
}
}
// doPing 执行一轮ping统计
func (p *PingCollector) doPing(target string) PingStats {
stats := PingStats{
Target: target,
Timestamp: time.Now(),
}
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
// 无root权限,使用TCP探测替代
return p.tcpFallback(target)
}
defer conn.Close()
dst, err := net.ResolveIPAddr("ip4", target)
if err != nil {
return stats
}
rtts := make([]float64, 0, p.count)
for i := 0; i < p.count; i++ {
rtt, success := p.sendPing(conn, dst, i)
stats.Sent++
if success {
stats.Received++
rtts = append(rtts, rtt)
}
time.Sleep(time.Millisecond * 200)
}
// 计算统计值
stats.PacketLoss = float64(stats.Sent-stats.Received) / float64(stats.Sent) * 100
if len(rtts) > 0 {
stats.MinRTT = rtts[0]
stats.MaxRTT = rtts[0]
sum := 0.0
for _, r := range rtts {
sum += r
if r < stats.MinRTT {
stats.MinRTT = r
}
if r > stats.MaxRTT {
stats.MaxRTT = r
}
}
stats.AvgRTT = sum / float64(len(rtts))
// 计算标准差
variance := 0.0
for _, r := range rtts {
diff := r - stats.AvgRTT
variance += diff * diff
}
stats.StddevRTT = math.Sqrt(variance / float64(len(rtts)))
// 计算抖动(相邻RTT差值的均值)
if len(rtts) > 1 {
jitterSum := 0.0
for i := 1; i < len(rtts); i++ {
jitterSum += math.Abs(rtts[i] - rtts[i-1])
}
stats.Jitter = jitterSum / float64(len(rtts)-1)
}
}
return stats
}
// sendPing 发送单个ICMP包并等待回应
func (p *PingCollector) sendPing(conn *icmp.PacketConn, dst *net.IPAddr, seq int) (float64, bool) {
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: 1,
Seq: seq,
Data: []byte("his-guardian"),
},
}
data, err := msg.Marshal(nil)
if err != nil {
return 0, false
}
start := time.Now()
_, err = conn.WriteTo(data, &net.UDPAddr{IP: dst.IP})
if err != nil {
return 0, false
}
conn.SetReadDeadline(time.Now().Add(p.timeout))
reply := make([]byte, 1500)
_, _, err = conn.ReadFrom(reply)
if err != nil {
return 0, false
}
rtt := float64(time.Since(start).Microseconds()) / 1000.0
return rtt, true
}
// tcpFallback 当ICMP不可用时,用TCP探测替代
func (p *PingCollector) tcpFallback(target string) PingStats {
stats := PingStats{
Target: target,
Timestamp: time.Now(),
}
// 尝试常见端口
ports := []int{80, 443, 22, 5522}
for _, port := range ports {
addr := fmt.Sprintf("%s:%d", target, port)
start := time.Now()
conn, err := net.DialTimeout("tcp", addr, p.timeout)
stats.Sent++
if err == nil {
conn.Close()
rtt := float64(time.Since(start).Microseconds()) / 1000.0
stats.Received++
stats.AvgRTT = rtt
stats.MinRTT = rtt
stats.MaxRTT = rtt
break
}
}
stats.PacketLoss = float64(stats.Sent-stats.Received) / float64(stats.Sent) * 100
return stats
}
3.2 采集层 - TCP端口状态采集器(Go)
Go
// collector/internal/port/conn_state.go
package port
import (
"bufio"
"fmt"
"net"
"os"
"runtime"
"strconv"
"strings"
"time"
)
// ConnState TCP连接状态
type ConnState string
const (
StateEstablished ConnState = "ESTABLISHED"
StateSynSent ConnState = "SYN_SENT"
StateSynRecv ConnState = "SYN_RECV"
StateFinWait1 ConnState = "FIN_WAIT1"
StateFinWait2 ConnState = "FIN_WAIT2"
StateTimeWait ConnState = "TIME_WAIT"
StateClose ConnState = "CLOSE"
StateCloseWait ConnState = "CLOSE_WAIT"
StateLastAck ConnState = "LAST_ACK"
StateListen ConnState = "LISTEN"
StateClosing ConnState = "CLOSING"
)
// PortStats 端口连接状态统计
type PortStats struct {
Port int
Timestamp time.Time
// 连接状态计数
Established int
SynSent int
SynRecv int
FinWait1 int
FinWait2 int
TimeWait int
CloseWait int
LastAck int
Listen int
Total int
// 按来源IP统计
SourceIPCount map[string]int
// 最大单IP连接数
MaxSingleIPConn int
// TCP探测结果
ProbeSuccess bool
ProbeLatency float64 // ms
}
// ConnStateCollector 连接状态采集器
type ConnStateCollector struct {
monitorPorts []int
}
// NewConnStateCollector 创建采集器
func NewConnStateCollector(ports []int) *ConnStateCollector {
return &ConnStateCollector{monitorPorts: ports}
}
// Collect 采集所有监控端口的连接状态
func (c *ConnStateCollector) Collect() ([]PortStats, error) {
switch runtime.GOOS {
case "linux":
return c.collectLinux()
case "windows":
return c.collectWindows()
default:
return nil, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
}
// collectLinux 从 /proc/net/tcp 解析连接状态
func (c *ConnStateCollector) collectLinux() ([]PortStats, error) {
// 初始化各端口统计
statsMap := make(map[int]*PortStats)
for _, port := range c.monitorPorts {
statsMap[port] = &PortStats{
Port: port,
Timestamp: time.Now(),
SourceIPCount: make(map[string]int),
}
}
// Linux TCP状态码映射
stateMap := map[string]ConnState{
"01": StateEstablished,
"02": StateSynSent,
"03": StateSynRecv,
"04": StateFinWait1,
"05": StateFinWait2,
"06": StateTimeWait,
"07": StateClose,
"08": StateCloseWait,
"09": StateLastAck,
"0A": StateListen,
"0B": StateClosing,
}
files := []string{"/proc/net/tcp", "/proc/net/tcp6"}
for _, filePath := range files {
if err := c.parseNetTCP(filePath, statsMap, stateMap); err != nil {
// tcp6不存在时忽略
continue
}
}
// 计算派生指标
result := make([]PortStats, 0, len(c.monitorPorts))
for _, port := range c.monitorPorts {
s := statsMap[port]
s.Total = s.Established + s.SynSent + s.SynRecv +
s.FinWait1 + s.FinWait2 + s.TimeWait +
s.CloseWait + s.LastAck
// 找最大单IP连接数
for _, cnt := range s.SourceIPCount {
if cnt > s.MaxSingleIPConn {
s.MaxSingleIPConn = cnt
}
}
// 做一次TCP探测
s.ProbeSuccess, s.ProbeLatency = c.tcpProbe("127.0.0.1", port)
result = append(result, *s)
}
return result, nil
}
// parseNetTCP 解析 /proc/net/tcp 文件
func (c *ConnStateCollector) parseNetTCP(
filePath string,
statsMap map[int]*PortStats,
stateMap map[string]ConnState,
) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Scan() // 跳过标题行
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
// 本地地址: IP:PORT (十六进制)
localAddr := fields[1]
remoteAddr := fields[2]
stateHex := strings.ToUpper(fields[3])
localPort := c.hexToPort(localAddr)
if localPort == 0 {
continue
}
state, ok := stateMap[stateHex]
if !ok {
continue
}
portStat, monitored := statsMap[localPort]
if !monitored {
continue
}
// 按状态累加
switch state {
case StateEstablished:
portStat.Established++
case StateSynSent:
portStat.SynSent++
case StateSynRecv:
portStat.SynRecv++
case StateFinWait1:
portStat.FinWait1++
case StateFinWait2:
portStat.FinWait2++
case StateTimeWait:
portStat.TimeWait++
case StateCloseWait:
portStat.CloseWait++
case StateLastAck:
portStat.LastAck++
case StateListen:
portStat.Listen++
}
// 统计来源IP
if state == StateEstablished {
remoteIP := c.hexToIP(remoteAddr)
if remoteIP != "" {
portStat.SourceIPCount[remoteIP]++
}
}
}
return scanner.Err()
}
// hexToPort 十六进制地址端口解析
// 格式:0F01A8C0:15A2 -> IP:PORT
func (c *ConnStateCollector) hexToPort(hexAddr string) int {
parts := strings.Split(hexAddr, ":")
if len(parts) != 2 {
return 0
}
port, err := strconv.ParseInt(parts[1], 16, 32)
if err != nil {
return 0
}
return int(port)
}
// hexToIP 十六进制地址转IP
func (c *ConnStateCollector) hexToIP(hexAddr string) string {
parts := strings.Split(hexAddr, ":")
if len(parts) != 2 {
return ""
}
hexIP := parts[0]
if len(hexIP) != 8 {
return ""
}
// 小端序,逐字节解析
b := make([]byte, 4)
for i := 0; i < 4; i++ {
val, err := strconv.ParseUint(hexIP[i*2:(i+1)*2], 16, 8)
if err != nil {
return ""
}