分类: 监控体系

  • 深入 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,强烈建议对 historyhistory_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 观察 %utilawait,如果 %util 长期 100% 且以写 IO 为主,必须立刻调整 innodb_flush_log_at_trx_commit

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

  • 深入 Prometheus OOM 雪崩排查:动态 Label 滥用引发的高基数风暴与 TSDB WAL 夯死实战

    某次生产核心监控集群突然全线熔断,Prometheus 节点 Load Average 飙升至 100+,Pod 陷入持续的 OOMKilled 死亡循环。排查确认,业务研发在一项 HTTP 统计指标中错误注入了 trace_iduser_id 作为 Label,导致时间序列(Time Series)基数瞬间暴增千万级别。最终通过介入 metric_relabel_configs 强制丢弃高基数 Label,并物理清理内存映射的 Head 块与臃肿的 WAL(Write-Ahead Log)才得以恢复。

    结论先放在这里:监控指标(Metrics)绝对不是日志(Logs),把无边界的动态变量作为 Label 写入 Prometheus,是对 TSDB 存储引擎最无知的谋杀。

    案发现场:失控的 OOM 与堵死的 IO

    排查过程中,告警通道首先报出 Prometheus target 掉线,紧接着是 Kubernetes 节点资源耗尽告警。登录宿主机,dmesg 日志非常直白:

    [52143.123456] prometheus invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=-998
    [52143.123458] Memory cgroup out of memory: Killed process 10245 (prometheus) total-vm:42949672960kB, anon-rss:34359738368kB, file-rss:0kB, shmem-rss:0kB
    

    32GB 内存的 Pod 被硬生生撑爆。由于 Pod 重启策略为 Always,Prometheus 尝试重新启动,但在恢复 WAL 阶段再次卡死,磁盘 IOPS 被打满,日志停留在:

    level=info ts=... caller=head.go:760 component=tsdb msg="Replaying WAL, this may take a while"
    level=info ts=... caller=head.go:812 component=tsdb msg="WAL segment loaded" segment=1023 maxSegment=1045
    

    重放速度极慢,且内存水位在重放过程中成级数增长,最终在启动完成前再次 OOM。这属于典型的高基数(High Cardinality)雪崩

    罪魁祸首:TSDB 的倒排索引与高基数之殇

    在处理这种无法启动的僵尸实例时,直接查 PromQL 是行不通的。直接把挂载的 PV 临时挂给一个 Debug 容器,掏出 promtool 对 TSDB 数据目录进行离线分析:

    promtool tsdb analyze /prometheus/data/
    

    输出结果直接锁定了元凶:

    Block ID: ...
    ...
    Label names with highest number of values:
    1. trace_id: 12045678
    2. user_id: 8543210
    ...
    Metrics with highest number of series:
    1. http_requests_total: 12045678
    

    一个原本只有几十个 endpoint 和 method 组合的 http_requests_total 指标,因为加上了 trace_id,硬生生裂变出了 1200 万个时间序列。

    为什么加个 Label 能把 32G 内存干爆?这要从 Prometheus TSDB 的底层机制说起:

    Prometheus TSDB 的设计前提是 Label 的组合是有限且收敛的。当前正在写入的数据存放在内存中的 Head Block,Head Block 默认保留最多 3 小时的数据。为了实现快速的多维查询,TSDB 维护了倒排索引(Inverted Index)。 每一条唯一的 Label 组合(例如 http_requests_total{method="GET", trace_id="abc"})都会被当作一个全新的 Series。

    1. 内存放大:在 Head 块中,每个活跃的 Series 都会占用几百字节到几 KB 不等的内存(包括结构体、索引缓存、Chunk 引用等)。一千万个 Series,光是基础的结构体开销就能轻易吃掉十几 GB 内存。

    2. WAL 风暴:每次出现一个新的 Series,TSDB 必须在 WAL 中写入一条 Series Record 以保证宕机不丢失。高基数意味着海量的新 Series 不断产生,WAL 写入量呈指数级上升,直接将磁盘 IO 打到饱和。

    3. Compaction 瘫痪:当 Head 块数据落盘生成持久化 Block 时,后台的 Compaction 机制需要对成千万的 Series 进行合并和索引重构,这会耗尽 CPU,并导致 Compaction 积压。

    业务将 trace_id 塞进 Label,等于把 O(N) 复杂度的存储系统当成了 O(1) 的 Key-Value 库在用。

    止血与修复实战

    既然抓到了凶手,修复逻辑就是:阻断毒流量输入,清理已中毒的数据。

    第一步:通过 relabel 丢弃高基数 Label 在不改动业务代码(或业务还没来得及回滚)的情况下,运维必须在 Prometheus 抓取阶段直接阉割掉这个恶意的 Label。在 prometheus.yml 中修改对应 Job 的配置:

    scrape_configs:
      - job_name: 'business_app'
        # 注意:必须使用 metric_relabel_configs,这作用于抓取后、落盘前的阶段
        metric_relabel_configs:
          - source_labels: [trace_id]
            regex: '.*'
            action: labeldrop
          - source_labels: [user_id]
            regex: '.*'
            action: labeldrop
    

    注:如果是客户端直接暴露了几千万行的 /metrics,那应用本身大概率也会因为构建 metrics 字符串而 OOM。此时需要业务立即回滚。

    第二步:处理无法启动的 TSDB 此时由于旧的脏数据还卡在 WAL 里,Prometheus 依然起不来。最粗暴有效的方法是放弃最近几小时的 Head 块数据(监控容忍短暂的断点,但不容忍系统不可用)。

    进入数据目录,直接清理 WAL 和 chunk_head:

    cd /prometheus/data/
    # 备份后删除(如果在乎现场的话)
    rm -rf wal/*
    rm -rf chunks_head/*
    

    清理后拉起 Prometheus,内存占用瞬间回落到正常的几 GB 水平,Load Average 恢复正常,集群起死回生。

    排查清单与同类问题速查

    1. 内存/OOM 快速定位
    2. 永远不要猜测,直接用 promtool tsdb analyze 分析本地数据块,查看 Metrics with highest number of series 排名。

    3. 区分 Relabel 阶段

    4. relabel_configs:作用于 Target 发现阶段,用于过滤抓取目标(改 IP、改端口、丢弃整个 Endpoint)。
    5. metric_relabel_configs:作用于抓取后、写入 TSDB 前,用于修改或过滤具体的 Metrics 和 Label(丢弃高基数 Label 必用)。

    6. 监控自身的监控

    7. 必须为 Prometheus 配置 prometheus_tsdb_head_seriesprometheus_target_scrapes_exceeded_sample_limit_total 的告警。当 Head 序列数突增时,能在 OOM 发生前拦截。

    8. 高基数需求替代方案

    9. 业务确实需要通过 Metrics 关联 TraceID 怎么办?使用 OpenMetrics 标准的 Exemplars。Exemplars 附着在具体的观测值上,不会被纳入倒排索引,不影响基数,完美解决 Metrics 到 Trace 的联动诉求。

    10. 防御性配置限制

    11. scrape_configs 中强制加上 sample_limitlabel_limitlabel_value_length_limit。宁可让超过阈值的抓取失败(报错 sample limit exceeded),也绝不让垃圾数据撑爆整个集群。
  • 深入 Zabbix 队列雪崩排查:Housekeeper 锁表引发的 MySQL IO 饱和与 History Syncer 堵塞实战

    排查某次监控系统大面积告警延迟事故,Zabbix Dashboard 显示待处理队列(Queue)堆积突破 20 万,告警通知 P99 延迟达到夸张的 2 小时。最终定位:业务团队滥用自定义模板,通过 log[] 键值将大段 Java 报错栈直接写入 Zabbix,导致 history_strhistory_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;,看到几十个处于 updatingLocked 状态的 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 性能雪崩速查)

    1. Zabbix 内部队列指标:在 Zabbix Frontend 检查 Administration -> Queue。如果延迟集中在 10 分钟以上,且大部分是 Zabbix agent (active) 或 Trapper,大概率是 Server 性能瓶颈而非网络问题。

    2. 底层数据库 IOPS 饱和度:通过 iostat -dx 1 或 Node Exporter 的 node_disk_io_time_seconds_total 指标,排查底层数据盘 %util 是否长期处于 90%+。如果是,立即停止 Zabbix Server 进程以保护 DB。

    3. Housekeeper 配置确认:检查 Zabbix Server 配置中的 HousekeepingFrequencyMaxHousekeeperDelete。大规模场景下必须置 0 关闭,改用 DB 原生表分区或直接使用 TimescaleDB/ClickHouse 作为后端。

    4. 滥用监控项审查:在 MySQL 执行 SELECT itemid, COUNT(*) FROM history_text GROUP BY itemid ORDER BY COUNT(*) DESC LIMIT 10;,揪出产生大量文本型历史数据的 Top 10 监控项(Items),并在 Zabbix 页面中直接 Disable,切断污染源。

    5. 缓存击穿指标:查看 Dashboard 中的 Zabbix cache usage, % free。如果 History index cacheValue cache 经常跌破 5%,说明 CacheSizeHistoryCacheSize 配置过小,或者存在大量低频的长周期聚合查询在刷缓存。

  • 深入 Zabbix 监控雪崩排查:Proxy 积压引发的 History Syncer 阻塞与数据库底层调优实战

    Zabbix 队列风暴的元凶往往不是 Server 计算能力不足,而是底层数据库 IO 瓶颈与 Proxy 离线数据猛灌。本文通过排查某次 NVPS 突发至 35k 导致的 History Syncer 打满与监控瘫痪事件,深入解析 MySQL 8.0 针对 Zabbix 写入优化的底层逻辑,并给出 Proxy 防御性配置与模板预处理过滤的实战方案。

    故障现场:History Syncer 进程 100% 死亡螺旋

    某次排查过程中,一套承载 20,000+ 主机、2,000,000+ 监控项的 Zabbix 6.0.22 LTS 集群突发严重告警。现象非常典型:

    1. Zabbix Queue 爆炸:延迟超过 10 分钟的 item 数量瞬间飙升到 150,000 以上。

    2. 内部进程打满:Zabbix Server 告警 Zabbix server history syncer processes more than 100% busy

    3. 前端瘫痪:Web 界面卡死,报 Zabbix server is not running: the information displayed may not be current

    查看 zabbix_server.log,满屏的慢查询和超时:

    23851:20231012:142211.512 [Z3005] query failed: [1205] Lock wait timeout exceeded; try restarting transaction [insert into history_uint (itemid,clock,ns,value) values (152342,1697101321,213412,0),(...)]
    23851:20231012:142215.123 server #12 active [history syncer #4]
    23851:20231012:142215.123 Zabbix server history syncer processes 100% busy
    

    很明显,数据落盘卡住了。History Syncer 负责将内存 cache 中的监控数据批量写入底层 MySQL。一旦它被阻塞,Server 内存中的 History Cache 会迅速耗尽,触发自我保护机制,拒绝接收任何新数据,最终导致 Poller 和 Trapper 进程全部雪崩。

    登陆底层 MySQL 8.0.34 节点,敲下 iostat -x 1,看到数据盘的 %util 稳稳地锁死在 100%,await 高达 200ms+。

    为什么 Proxy 断网恢复会导致 Zabbix Server 瞬间雪崩?

    排查发现,在雪崩发生前 15 分钟,某跨机房专线发生了短暂抖动。该机房部署了 3 台 Zabbix Proxy(Active 模式),承载了约 8,000 台主机的监控抓取。

    这里牵扯到 Zabbix Proxy 的底层工作机制。Proxy 默认会将采集到的数据暂存在本地的 SQLite3(或 MySQL)中。当与 Server 断开连接时,Proxy 会根据 ProxyOfflineBuffer 的配置(默认 1 小时)在本地堆积数据。

    雪崩的逻辑链条:

    1. 专线抖动,3 台 Proxy 与 Server 失联,期间不断采集并缓存数据到本地。

    2. 专线恢复,Proxy 瞬间将积压的数十万条历史数据打包。

    3. Proxy 根据 DataSenderFrequency=1(每秒发送)无脑向 Server 的 Trapper 进程猛灌。

    4. Server 的 Trapper 进程将海量数据塞入 History Cache。

    5. History Syncer 进程全速运转,向 MySQL 发起天量 INSERT INTO history... 请求。

    6. MySQL InnoDB Buffer Pool 的脏页刷新速率跟不上写入速率,Redo Log 爆满,触发同步刷盘,导致 IO 彻底僵死。

    防御性配置:限制 Proxy 突发流量

    为了防止类似情况再次发生,必须对 Proxy 的回传机制进行限流。

    1. 调优 Proxy 端缓存发送频率与体积 不要让 Proxy 一次性将积压数据全吐出来。在 zabbix_proxy.conf 中调整:

    # ProxyOfflineBuffer=1 # 离线缓存保留时间,不要设置太大,无意义的历史数据宁可丢弃
    DataSenderFrequency=1
    # 增加批量发送的限制(隐式受制于 Server 端 Trapper 进程处理能力)
    

    注:Zabbix 6.0 引入了 Proxy 内存缓存机制,但在面对海量离线回传时,核心仍是保护 Server 的 DB IO。

    2. 调优 Server 端接收与刷盘并发zabbix_server.conf 中调整:

    # 控制 Trapper 进程数,不要无限调大,防止压垮 History Cache
    StartTrappers=50
    # 增加 History Cache 大小,做大内存缓冲池,争取时间
    HistoryCacheSize=2G
    HistoryIndexCacheSize=512M
    # 增加 Syncer 进程数,但不要超过 DB 磁盘阵列的物理 IO 并发能力上限
    StartHistoryPollers=20
    

    数据库底层调优:拯救被压垮的 MySQL 8.0

    Zabbix 是一个典型的“读少写极其密集”的系统,标准的 MySQL 默认配置在这里就是灾难。针对本次 IO 瓶颈,我们在 my.cnf 中进行了以下针对性调优。

    1. 禁用 Doublewrite Buffer 与放宽事务持久性

    由于 Zabbix 数据并非金融级账本,丢失一两秒的监控数据完全可以接受。

    [mysqld]
    # 核心:将事务刷盘策略改为 2。每次提交仅写入 OS Cache,每秒刷盘一次。
    # 直接将 IOPS 需求降低一个数量级。
    innodb_flush_log_at_trx_commit = 2
    
    # 针对支持原子写的存储设备(如现代 NVMe SSD 或部分企业级 SAN),关闭双写缓冲
    innodb_doublewrite = 0
    
    # 优化 Redo log 大小,防止频繁触发 Checkpoint 导致 IO 抖动
    innodb_redo_log_capacity = 4G # MySQL 8.0.30+ 的新参数,替代旧的 innodb_log_file_size
    

    2. 匹配硬件的 InnoDB IO 刷盘能力

    MySQL 默认假设你的磁盘很慢(innodb_io_capacity=200),这会导致在 SSD 环境下脏页刷得太慢,最终堆积引发急剧抖动。

    # 根据实际 FIO 测试结果配置
    innodb_io_capacity = 3000
    innodb_io_capacity_max = 6000
    innodb_flush_sync = OFF # 避免 Checkpoint 时卡死用户查询
    

    3. 表分区与废弃 Housekeeper

    导致底层 IO 缓慢的另一个隐患是 Zabbix 自带的 Housekeeper 清理进程。它通过 DELETE FROM history WHERE clock < ... 清理过期数据,这在海量数据下会产生巨大的锁竞争和 Undo Log 开销。

    必须彻底关闭 Housekeeper 对历史表和趋势表的清理: Web 界面:Administration -> General -> Housekeeping,关闭 History and TrendEnable internal housekeeping

    替代方案:使用 MySQL 表分区(Table Partitioning)。 每天为 historyhistory_uinthistory_str 等表建一个新分区,清理数据时直接 ALTER TABLE ... DROP PARTITION,这是元数据操作,耗时 0.1 秒,没有任何 IO 负担。

    自定义模板防作死指南:在 Proxy 侧掐断垃圾数据

    数据库调优只是续命,真正的治本之策是降低 NVPS(New Values Per Second)。排查中发现,某开发团队的自定义模板中,有一个抓取应用日志错误状态的 item,类型居然是 Text,且每 5 秒抓取一次。无论状态是否改变,全量文本都在往 DB 里塞,直接打爆了 history_str 表。

    过滤绝招:Discard unchanged with heartbeat

    利用 Zabbix 的 Preprocessing(预处理)功能,直接在 Proxy 内存中过滤掉无用数据,根本不让它通过网络发给 Server。

    配置步骤:

    1. 打开 Item 的 Preprocessing 选项卡。

    2. 添加 Step:Discard unchanged with heartbeat

    3. 参数设置为 1h(或 3600s)。

    原理解析: 如果该 Item 的值相比上次抓取没有发生变化,Proxy 会直接将这个数据丢弃,不往 Server 发送。只有当值发生变化,或者超过设定的 heartbeat 时间(比如 1 小时没有变化),才会发送一次数据保持激活。 仅仅配置了这一项,我们的整体 NVPS 从 35,000 直接断崖式下降到 12,000,MySQL IO 负载瞬间降至 15% 以下。

    常见问题

    Q1:Web 界面经常报 Zabbix Server is not running,但查看进程都在,怎么回事? 通常是因为 PHP 前端通过 TCP 10051 端口请求 Server 的 StartTrappers 进程超时。大概率是因为 History Syncer 阻塞,导致 Trapper 进程全都在等待获取 Cache 锁。检查数据库负载,或适当增加 StartTrappers 数量。

    Q2:StartPollers 到底设置多少合适?为什么我设了 1000 还是不够? 千万不要无脑调大 Poller 数量。Poller 过多会导致严重的上下文切换和内存消耗。查看 Zabbix server data collector processes 图表,如果 Poller 使用率长期 > 75%,首先应该考虑将监控项改为 Active 模式(让 Agent 主动推),或者把采集任务剥离给下层 Proxy。

    Q3:存在大量 SNMP 监控导致 Poller 经常超时卡死,如何缓解? SNMP 采用 UDP,极易丢包阻塞。最佳实践:1) 将 SNMP 采集全部下放给专属的 Zabbix Proxy,将故障隔离;2) 在 Host 级别勾选 Use bulk requests;3) 在 zabbix_server.confzabbix_proxy.conf 中增加 Timeout=15(默认只有 3 秒,对于老旧交换机绝对不够)。