近期处理了一起 Zabbix 6.0 LTS 集群雪崩事故。根因是某业务线引入劣质自定义 LLD 模板,单机生成逾万监控项,引发 Proxy 缓存打满与 History Syncer 进程 100% 繁忙,最终压垮后端 DB IO 导致全局断连。核心解法:阻断异常 LLD 发现、调优 Zabbix 核心缓存参数,并将底层存储彻底迁移至 PostgreSQL + TimescaleDB 解决写入墙问题。
故障现场:Queue 积压与 Poller 满载
排查过程中,监控大屏首先报警的是 Zabbix Queue 严重积压,延迟超过 10 分钟的 item 数量直线飙升破 5 万。登录 Zabbix Server 核心节点,top 命令显示 Load Average 飙升至 80+,系统 iowait 长期盘踞在 40% 以上。
查看 Zabbix Server 日志 /var/log/zabbix/zabbix_server.log,满屏都是极其致命的告警:
Zabbix server history syncer processes more than 75% busy
Zabbix server history syncer processes more than 100% busy
server is out of memory: Out of memory (data: 256M, index: 64M)
cannot accept connection from proxy "cn-sh-proxy-01": max number of Trapper processes reached
切到前端分布式 Proxy 节点 /var/log/zabbix/zabbix_proxy.log,同样处于崩溃边缘:
cannot send proxy data to server at "10.0.0.10": Zabbix server connection failed
history cache is full, sleeping for 1 second
表象很清晰:数据写不进数据库,导致 Zabbix Server 的 History Syncer(负责将内存数据刷入 DB 的核心进程)全部夯死。Server 端 Trapper 进程耗尽,导致 Proxy 无法上报数据,Proxy 本地的 HistoryCache 被打爆,最终整个监控链路瘫痪。
为什么一个简单的自定义模板能搞垮整个监控集群?
很多开发在写 Zabbix 监控脚本时,缺乏“防御性编程”思维。抓取故障现场的 Proxy sqlite3 库(或本地临时文件),发现罪魁祸首是一个名为 Custom_K8s_Pod_Discovery 的 LLD (Low-Level Discovery) 脚本。
该脚本通过 Python 遍历全量 Pod 状态,但没有做任何 Limit 限制和状态机过滤。单台 Kubernetes Node 上的脚本直接返回了近 5MB 的 JSON Array:
{
"data": [
{"{#PODNAME}": "web-api-7b89f...", "{#NAMESPACE}": "prod", "{#CONTAINER}": "nginx"},
// ... 往下还有 15000+ 个对象
]
}
Zabbix LLD 引擎在处理这个宏大 JSON 时,会为每一个 {#PODNAME} 动态生成 5 个 Item(CPU、内存、网络 IO 等)。
算一笔账:1 台机器抛出 15000 个实体 $\times$ 5 个 Item = 75000 个监控项。
如果是 100 台节点的集群,瞬间生成 750 万个新监控项。
这些海量监控项每 30 秒采集一次数据,疯狂涌入 Zabbix Proxy。
Proxy 的默认 HistoryCacheSize 仅有区区 16M,瞬间被打满。随后 Proxy 将庞大的 Payload 塞给 Zabbix Server,Server 端的 History Syncer 试图将这几百万条并发写入后端的 MySQL history_uint 表。MySQL InnoDB 面对这种毫无规律的极高频并发 Insert,B+ 树页分裂严重,NVMe 磁盘的 IOPS 直接打满,写延迟达到 500ms 以上,彻底堵死。
架构级改造:从 MySQL 到 PG+TimescaleDB
在千万级 Item 的企业监控场景下,MySQL 表分区脚本(如常用的 partitioning.sql 存储过程)不仅维护极其痛苦,且对历史数据的清理依然会产生锁争用。
解决写入瓶颈的最终态方案,是利用原生时序数据库。Zabbix 从 5.0 开始深度支持 PostgreSQL + TimescaleDB 扩展,将 history 相关的表转化为 hypertable,实现按时间维度的透明 Chunk 分片。
迁移与落地步骤:
-
部署 PostgreSQL 14 与 TimescaleDB 插件。
-
导入 Zabbix 基础 Schema 后,务必执行 TimescaleDB 转换脚本:
# Zabbix 6.0 环境下开启 TimescaleDB 支持
zcat /usr/share/doc/zabbix-sql-scripts/postgresql/timescaledb.sql | sudo -u zabbix psql zabbix
- 在 Zabbix Server 开启内部历史数据压缩(极大降低磁盘 IO 并节省 70% 空间):
-- 连接到 zabbix 库
UPDATE config SET db_extension='timescaledb', history_compression_status=1, history_compress_older='7d';
切换到 TimescaleDB 后,Zabbix History Syncer 的写操作变成了针对内存中最新 Chunk 的顺序追加写(Append-only),避开了全表扫描和巨型 B-Tree 维护,单机轻松抗住 10万+ QPS 的监控项写入。
调优与防御性配置落地
底层存储问题解决后,必须对 Zabbix 核心配置进行防御性加固,防止类似 LLD 风暴再次冲垮服务。
1. Zabbix Server 核心参数重调
编辑 /etc/zabbix/zabbix_server.conf:
# 根据物理内存,大幅提高历史缓存,作为 DB 抖动时的缓冲池
HistoryCacheSize=2G
HistoryIndexCacheSize=256M
ValueCacheSize=1G
# 增加数据刷盘进程数(需结合 DB 最大连接数考量)
StartHistorySyncers=30
# 增加处理 Proxy 和 Agent 主动上报的 Trapper 进程
StartTrappers=100
# 禁用 Server 端轮询,强制全部走 Proxy 分布式采集
StartPollers=0
2. Zabbix Proxy 缓冲防御
编辑 /etc/zabbix/zabbix_proxy.conf:
# 提高 Proxy 侧的缓存,容忍更长时间的 Server 端断连
HistoryCacheSize=1G
HistoryIndexCacheSize=128M
# 严格控制外部脚本超时时间,防止进程卡死(默认3秒,最大不超过10秒)
Timeout=10
3. 数据预处理(Pre-processing)截流
针对自定义监控项,强制要求在 Zabbix Web UI 的 Item Preprocessing 中配置以下规则:
-
Discard unchanged with heartbeat (心跳抑制): 如果监控值没有变化,直接在 Proxy/Server 端丢弃,只在达到 heartbeat(如 1 小时)时强制写入一次。这能削减 60% 以上的无用状态写入。
-
正则表达式过滤: 对 LLD 发现的文本进行白名单截断,丢弃非核心进程的数据。
常见问题
Q1: Proxy 报错 “Zabbix server connection failed”,但网络 Ping 和 Telnet 都通,如何排查?
通常不是网络问题,而是 Zabbix Server 端的 Trapper 进程全忙。检查 Zabbix Server 监控大屏上的 Zabbix server trapper processes busy 指标是否达到 100%。若是,需调大 StartTrappers,或检查是否有超大 Payload 正在阻塞网络层解析。
Q2: 监控项经常出现断点,日志提示 “first network error, wait for 15 seconds”,如何优化?
这是 Poller 进程在执行某些慢请求(如大文本抓取、远端 API 调用)时超时了。Zabbix 默认超时 Timeout=3 秒。建议将耗时任务改成 Agent 端的异步 Crontab 写入本地文件,Zabbix 只做简单的 vfs.file.contents 读取;或者将 Timeout 谨慎上调至 10。
Q3: 迁移到 TimescaleDB 后,Zabbix 的 Housekeeper 还需要开启吗?
绝对不需要对历史表开启。开启 TimescaleDB 后,应在 Zabbix UI 的 “Administration -> General -> Housekeeping” 中,勾选 Override item history period 并启用内部机制。旧数据的清理会由 DB 原生的 drop_chunks() 函数瞬间完成,而不是 Housekeeper 一行行执行极度耗 IO 的 DELETE 语句。
Q4: 怎样防止自定义 LLD 脚本再次引发灾难? 运维必须剥夺业务组直接创建 LLD Template 的权限。通过 CI/CD 管道扫描业务侧提交的脚本,限制 LLD 返回的 JSON 最大数组长度(如不超过 200)。此外,在 Zabbix 中利用 “LLD overrides” 功能,强制要求匹配特定正则的对象才能触发 Item 发现。