标签: 监控系统

  • 深入 Zabbix 陷阱排查:未过滤 JSON 大对象轮询引发的 Proxy 积压与 MySQL 脏页刷新风暴实战

    近期处理了一起极具破坏性的监控系统瘫痪事故。现象非常经典:Zabbix Server 内部队列(Queue)在十分钟内飙升至 10 万以上,海量 nodata 触发器误报风暴瞬间淹没了告警通道,Zabbix Web 前端直接 502。最终结论很简单:业务团队在 Zabbix 中挂载了一个低效的自定义模板,每 30 秒通过被动模式(Passive Check)拉取一次包含海量无用信息的 500KB JSON 报文,并直接存入 history_text 表。这种把关系型数据库当日志库用的粗暴操作,直接打满了 Zabbix Proxy 的同步通道,并引发了后端 MySQL InnoDB 的脏页刷新风暴(Page Flush Storm)。

    解决这类问题,单纯调大 Zabbix 的 HistoryCacheSize 或升级数据库磁盘只是扬汤止沸。真正的解法在于架构级的防御设计:在边缘端(Proxy/Agent)前置预处理(Preprocessing),剥离无效载荷,配合 MySQL 历史表分区(Table Partitioning)与 InnoDB 落盘策略调优,才能彻底稳住底盘。

    案发现场与深层诊断

    排查过程中,第一眼看到的是 Zabbix Server 自身的监控指标全盘崩溃。 提取 Zabbix Server 日志,满屏的告警触目惊心:

    Zabbix server history syncer processes more than 75% busy
    Zabbix server poller processes more than 75% busy
    [Z3005] query failed: [1205] Lock wait timeout exceeded; try restarting transaction
    

    直接通过运行时控制命令导出诊断信息:

    zabbix_server -R diaginfo="historycache"
    

    查看输出,History Cache 和 History Index Cache 的剩余空间几乎为零。这说明 Zabbix Server 的数据接收层已经被塞满,而负责将数据落盘的 History Syncer 进程被卡死。

    此时切到后端 MySQL 节点,跑一下 iostat -x 1,发现数据盘的 %util 长期死锁在 100%,写入延时(w_await)飙升到 200ms 以上。 登入 MySQL 查看引擎状态:

    SHOW ENGINE INNODB STATUS\G
    

    发现 Modified db pages 指标极高,Buffer Pool 的脏页比例突破了 innodb_max_dirty_pages_pct 的阈值。Page Cleaner 线程疯狂刷盘,引发了严重的 I/O 拥塞,导致事务提交时写 Redo Log 发生等待。

    究竟是哪个业务指标在疯狂灌水?直接查 Zabbix 数据库的 items 表:

    SELECT h.host, i.key_, i.delay, i.value_type 
    FROM items i 
    JOIN hosts h ON i.hostid = h.hostid 
    WHERE i.status = 0 AND i.value_type IN (1, 4) -- 1: character, 4: text
    ORDER BY i.delay ASC LIMIT 10;
    

    真相大白:某个新上线的微服务,为了图省事,写了一个自定义脚本获取应用状态,返回的是一个完整的、未经任何裁剪的巨型 JSON 结构。更致命的是,配置为 Text 类型,每 30 秒采集一次,且通过被动模式(Zabbix Server/Proxy 主动建立 TCP 连接去拉取)。

    为什么这个操作不可原谅?

    在运维架构中,监控系统是生命线,对监控系统本身进行 DoS 攻击是极其低级的错误。

    1. 被动模式阻塞 Poller 进程:被动模式下,Zabbix Poller 需要等待 Agent 执行脚本并返回结果。当脚本执行耗时或报文过大时,TCP 会话长期占用 Poller 进程。默认几百个 Poller 瞬间耗尽,其他正常主机的 CPU、内存采集自然被挂起,引发大面积 nodata 误报。

    2. 关系型数据库的文本噩梦:Zabbix 依赖的 MySQL InnoDB 对高频、大字段(Blob/Text)的插入极度不友好。海量的无序大文本写入不仅无法有效压缩,还会导致 Redo Log 极速轮转,触发 Checkpoint 强行刷脏,打爆磁盘 IOPS。把 Zabbix 当作 ELK 或 Prometheus(TSDB)来用,是典型的架构选型错位。

    止血与根治方案

    应对这种监控血崩,必须分级处理,先止血,再重构。

    第一步:紧急熔断,恢复全局监控 直接在数据库层面禁用该危险模板的所有监控项,并清理历史缓存,重启 Server:

    UPDATE items SET status = 1 WHERE key_ LIKE 'custom.app.json_dump%';
    
    systemctl restart zabbix-server
    

    第二步:利用 Zabbix Preprocessing 进行边缘降维 如果业务确实需要从该 JSON 中提取状态码,绝对不应将原始 JSON 透传给 Server。 利用 Zabbix 4.0 引入的 Preprocessing(预处理)功能,在 Agent 或 Proxy 端完成数据解析:

    • 监控项类型改为 Numeric (unsigned)。

    • 在 Item 的 Preprocessing 标签页添加:JSONPath -> $.data.status_code。 这样,Zabbix Proxy 拉取到 500KB 的 JSON 后,仅提取出数字状态码(如 200 或 500),再由 Proxy 将这几个字节的整型数据上报给 Server。网络带宽占用降低了 99.9%,数据库写入压力直接消失。

    第三步:主动模式改造 将 Item 类型从 Zabbix agent 更改为 Zabbix agent (active)。把连接发起方从 Server/Proxy 转移到 Agent 端,彻底释放 Server 端宝贵的 Poller 进程池。

    第四步:MySQL 数据库底层防御性调优 针对 Zabbix 这种典型的“重写轻读”场景,必须对 InnoDB 进行专项调优:

    # my.cnf
    # 牺牲极端的 ACID 保证,换取成倍的写入吞吐量。Zabbix 丢失最近 1 秒的数据完全可接受。
    innodb_flush_log_at_trx_commit = 2 
    
    # 提升 Buffer Pool 命中率,通常设为物理内存的 60%-70%
    innodb_buffer_pool_size = 32G 
    
    # 匹配 SSD 的真实 IOPS 能力,避免 Page Cleaner 罢工
    innodb_io_capacity = 3000
    innodb_io_capacity_max = 6000
    
    # 优化大事务日志
    innodb_log_file_size = 2G
    

    同时,强烈建议对 Zabbix 的 history 系列表(尤其是 history_uint 和 history)实施按天分区(Table Partitioning)。Zabbix 内部的 Housekeeper 线程执行 DELETE 语句清理旧数据时会产生海量碎片和全表锁;改成分区表后,清理历史数据仅需一条 ALTER TABLE DROP PARTITION,瞬间完成且零磁盘 I/O。

    同类问题排查清单(Troubleshooting Checklist)

    1. Server 性能自证:检查 Zabbix 内部监控指标 zabbix[process,history syncer,avg,busy] 和 zabbix[process,poller,avg,busy],若持续 >75%,说明落盘或采集被阻塞。

    2. 大对象排查:在 Zabbix DB 执行 SELECT key_, delay FROM items WHERE value_type=4 AND status=0; 揪出文本类高频轮询项。

    3. Proxy 积压检查:通过 zabbix[proxy,{$PROXY_NAME},lastaccess] 监控 Proxy 心跳延迟,若延迟激增,多为 Proxy 本地 SQLite/MySQL I/O 瓶颈或 Server 拒收。

    4. 数据库 I/O 饱和度确认:使用 iostat -dmx 1 观察 w/s 与 %util,结合 MySQL SHOW ENGINE INNODB STATUS 中的 Log sequence number 推进速度,判断是否遭遇 InnoDB 脏页刷新瓶颈。

  • 深入 Zabbix 监控雪崩排查:自定义 LLD 滥用引发的 MySQL IO 饱和与 History Syncer 阻塞实战

    近期处理了一起极为惨烈的监控系统雪崩事故。故障表现为 Zabbix Server 队列疯狂飙升,延迟监控项(Queue over 10 minutes)达到数十万,所有告警延迟触发甚至丢失,DB 节点的 Load Average 飙到 80+,底层 NVMe 磁盘 %util 锁死在 100%。最终排查结论:业务研发在未经评审的情况下,引入了一个存在严重缺陷的自定义 LLD(Low-Level Discovery)模板,将大量高频更新的容器环境变量作为 Text 类型全量上报。这直接撑爆了 history_text 表,触发了 Zabbix 内部 Housekeeper 的疯狂 DELETE 清理操作,导致 MySQL InnoDB 产生海量随机 IO 与 Undo Log 积压,最终拖垮 History Syncer 进程,引发监控全局假死。

    把关系型数据库当成时序库甚至日志库来用,是监控系统运维中最不可原谅的低级错误。自定义监控脚本返回几百 KB 的冗余字符串,还要每隔 30 秒上报一次,这种粗暴的做法不仅毫无数据价值,更是对底层存储 IO 的公开处刑。

    排查过程的 Dashboard 是一片刺眼的红。首先看 Zabbix Server 的自身监控,核心指标 Zabbix server history syncer processes more than 75% busy 和 Zabbix server history cache, % used 均已触顶 100%。 这说明 Zabbix Server 接收到的数据已经塞满了内存中的 History Cache,但负责将 Cache 刷入数据库的 History Syncer 进程被严重阻塞,无法写入。

    顺藤摸瓜,直接登录 Zabbix 的 MySQL 后端节点,抓取现场指标:

    # 查看磁盘 IO 情况
    $ iostat -x 1
    Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
    nvme0n1           0.00     0.00  120.50 8500.20  1928.00 136003.2  31.97    145.20   16.80   25.50   16.60   0.12 100.00
    
    # 查看 MySQL 进程状态
    $ mysql -e "SHOW FULL PROCESSLIST;" | grep -i "history_text"
    

    不出所料,SHOW PROCESSLIST 中堆积了大量处于 updating 状态的 SQL,其中最致命的是这条: DELETE FROM history_text WHERE itemid=105432 AND clock<1690000000

    此时再看 InnoDB 引擎状态:

    mysql> SHOW ENGINE INNODB STATUS\G
    ---TRANSACTION 4598234, ACTIVE 45 sec fetching rows
    mysql tables in use 1, locked 1
    100345 lock struct(s), heap size 8345120, 5345020 row lock(s), undo log entries 3450920
    

    Undo log entries 达到 300 万级别,History List Length 严重超标。到这里,整个雪崩的逻辑链条已经非常清晰了:

    1. 毒模板泛滥:业务层添加了自定义的 LLD 规则,自动发现并生成了近 50 万个 Text 类型的 Item。这些 Item 每 30 秒抓取一次,数据载荷大。

    2. 写放大与表膨胀:history_text 表在短时间内急剧膨胀。对于 MySQL InnoDB 而言,大批量变长字符串的写入会引发严重的页分裂(Page Split)。

    3. Housekeeper 绞肉机:Zabbix 默认启用了内部的 Housekeeper 来清理过期历史数据。当它试图通过 DELETE 语句清理数亿条历史记录时,由于 clock 字段的索引效率在庞大的表体量前急剧下降,操作退化为缓慢的范围扫描。

    4. IO 饱和与雪崩:巨量 DELETE 产生了惊人的 Undo Log 和 Redo Log,将 NVMe 的写带宽榨干。History Syncer 进程在执行 INSERT 时等不到锁和 IO 资源,集体挂起。History Cache 随之满载,Poller 无法继续收集数据,监控队列彻底爆掉。

    解决这种系统级阻塞,第一步必须是止血,而不是优雅地等待 SQL 执行完毕。

    第一步:暴力阻断清理任务并清理毒模板 直接在 DB 端 Kill 掉所有 Housekeeper 相关的 DELETE 线程,同时在 Zabbix Server 配置中紧急禁用内部清理:

    # /etc/zabbix/zabbix_server.conf
    HousekeepingFrequency=0
    

    重启 zabbix-server 服务。随后在 Web 端找出那个罪魁祸首的 LLD 模板,强制 Unlink and clear,并批量禁用相关主机上的遗留 Item。

    第二步:DB 层面的历史表彻底改造(MySQL Partitioning) Zabbix 原生 Housekeeper 依赖 DELETE,这在千万级以上的监控规模中是绝对的性能毒药。防御性运维的最佳实践是:永远不要用 DELETE 清理 Zabbix 历史,全部改用表分区(Table Partitioning)并通过 DROP Partition 来回收空间。

    对 history、history_uint、history_str、history_text 以及 trends 系列表实施按天/月分区:

    -- 以 history_uint 为例,按天分区
    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')),
        ...
    );
    

    配合 Crontab 每天执行存储过程,自动 ALTER TABLE history_uint DROP PARTITION p_old,耗时仅需几十毫秒,彻底根除 IO 灾难。

    第三步:Zabbix Server 核心参数调优 为了防止未来再出现单点突发流量打挂整个 Server 的情况,必须对 Cache 和 Syncer 进行硬性隔离和扩容:

    # 提高历史缓存,给 DB 抖动留出缓冲池 (根据 RAM 大小调整)
    HistoryCacheSize=2G
    HistoryIndexCacheSize=256M
    ValueCacheSize=1G
    
    # 增加 Syncer 进程数,但不要超过 CPU 核心数的一半
    StartDBSyncers=16
    
    # 限制自定义脚本超时时间,防止 Poller 假死
    Timeout=10
    

    经过上述改造,截断庞大的 history_text,重构分区表后,Zabbix Server 的 Queue 在 5 分钟内迅速清零,IO 恢复到个位数,系统平稳落地。

    同类问题排查清单(Zabbix 性能速查)

    1. 观察 History Cache 使用率 若 Zabbix server history cache, % used 持续走高,说明后端 DB 已经成为瓶颈。切勿盲目增加 Zabbix Poller 数量,这只会加重 DB 负担,应优先排查 MySQL IO 及慢查询。

    2. 检查 Housekeeper 状态 查看 Zabbix Server 日志中 housekeeper 每次执行的耗时。如果耗时超过几十分钟甚至小时级,必须立即停用内部 Housekeeper,改用 MySQL 数据库表分区(Partitioning)策略。

    3. 排查 Unsupported Items 与高频 LLD 使用 zabbix_get 测试自定义脚本耗时。严格限制 LLD 的发现频率(通常建议 1 小时以上),并慎用 Text 和 Log 类型监控项。对于单纯的日志采集,请移步 ELK 或 Loki,Zabbix 不是日志垃圾桶。

    4. MySQL InnoDB 参数防御 确保 innodb_buffer_pool_size 分配了机器物理内存的 60%-70%;innodb_flush_log_at_trx_commit 在极致性能要求且允许少量数据丢失时可设为 2;innodb_io_capacity 需根据底层 SSD/NVMe 性能调高(如设为 10000+)。