前情提要
前面我们讲到了如何实时、安全、可扩展地解决底层通信和数据采集的问题。agent 能上报数据了,server 能转发了,db_service 能入库了。但这一堆原始数据躺在表里,不加工、不告警、不按人分权,就是数字堆砌。
这一篇讲三件事:怎么存、怎么判、怎么看。
一、数据建模:不追时髦,追可读、易于理解和维护
1.1 在这一层为什么不用"监控项"抽象
传统那套 {__name__="cpu_usage", instance="A", job="B"} 的 label 模型很强大,但对我来说过度抽象了:
|
问题 |
我的场景 |
|
查询写 PromQL,学习曲线陡 |
运维同事要直接看 SQL |
|
时序库(TSDB)优化针对高基数 |
就几百台机器,PostgreSQL 够扛 |
|
标签自由组合,容易失控 |
我要的是"磁盘表就是磁盘表,进程表就是进程表" |
1.2 三元组:CM / PM / FM
传统网管的三层划分,我直接拿来用:
|
层级 |
含义 |
表举例 |
|
CM (Configuration Management) |
配置数据,慢变 |
`cm_hosts`, `cm_alarm_threshold_default`, `cm_alarm_threshold_host` |
|
PM (Performance Management) |
性能数据,时序插入 |
`pm_disk_monitor`, `pm_process_status_min`, `pm_resource_stats_min` |
|
FM (Fault Management) |
告警数据,状态驱动 |
`fm_alarm_data`, `fm_alarm_notify` |
关键设计:CM 驱动 PM 和 FM。主机注册时,CM 生成告警门限配置;PM 插入时,触发器读 CM 门限,决定是否写 FM。
1.3 面向数据权限编程,不同的人管理不同的主机
前台呈现基于ruoyi框架二次开发,该框架支持面向数据切面编程,所有数据根据hostid挂接,而hostid根据部门和用户id挂接sys_相关表,实现权限功能对接。详见第四部分权限模型和第六部分数据库表关系图。
二、核心表结构
2.1 CM 层:主机与门限
sql
-- 主机表:agent 注册时插入或更新
CREATE TABLE cm_hosts (
hostid SERIAL PRIMARY KEY, -- 与 server 分配的 hostid 对齐
ip INET NOT NULL UNIQUE,
hostname VARCHAR(128),
os_type VARCHAR(32), -- Linux/Windows/AIX
agent_version VARCHAR(16),
dept_id INT REFERENCES sys_dept, -- 关联部门,权限控制用
status INT DEFAULT 1, -- 0离线 1在线 2未注册
last_register TIMESTAMP DEFAULT NOW(),
last_heartbeat TIMESTAMP
);
-- 默认告警门限:全局模板
CREATE TABLE cm_alarm_threshold_default (
threshold_id SERIAL PRIMARY KEY,
metric_type VARCHAR(32) NOT NULL, -- 'cpu'/'mem'/'disk'/'proc'
metric_name VARCHAR(64), -- 具体指标,如 'root_usage'
condition_op VARCHAR(8) DEFAULT '>', -- '>' '<' '=' '>=' '<='
threshold_value NUMERIC(10,2), -- 门限值
severity INT DEFAULT 2, -- 1紧急 2重要 3一般 4提示
duration_sec INT DEFAULT 60, -- 持续多久才告警
enabled BOOLEAN DEFAULT TRUE
);
-- 主机级告警门限:从默认拷贝,可个性化覆盖
CREATE TABLE cm_alarm_threshold_host (
host_threshold_id SERIAL PRIMARY KEY,
hostid INT NOT NULL REFERENCES cm_hosts,
threshold_id INT REFERENCES cm_alarm_threshold_default,
metric_type VARCHAR(32),
metric_name VARCHAR(64),
condition_op VARCHAR(8),
threshold_value NUMERIC(10,2),
severity INT,
duration_sec INT,
enabled BOOLEAN DEFAULT TRUE,
UNIQUE(hostid, metric_type, metric_name)
);
2.2 PM 层:三类时序数据
sql
-- 磁盘监控:定时采集 mount 点使用率
CREATE TABLE pm_disk_monitor (
id BIGSERIAL PRIMARY KEY,
hostid INT NOT NULL REFERENCES cm_hosts,
clock TIMESTAMP NOT NULL, -- 采集时间
mount_point VARCHAR(64), -- / /home /var
total_kb BIGINT,
used_kb BIGINT,
usage_pct NUMERIC(5,2), -- 使用率,触发器读这个
inode_pct NUMERIC(5,2)
);
-- 进程状态:agent 执行脚本返回的离散值
CREATE TABLE pm_process_status_min (
id BIGSERIAL PRIMARY KEY,
hostid INT NOT NULL,
clock TIMESTAMP NOT NULL,
name VARCHAR(128), -- 进程/脚本名
value INT, -- 退出码或状态值
taskid INT, -- 关联 admin 下发的任务
cmd_id INT -- 关联协议层 cmd_id
);
-- 资源统计:CPU/Mem 综合指标
CREATE TABLE pm_resource_stats_min (
id BIGSERIAL PRIMARY KEY,
hostid INT NOT NULL,
clock TIMESTAMP NOT NULL,
cpu_pct NUMERIC(5,2),
mem_pct NUMERIC(5,2),
load_1m NUMERIC(6,2),
disk_io_wait NUMERIC(5,2)
);
2.3 FM 层:告警生命周期
sql
-- 告警数据:一条记录代表一个"未恢复"的告警
CREATE TABLE fm_alarm_data (
alarm_id BIGSERIAL PRIMARY KEY,
hostid INT NOT NULL REFERENCES cm_hosts,
metric_type VARCHAR(32),
metric_name VARCHAR(64),
alarm_value NUMERIC(10,2), -- 触发时的实际值
threshold_value NUMERIC(10,2), -- 门限值
severity INT,
status INT DEFAULT 0, -- 0活跃 1已恢复 2已确认 3已屏蔽
first_time TIMESTAMP DEFAULT NOW(), -- 首次触发
last_time TIMESTAMP DEFAULT NOW(), -- 最近更新
recover_time TIMESTAMP,
duration_sec INT DEFAULT 0, -- 累计持续时间
count INT DEFAULT 1, -- 重复次数
notify_status INT DEFAULT 0 -- 0未通知 1已通知 2通知失败
);
-- 告警通知:记录发送渠道和结果
CREATE TABLE fm_alarm_notify (
notify_id BIGSERIAL PRIMARY KEY,
alarm_id BIGINT REFERENCES fm_alarm_data,
notify_type VARCHAR(16), -- 'sms'/'email'/'webhook'
notify_to VARCHAR(256),
content TEXT,
send_time TIMESTAMP,
status INT DEFAULT 0 -- 0待发送 1成功 2失败
);
三、触发器:数据库里的"隐形代码"
不用应用层轮询判告警,用 PostgreSQL 触发器,入库即判定。
3.1 主机注册时:拷贝默认门限
sql
-- cm_hosts 插入触发器:自动生成主机级门限
CREATE OR REPLACE FUNCTION fn_init_host_threshold()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO cm_alarm_threshold_host
(hostid, threshold_id, metric_type, metric_name,
condition_op, threshold_value, severity, duration_sec)
SELECT
NEW.hostid, threshold_id, metric_type, metric_name,
condition_op, threshold_value, severity, duration_sec
FROM cm_alarm_threshold_default
WHERE enabled = TRUE;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_host_register
AFTER INSERT ON cm_hosts
FOR EACH ROW
EXECUTE FUNCTION fn_init_host_threshold();
3.2 性能数据插入时:判定告警
sql
-- pm_disk_monitor 插入触发器:检查磁盘使用率
CREATE OR REPLACE FUNCTION fn_check_disk_alarm()
RETURNS TRIGGER AS $$
DECLARE
v_threshold RECORD;
v_alarm_id BIGINT;
BEGIN
-- 查该主机该 mount 点的门限
SELECT * INTO v_threshold
FROM cm_alarm_threshold_host
WHERE hostid = NEW.hostid
AND metric_type = 'disk'
AND metric_name = NEW.mount_point
AND enabled = TRUE;
IF NOT FOUND THEN
RETURN NEW; -- 无门限,不告警
END IF;
-- 判定:使用率 > 门限?
IF NEW.usage_pct > v_threshold.threshold_value THEN
-- 查是否已有活跃告警
SELECT alarm_id INTO v_alarm_id
FROM fm_alarm_data
WHERE hostid = NEW.hostid
AND metric_type = 'disk'
AND metric_name = NEW.mount_point
AND status = 0; -- 活跃
IF FOUND THEN
-- 更新现有告警:次数+1,时间刷新
UPDATE fm_alarm_data
SET last_time = NEW.clock,
duration_sec = EXTRACT(EPOCH FROM (NEW.clock - first_time)),
count = count + 1
WHERE alarm_id = v_alarm_id;
ELSE
-- 新建告警
INSERT INTO fm_alarm_data
(hostid, metric_type, metric_name, alarm_value,
threshold_value, severity, first_time, last_time)
VALUES
(NEW.hostid, 'disk', NEW.mount_point, NEW.usage_pct,
v_threshold.threshold_value, v_threshold.severity,
NEW.clock, NEW.clock);
END IF;
ELSE
-- 使用率回落,恢复告警
UPDATE fm_alarm_data
SET status = 1,
recover_time = NEW.clock,
duration_sec = EXTRACT(EPOCH FROM (NEW.clock - first_time))
WHERE hostid = NEW.hostid
AND metric_type = 'disk'
AND metric_name = NEW.mount_point
AND status = 0;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_disk_alarm
AFTER INSERT ON pm_disk_monitor
FOR EACH ROW
EXECUTE FUNCTION fn_check_disk_alarm();
3.3 类似触发器:进程状态、资源统计
触发器配置示例:

