排查某次监控系统大面积告警延迟事故,Zabbix Dashboard 显示待处理队列(Queue)堆积突破 20 万,告警通知 P99 延迟达到夸张的 2 小时。最终定位:业务团队滥用自定义模板,通过 log[] 键值将大段 Java 报错栈直接写入 Zabbix,导致 history_str 和 history_text 表数据暴增;同时 Zabbix 原生 Housekeeper 清理过期数据时触发海量 DELETE 操作,彻底打爆 MySQL InnoDB 的 IOPS,引发 History Syncer 进程全部夯死。解决方案极其粗暴且有效:彻底关闭 Zabbix 原生 Housekeeper,将所有历史与趋势表改造成 MySQL 按天/按月分区表(Table Partitioning),用 DROP PARTITION 替代 DELETE。
故障现场极具讽刺意味:监控系统本身成了最需要被监控的系统。登录 Zabbix Server 节点,系统负载(Load Average)出奇的低,但查看 zabbix_server.log,满屏的红色告警:
Zabbix server history syncer processes more than 75% busy
Zabbix server trapper processes more than 75% busy
[Z3005] query failed: [1205] Lock wait timeout exceeded; try restarting transaction [delete from history_text where itemid=19283 and clock<1698710400]
History Syncer 是 Zabbix 将内存缓存数据刷入数据库的核心进程,它被堵死,意味着前端 Poller 和 Trapper 收集到的数据全憋在 Server 的 Shared Memory 里,最终导致采集停滞、队列爆炸。
切到 MySQL 数据库节点,罪魁祸首立刻浮出水面。执行 iostat -dx 2 观察磁盘,数据盘的 %util 死死钉在 100%,w/s (每秒写 IO)达到磁盘物理极限,await 延迟飙升到 800ms 以上。
进入 MySQL 终端敲下 SHOW FULL PROCESSLIST;,看到几十个处于 updating 或 Locked 状态的 SQL 线程,全部是类似这样的语句:
DELETE FROM history_str WHERE itemid=40281 AND clock < 1698710400 LIMIT 5000;
DELETE FROM history_text WHERE itemid=40282 AND clock < 1698710400 LIMIT 5000;
技术逻辑其实非常清晰。把关系型数据库当做时序数据库(TSDB)来用,本身就是一种架构妥协。在 InnoDB 引擎中,执行 DELETE 语句删除几百万行历史数据,简直是运维自杀。DELETE 并不是简单地抹掉磁盘空间,而是会产生海量的 Undo Log(用于回滚)和 Redo Log(用于崩溃恢复),同时需要更新 B+ 树索引,引发大量的数据页分裂与 Buffer Pool 内存淘汰(Churn)。
当这种重度 IO 操作遇上业务团队滥用 Zabbix 收集文本日志(把 Zabbix 当 ELK 用),history_text 表的一行数据可能高达几 KB。Housekeeper 一启动,瞬间的随机写 IO 洪峰直接把底层存储击穿。
为什么说这种配置不可原谅?因为在超过万级 NVPS(每秒处理新值数)的中大型 Zabbix 架构中,依赖原生 Housekeeper 清理数据是标准的新手雷区。Zabbix 官方手册虽然提过分区表的替代方案,但默认安装依然开启 Housekeeper,这坑了无数没有经历过数据量毒打的运维。
止血与根治方案:
第一步,紧急止损。立刻修改 zabbix_server.conf,将 Housekeeping 的频率设置为 0,切断 IO 洪峰的源头,并重启 Zabbix Server,等待队列慢慢消化。
# zabbix_server.conf
HousekeepingFrequency=0
第二步,架构重构,实施 MySQL 表分区(Table Partitioning)。
原理很简单:将时间序列数据按时间(clock 字段)分散到不同的物理文件中。当需要删除过期数据时,直接 ALTER TABLE ... DROP PARTITION。在文件系统层面,这等同于直接 rm -f 一个物理文件,时间复杂度为 O(1),瞬间释放空间,彻底零 IO 负担,不会产生任何 Undo Log。
针对 Zabbix 历史表的改造核心 SQL 如下(以 history_uint 为例):
-- 确保表结构没有外键,且时钟字段是主键/唯一键的一部分
ALTER TABLE history_uint PARTITION BY RANGE (clock) (
PARTITION p20231101 VALUES LESS THAN (UNIX_TIMESTAMP('2023-11-02 00:00:00')),
PARTITION p20231102 VALUES LESS THAN (UNIX_TIMESTAMP('2023-11-03 00:00:00')),
-- 预先建好未来的分区
PARTITION p_future VALUES LESS THAN MAXVALUE
);
配合一个定时执行的 Shell 或 Python 脚本(业内常用 zabbix-mysql-partitioning.pl 脚本),每天零点自动检查并 DROP 掉过期的历史分区,同时 ADD 未来的新分区。
最后,强烈建议剥离 Zabbix 的文本日志收集职能。Zabbix 核心是数值型时序监控(float/uint),日志型(str/text)数据一律丢给 Filebeat + Elasticsearch 或 Promtail + Loki 去处理。在监控架构里,强行让一个工具做所有事,最后通常是什么都做不好。
同类问题排查清单(Zabbix 性能雪崩速查)
-
Zabbix 内部队列指标:在 Zabbix Frontend 检查
Administration -> Queue。如果延迟集中在 10 分钟以上,且大部分是 Zabbix agent (active) 或 Trapper,大概率是 Server 性能瓶颈而非网络问题。 -
底层数据库 IOPS 饱和度:通过
iostat -dx 1或 Node Exporter 的node_disk_io_time_seconds_total指标,排查底层数据盘%util是否长期处于 90%+。如果是,立即停止 Zabbix Server 进程以保护 DB。 -
Housekeeper 配置确认:检查 Zabbix Server 配置中的
HousekeepingFrequency、MaxHousekeeperDelete。大规模场景下必须置 0 关闭,改用 DB 原生表分区或直接使用 TimescaleDB/ClickHouse 作为后端。 -
滥用监控项审查:在 MySQL 执行
SELECT itemid, COUNT(*) FROM history_text GROUP BY itemid ORDER BY COUNT(*) DESC LIMIT 10;,揪出产生大量文本型历史数据的 Top 10 监控项(Items),并在 Zabbix 页面中直接 Disable,切断污染源。 -
缓存击穿指标:查看 Dashboard 中的
Zabbix cache usage, % free。如果History index cache或Value cache经常跌破 5%,说明CacheSize和HistoryCacheSize配置过小,或者存在大量低频的长周期聚合查询在刷缓存。