刚把监控大盘上的 Zabbix Queue 积压量从 50 万硬生生压回 0,顺手把跑满的数据库主库切断了重连。拿起手边的茶杯,茶水已经冷透了,窗外是凌晨三点的夜色。
在过去的四个小时里,整个机房的告警系统处于半瘫痪状态。大量宿主机的 CPU、内存告警出现长达数小时的延迟,甚至发生了“主机已宕机,告警还在报 CPU 负载高”的时空错乱感。
表象很容易看清:Zabbix Server 的 History Syncer 进程长时间 100% busy,导致 History Cache 被打满。紧接着,各地分布的 Zabbix Proxy 无法将采集数据上报给 Server,Proxy 本地的数据库开始急速膨胀,最终导致全部监控链路阻塞。
但这只是表象。高并发监控系统崩溃的尽头,往往都是存储的底层挣扎。
1. 拆解 IO 风暴:Housekeeper 的无差别屠杀
当监控项规模达到几十万,NVPS(每秒处理的新值数量)突破两三万时,Zabbix 原生的架构设计会暴露出一个致命缺陷:Housekeeper 清理机制。
Zabbix 默认依赖内部的 Housekeeper 进程去定期删除过期历史数据。其本质是执行类似这样的 SQL:
DELETE FROM history_uint WHERE clock < 1698765432;
在 MySQL (InnoDB) 引擎下,对一张高达 TB 级别的超级大表执行海量 DELETE 操作,简直是一场灾难。
首先,它会导致严重的写放大。InnoDB 需要为每一行被删除的数据记录 Undo Log 以支持 MVCC 回滚;其次,这些操作会把 Buffer Pool 中大量热点业务数据挤出,导致缓存命中率暴跌;最后,删除后的空间并不会立刻释放,而是留下大量数据空洞(Fragment),引发不可预测的页分裂和合并,让底层的随机 IOPS 直接拉满。
我的处理动作很直接:停掉 Housekeeper,用 MySQL 的表分区(Table Partitioning)降维打击。
在 zabbix_server.conf 中直接斩断历史数据的清理动作:
# 禁用历史数据和趋势数据的原生清理
HousekeepingFrequency=0
MaxHousekeeperDelete=0
随后在数据库端,对 history、history_uint、trends 等核心大表实施按天/按月的分区策略。改写后的表结构如下(以 history_uint 为例):
ALTER TABLE history_uint PARTITION BY RANGE (clock) (
PARTITION p20231024 VALUES LESS THAN (UNIX_TIMESTAMP('2023-10-25 00:00:00')),
PARTITION p20231025 VALUES LESS THAN (UNIX_TIMESTAMP('2023-10-26 00:00:00')),
PARTITION p20231026 VALUES LESS THAN (UNIX_TIMESTAMP('2023-10-27 00:00:00')),
...
PARTITION p_max VALUES LESS THAN MAXVALUE
);
用定时脚本或者存储过程,每天凌晨执行 ALTER TABLE history_uint DROP PARTITION p20231024;。在文件系统层面,这等同于直接 unlink 删除了一个 .ibd 物理文件,这是一个 $O(1)$ 的顺序 IO 操作。原本需要锁表死磕几个小时的清理动作,现在几十毫秒就能完成,数据库 IO 瞬间回归平稳。
2. 重塑 Proxy 架构缓冲:打破内存与连接的死锁
数据库的 IO 瓶颈解除后,Zabbix Server 的写入速度恢复,但 Proxy 端的积压并没有立刻消化。
看了一眼 Zabbix Server 的内部状态:
zabbix_server -R diaginfo
输出显示 HistoryCacheSize 的可用空间在剧烈震荡。Zabbix Proxy 的运行逻辑是:如果 Server 的接收缓冲满了,Trapper 进程会拒绝 Proxy 的批量推送。Proxy 被拒后,只能把数据继续积压在自己本地的 SQLite/MySQL 中。随着本地数据越攒越多,Proxy 的 Poller 进程会被拖慢,引发更大范围的采集延迟。
为了加速存量几百万积压数据的消化,我调整了 Server 端负责衔接 Proxy 的关键内存参数,并大幅增加了同步器并发:
# 扩大历史数据缓存,防止 Proxy 突发大流量将 Cache 击穿
HistoryCacheSize=2G
HistoryIndexCacheSize=256M
# 扩大底层同步进程数(对应写入 MySQL 的并发数)
StartHistoryPollers=30
# 增加 Trapper 进程以接收大批量的 Proxy 连接
StartTrappers=50
注意,StartHistoryPollers 并不是越大越好。如果这个数值超过了 MySQL 能承载的最大并发写入线程数,反而会导致 InnoDB Row Lock Contention(行锁争用)。在做了分区表的基础上,我将并发控制在 30 左右,既能保证写入吞吐,又不会引发严重的锁竞争。
3. 从源头止损:自定义模板与预处理(Preprocessing)的重构
当数据洪峰终于退去,系统负载降下来后,我开始查根源:为什么今天的 NVPS 会突然飙升到平时的三倍?
排查 Zabbix 的 items 表发现,近期业务组导入了一套自定义的“全栈监控模板”。这套模板存在两个极其外行的设计:
第一,滥用被动模式(Passive Check)。
模板里包含了几百个针对端口存活、TCP 状态的监控项,且全部配置为 Zabbix agent(被动模式),采集周期设为 10 秒。
在被动模式下,Zabbix Proxy 或 Server 的 Poller 进程需要主动发起 TCP 连接去拉取数据。面对上千台机器,成千上万的短连接频繁建立和销毁,直接耗尽了 Proxy 本地的临时端口号(TIME_WAIT 飙升),Poller 进程全被网络 IO 阻塞。
我立刻用 SQL 批量将这部分监控项全部修改为 Zabbix agent (active)。主动模式下,Agent 会自己在本地汇总数据,然后在一个长连接中批量推给 Proxy,彻底释放了 Proxy 的并发调度压力。
第二,大量采集无意义的静态冗余数据。
比如“系统内核版本”、“网卡 MAC 地址”、“挂载点配置”,这些数据几个月都不会变一次,模板却丧心病狂地设置了每分钟采集一次,并且全部原样存入数据库。
这是对存储资源的极大浪费。我直接在自定义模板的监控项中,加入了 Zabbix 原生的 Preprocessing(预处理) 逻辑:
使用了 Discard unchanged with heartbeat(丢弃未更改的心跳数据),心跳周期设置为 1d(一天)。
// 在监控项预处理步骤中添加
{
"type": "DISCARD_UNCHANGED_HEARTBEAT",
"params": "1d"
}
这行简单的配置在底层起到了奇效:当 Agent 将数据推送到 Server 时,Server 的 Preprocessing Manager 进程会在内存里比对上一次的值。如果内核版本还是 3.10.0-1160,直接在内存中丢弃这条数据,不进入 History Cache,更不发起任何数据库 INSERT 操作。仅此一项改动,全局 NVPS 瞬间断崖式下降了 40%,系统终于迎来了真正的平静。
监控系统的本质,是处理海量时间序列数据的流式计算与存储架构。很多人习惯把 Zabbix 当成一个无脑的黑盒工具,堆机器、加内存。但当架构演进到真正的深水区,决定系统生死存亡的,往往是对一条 SQL 锁范围的精确评估,是对 TCP 队列状态的底层感知,是对每一字节数据生命周期的严苛控制。
问题解决了。收拾完手头的脚本,该去补个觉了。