pm_process_status_min 和 pm_resource_stats_min 各有一个触发器,逻辑结构相同,只是判定字段不同:
进程: value 与门限比较(如 value != 0 表示异常)
CPU/Mem: cpu_pct / mem_pct 与门限比较
cm_hosts INSERT ──► trg_host_register ──► 生成 cm_alarm_threshold_host
pm_disk_monitor INSERT ──► trg_disk_alarm ──► 读 cm_alarm_threshold_host
├──► 超门限 + 无活跃告警 ──► INSERT fm_alarm_data
├──► 超门限 + 有活跃告警 ──► UPDATE fm_alarm_data
└──► 未超门限 + 有活跃告警 ──► 恢复 UPDATE
pm_process_status_min INSERT ──► trg_proc_alarm ──► 类似结构
pm_resource_stats_min INSERT ──► trg_resource_alarm ──► 类似结构
四、权限模型:主机归部门,部门归人
基于 RuoYi 框架的 sys_user / sys_dept / sys_role 表,不做大改,只加关联:
sys_dept (部门表)
└── dept_id = 1 "运维一部"
└── sys_user (用户表)
└── user_id = 101 "张三"
└── cm_hosts (主机表)
└── hostid = 1001, dept_id = 1
数据切面(AOP):RuoYi 的 @DataScope 注解,自动给 SQL 加 WHERE dept_id IN (...) 条件。
java
// RuoYi 控制器示例
@PreAuthorize("@ss.hasPermi('monitor:host:list')")
@DataScope(deptAlias = "h") // 自动注入当前用户可见的 dept_id 列表
@GetMapping("/list")
public TableDataInfo list(CmHost host) {
// 最终 SQL: SELECT * FROM cm_hosts h WHERE h.dept_id IN (1, 2, 3)
List<CmHost> list = hostService.selectHostList(host);
return getDataTable(list);
}
契合点:传统企业的行政管理方式——"我管哪个部门,就看哪个部门的主机"。不是标签自由组合,是组织树映射。
五、处理流程总览
1. Agent 注册 ──► Server ──► db_service INSERT cm_hosts
│
▼
触发器 trg_host_register
│
▼
生成 cm_alarm_threshold_host(从默认拷贝)
2. Agent 定时上报 ──► Server ──► db_service INSERT pm_xxx
│
▼
触发器 trg_xxx_alarm
│
├──► 超门限 ──► INSERT/UPDATE fm_alarm_data
│ │
│ ▼
│ 触发器 trg_alarm_notify
│ │
│ ▼
│ 生成 fm_alarm_notify
│ │
│ ▼
│ Python notify_worker 发送
│
└──► 未超门限 ──► 恢复 UPDATE fm_alarm_data
3. 用户登录 Web ──► RuoYi 鉴权 ──► 查 sys_user_dept ──► 数据切面过滤
│
▼
可见 cm_hosts / pm_xxx / fm_alarm_data
六、数据库表关系图

七、小结
|
层面 |
设计要点 |
|
数据建模 |
CM/PM/FM 三元组,专用表替代抽象监控项 |
|
告警判定 |
触发器入库即判,零延迟,零额外组件 |
|
通知发送 |
数据库生成任务,Python 轮询执行 |
|
权限控制 |
RuoYi 数据切面,部门树映射主机 |
|
代价 |
触发器调试难、隐式耦合、未来规模可能存在瓶颈 |
感悟:这一层的原则也是需要设计先行、流程清晰,所幸AI能帮忙做很多事。
八、预告
下一篇:《全栈之路6---Web 集成与呈现》,RuoYi 二次开发、ECharts 可视化、AI 辅助调测,收尾

771

被折叠的 条评论
为什么被折叠?



