标签: MySQL调优

  • 深入 Zabbix 陷阱排查:原生 Housekeeper 扫表引发的 History Syncer 阻塞与 Proxy 队列雪崩实战

    监控系统本身的雪崩,是运维架构中最不能忍受的“灯下黑”。近期接手处理了一起 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 语句的成本是极高的。它不是简单地把数据抹掉,而是:

    1. 寻址与锁:在二级索引和聚簇索引(B+树)中找到目标记录,加上行锁(甚至间隙锁)。

    2. 写 Undo Log:为了支持事务回滚和 MVCC,必须把删除前的数据写入 Undo 表空间。

    3. 写 Redo Log:修改内存中的数据页,并记录 Redo Log 以保证持久化。

    4. 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 队列堆积与延迟速查

    1. 查 Zabbix 内部性能瓶颈:先看 Zabbix server performance 面板,如果 history syncer > 90% 且 History cache % free < 10%,问题必在后端数据库写入慢。如果 poller 进程 > 90%,则是自定义监控脚本超时或网络不通导致进程耗尽。

    2. 查数据库 I/O 负载:使用 iostat 观察磁盘写延迟,通过 SHOW PROCESSLIST 抓取长事务或慢查询,确认是否存在 DELETE 大量扫表。

    3. 确认 Housekeeper 配置:进入 Administration -> General -> Housekeeping,确认 NVPS > 1000 时,是否已强制接管并禁用了 History 和 Trends 的内置清理。

    4. 查 Proxy 积压状态:在 Server 端执行 zabbix_get -s -k "zabbix[proxy,history,queue]",精准定位是哪个 Proxy 无法上报数据,区分是全局故障还是单节点网络隔离。

  • 深入 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 Proxy 雪崩排查:断网恢复引发的 Trapper 耗尽与 MySQL InnoDB 锁死实战

    某次排查过程中,某跨机房专线发生了一次约 15 分钟的抖动,导致该机房的 Zabbix Proxy 节点与中心 Zabbix Server 短暂失联。当专线恢复的瞬间,中心 Zabbix Server 瞬间陷入瘫痪:Load Average 飙升至 120+,监控大盘变成一片空白,所有告警通道静默。最终排查结论极其经典——典型的“重试风暴与并发写入雪崩”。Proxy 在离线期间囤积了海量历史数据,恢复网络后全量、高并发地向 Server 端倾泻。Server 端的 Trapper 进程瞬间耗尽,且底层 MySQL 因未做针对性调优,在海量并发 INSERT 下触发 InnoDB 严重锁等待(Lock wait timeout)与 IO 饱和,最终拖垮了整个监控体系。

    将企业级监控系统当成黑盒跑默认配置,是对生产环境的极度不负责任。监控系统的架构设计同样需要“防御性编程”思维,任何一个下游组件的故障恢复,都不应该成为压垮中心节点的最后一根稻草。

    案发现场:队列爆炸与 DB 假死

    故障发生时,登录中心 Zabbix Server,终端已经非常卡顿。第一时间查看系统指标与核心进程状态:

    1. 系统负载极高,IO 成为瓶颈 执行 top 发现 mysqld 进程 CPU 占用达到 600%,但更致命的是 %wa(IO Wait)高达 45%。

    2. Zabbix Server 内部运行状态崩溃 查看 /var/log/zabbix/zabbix_server.log,满屏的告警日志: text 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 [insert into history_uint (itemid,clock,ns,value) values ...] server is out of memory: out of memory (allocating 4294967296 bytes) Trapper(负责接收 Proxy 数据)和 History Syncer(负责将数据刷入 DB)进程全部处于打满状态。

    3. MySQL 锁等待与堆积 直连 MySQL 数据库,执行 SHOW PROCESSLIST,看到上百个状态为 update 的线程,全部卡在写入历史表: sql INSERT INTO history_uint (itemid,clock,ns,value) VALUES ... 查看 SHOW ENGINE INNODB STATUS\G,发现大量的 Record Lock 争用,Buffer Pool 命中率急剧下降,磁盘 IOPS 被写 Redo Log 和 Flush Page 彻底榨干。

    根因剖析:为什么一次断网恢复会引发全局瘫痪?

    问题的核心在于 Zabbix Proxy 的离线缓存机制 与 中心数据库的并发写入能力 极度不匹配。

    在分布式架构中,Zabbix Proxy 在与 Server 断开连接时,会将采集到的监控数据缓存在本地(默认是 SQLite 或轻量 MySQL/PostgreSQL)。当网络恢复,Proxy 会尝试将离线期间积压的数据(通过 DataSenderFrequency 控制周期)打包发送给 Zabbix Server(端口 10051)。

    雪崩的传导链条如下:

    1. Proxy 数据洪峰:一个承载了 10,000 NVPS(每秒新值)的 Proxy,断网 15 分钟会囤积约 900 万条数据。网络恢复后,Proxy 会以极具攻击性的方式将这些数据并发推向 Server。

    2. Trapper 耗尽:Zabbix Server 的 StartTrappers 进程负责接收这些数据,存入内存共享池。面对突发洪峰,Trapper 进程瞬间被全部分配完毕,导致其他正常的 Proxy 或 Agent Active 检查也无法建立连接,监控出现全局断点。

    3. History Syncer 阻塞:内存池迅速填满,StartHistorySyncers 进程开始疯狂从内存中提取数据拼接成 INSERT 语句写入 MySQL。

    4. MySQL InnoDB IO 饱和与锁死: 这是最致命的一环。如果 MySQL 采用了默认的 innodb_flush_log_at_trx_commit = 1,意味着每一次 History Syncer 的批量事务提交,都会强制触发一次 Redo Log 的 fsync 刷盘。 机械硬盘或普通 SSD 的 IOPS 瞬间被击穿。IO 阻塞导致 INSERT 事务执行变慢,持有的行锁或间隙锁迟迟不释放。新的写入请求被阻塞(Lock wait timeout),进而导致 History Syncer 挂起,最终内存池爆满,Zabbix Server 进程直接 OOM 崩溃。

    止血与根治方案:防御性监控架构的落地

    面对这种雪崩,常规的重启服务毫无意义,启动后几秒钟内又会被积压的数据再次击穿。正确的止血操作是:先切断洪峰源头,再提升消化能力。

    紧急止血操作:

    1. 停掉故障节点的 zabbix_proxy 服务。

    2. 重启中心 zabbix_server,让其消化掉内存中和 DB 中积压的残余事务,确保其他机房的监控恢复正常。

    3. 如果 Proxy 积压数据已经失去时效性且无关紧要,直接清空 Proxy 侧本地 DB 的 proxy_history 表(极其暴力的断臂求生,视业务容忍度而定)。

    深度调优与根治配置(必须落地的最佳实践):

    1. MySQL 底层性能释放(关键) 监控数据是典型的“写多读少、允许极少量丢失”的时序数据。严格的 ACID 对 Zabbix 历史表来说是性能毒药。

    # /etc/my.cnf
    # 牺牲极为罕见的 MySQL 宕机(非 OS 宕机)时 1 秒的数据,换取成百上千倍的写入性能提升
    innodb_flush_log_at_trx_commit = 2 
    
    # 将 Buffer Pool 设置为物理内存的 60%-70%,让尽可能多的数据在内存中合并写入
    innodb_buffer_pool_size = 64G 
    
    # 提升 IO 线程数,榨干 NVMe SSD 的并发能力
    innodb_read_io_threads = 16
    innodb_write_io_threads = 16
    innodb_io_capacity = 5000
    innodb_io_capacity_max = 10000
    

    注:对于 TB 级别的企业级 Zabbix,强烈建议对 history 和 history_uint 表实施按天/周的 MySQL Table Partitioning(表分区),利用 DROP PARTITION 替代 Zabbix 自带的 Housekeeper 删除过期数据,这能彻底解决 Housekeeper 引发的数据库 CPU 毛刺死锁。

    2. Zabbix Server 并发与缓冲调优 扩展 Server 的吞吐管道,并增加共享内存以缓冲洪峰。

    # /etc/zabbix/zabbix_server.conf
    # 增加处理 Proxy 数据的 Trapper 进程(视代理数量和内存而定)
    StartTrappers=50
    # 增加历史数据同步进程,加速刷盘
    StartHistorySyncers=30
    # 扩大缓存池,避免瞬间 OOM 或假死
    CacheSize=8G
    HistoryCacheSize=2G
    HistoryIndexCacheSize=512M
    

    3. Proxy 侧的防御性限流 控制离线数据的囤积量和发送频率,避免在恢复时“一波流”带走中心端。

    # /etc/zabbix/zabbix_proxy.conf
    # 离线数据最多保留时长(小时)。断网太久的数据直接丢弃,保全大局
    ProxyOfflineBuffer=2
    # 控制 Proxy 发送数据的频率,避免过高的并发连接
    DataSenderFrequency=1
    

    排查清单:Zabbix 并发写入雪崩同类问题速查

    1. 检查 Zabbix Server 内部队列与进程瓶颈:使用 zabbix_get -s 127.0.0.1 -k "zabbix[process,history syncer,avg,busy]" 获取内部指标,超过 75% 即需警惕 DB 写入瓶颈。

    2. 排查 MySQL InnoDB 锁等待:在 MySQL 执行 SELECT * FROM information_schema.innodb_trx WHERE trx_state = 'LOCK WAIT'; 揪出阻塞源头,通常是 Housekeeper 与 History Syncer 发生了锁冲突。

    3. 确认磁盘 IO 饱和度:使用 iostat -xz 1 观察 %util 和 await,如果 %util 长期 100% 且以写 IO 为主,必须立刻调整 innodb_flush_log_at_trx_commit。

    4. Housekeeper 负载自查:如果未开启 DB 表分区,检查 zabbix_server.log 中 housekeeper 每次删除数据耗时,若超过 1 分钟,必须调整 MaxHousekeeperDelete 或尽快实施分区改造。

  • 深入 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+)。