监控系统本身的雪崩,是运维架构中最不能忍受的“灯下黑”。近期接手处理了一起 Zabbix 分布式集群的严重延迟故障。现象极为惨烈:全局 30+ Zabbix Proxy 出现严重的数据积压,Server 端监控队列(Queue)飙升至 50万以上,引发大面积的“Zabbix agent on xxx is unreachable”误告警,On-Call 手机直接被打爆。
直接抛结论:当 Zabbix 集群的 NVPS(每秒处理的新值数量)超过 5000 时,绝对不能依赖 Zabbix 原生的 Housekeeper 来清理历史数据。本次故障的根因正是 Housekeeper 唤醒后,向后端 MySQL 提交了海量的 DELETE 语句,导致 InnoDB Buffer Pool 频繁换页,底层存储 I/O 被随机写彻底打满。数据库的极度缓慢导致 Zabbix Server 的 history syncer(历史数据同步器)进程 100% 阻塞,进而耗尽 History Cache,最终拒绝 Proxy 的数据写入请求,引发全链路雪崩。
解法极其粗暴但有效:全局关闭 Housekeeper 的历史数据清理功能,将 Zabbix 核心时序表(history、history_uint、trends 等)全部改造为基于 clock 字段的按天表分区(Table Partitioning),通过 DROP PARTITION 替代 DELETE 实现 O(1) 复杂度的过期数据淘汰。
故障现场与弱智的灾难链
排查过程中,我第一时间查看了 Zabbix Server 的内部性能指标图表(Zabbix internal process busy %)。图表显示:
-
history syncer进程繁忙度直接拉平在 100%。 -
trapper进程(负责接收 Proxy 数据的进程)繁忙度紧随其后,也达到 100%。 -
Zabbix 内部缓存
History cache % free跌至 0%。
登录数据库节点,敲下 iostat -x 1,磁盘 %util 钉死在 100%,w/s(每秒写请求)极高。
再切进 MySQL 执行 SHOW FULL PROCESSLIST;,满屏都是类似下面的慢查询:
DELETE FROM history_uint WHERE itemid=45923 AND clock<1696118400;
这就是典型的架构认知缺失。指望关系型数据库用标准 SQL 处理时序数据的生命周期,是一种极其天真的做法。
底层原理解析:为什么 DELETE 是时序数据的毒药?
在 MySQL InnoDB 引擎中,执行一条 DELETE 语句的成本是极高的。它不是简单地把数据抹掉,而是:
-
寻址与锁:在二级索引和聚簇索引(B+树)中找到目标记录,加上行锁(甚至间隙锁)。
-
写 Undo Log:为了支持事务回滚和 MVCC,必须把删除前的数据写入 Undo 表空间。
-
写 Redo Log:修改内存中的数据页,并记录 Redo Log 以保证持久化。
-
B+ 树维护:大量删除会导致数据页变为空(Page Empty),触发树的分裂与合并(Page Merge),产生极严重的写放大(Write Amplification)。
当 Housekeeper 试图一次性删除几千万条过期监控数据时,InnoDB 的 Buffer Pool 被大量的脏页填满,后台刷脏(Page Cleaner)线程疯狂工作,直接导致数据库整体吞吐量暴跌。
此时,Zabbix Server 的 history syncer 进程正试图将内存中的新监控数据批量 INSERT 到 MySQL。由于 MySQL 处于高负载的锁竞争和 I/O 拥塞状态,INSERT 被迫挂起。
history syncer 阻塞后,Zabbix Server 分配的 HistoryCacheSize(通常配置为几十MB到几百MB)迅速被新来的数据填满。缓存一旦打满,接收 Proxy 数据的 trapper 进程就无法将数据写入缓存,只能挂起。最终,Proxy 无法推送数据,只能将数据堆积在 Proxy 本地的 SQLite 库中,全局队列雪崩就此形成。
改造方案与落地实战
不要试图通过调优 InnoDB 参数(如 innodb_io_capacity 或 innodb_buffer_pool_size)来解决这个问题,那是治标不治本。解法只有一种:表分区。
1. 禁用 Housekeeper
在 Zabbix Web 界面:Administration -> General -> Housekeeping。
将 History 和 Trends 的 Enable internal housekeeping 勾选全部去掉。(注意:Events、Services 等小数据量表可以保留由 Housekeeper 清理)。
或者在 zabbix_server.conf 中直接修改配置并重启:
# 禁用所有后台自动清理操作(非极端情况不建议全禁,建议走 GUI 精细化控制)
HousekeepingFrequency=0
2. 核心表实施 Range 分区
对于 MySQL,必须按时间戳(clock)字段对大表进行拆分。Zabbix 核心的大表主要有:
-
history(浮点数) -
history_uint(无符号整数 – 通常是最大的表) -
history_str,history_text,history_log -
trends,trends_uint
以 history_uint 为例,改造分区的核心 SQL 逻辑如下:
-- 注意:操作前必须停止 Zabbix Server,确保无新数据写入
ALTER TABLE history_uint
PARTITION BY RANGE (clock) (
PARTITION p20231001 VALUES LESS THAN (UNIX_TIMESTAMP('2023-10-02 00:00:00')),
PARTITION p20231002 VALUES LESS THAN (UNIX_TIMESTAMP('2023-10-03 00:00:00')),
-- 预先建好未来几天的分区...
PARTITION p_max VALUES LESS THAN MAXVALUE
);
(注:如果原表已经包含海量数据,直接 ALTER 会锁表很久,建议采用新建分区表 -> INSERT INTO ... SELECT -> RENAME TABLE 的平滑迁移方案。)
3. 部署分区维护脚本
分区建好后,需要通过 Cron 每天定时执行脚本,创建未来的新分区,并 DROP 掉过期的老分区。
ALTER TABLE history_uint DROP PARTITION p20230901;
降维打击就在这里:DROP PARTITION 是一个 DDL 操作,它在文件系统层面上直接删除了对应的 .ibd 数据文件。不走 Buffer Pool,不写 Undo Log,不触发 B+ 树合并,时间复杂度永远是 O(1)。原本需要数小时且耗尽 I/O 的清理任务,现在只需要几毫秒。
总结
Zabbix 是一个优秀的监控框架,但它的默认配置是为了让新手能在单机上无脑跑起来而设计的,绝对不是为了生产环境的高可用。在面对大规模监控场景时,任何依赖 RDBMS 进行高频 Delete 操作的设计都是反模式的。如果条件允许,直接使用 TimescaleDB(PostgreSQL 的时序插件)作为 Zabbix 后端,利用其原生的 Chunk 机制进行数据淘汰,是更优雅的现代化选择。
排查清单:Zabbix 队列堆积与延迟速查
-
查 Zabbix 内部性能瓶颈:先看
Zabbix server performance面板,如果history syncer> 90% 且History cache % free< 10%,问题必在后端数据库写入慢。如果poller进程 > 90%,则是自定义监控脚本超时或网络不通导致进程耗尽。 -
查数据库 I/O 负载:使用
iostat观察磁盘写延迟,通过SHOW PROCESSLIST抓取长事务或慢查询,确认是否存在DELETE大量扫表。 -
确认 Housekeeper 配置:进入
Administration -> General -> Housekeeping,确认 NVPS > 1000 时,是否已强制接管并禁用了 History 和 Trends 的内置清理。 -
查 Proxy 积压状态:在 Server 端执行
zabbix_get -s,精准定位是哪个 Proxy 无法上报数据,区分是全局故障还是单节点网络隔离。-k "zabbix[proxy,history,queue]"