标签: TimescaleDB

  • 深入 Zabbix 预处理雪崩排查:复杂 JSONPath 滥用引发的 Proxy 内存打爆与 TimescaleDB 写入夯死实战

    结论先行:某次 Zabbix 6.0 LTS 分布式集群雪崩,根因是自定义模板中滥用极其复杂的 JSONPath 与正则预处理,导致 Proxy 端 Preprocessing Worker 长期 100% 满载。堆积的历史数据在洪峰释放时,由于大量乱序时间戳,瞬间击穿后端 PostgreSQL 14 (TimescaleDB) 的 Chunk 写入性能,引发 Server 端 History Syncer 全面夯死。核心解法是将重度解析逻辑下沉至 Agent 端侧(边缘计算),并调优 TimescaleDB 历史数据的乱序写入内存参数。

    故障现场:Proxy 频繁断连与 Server 端 P99 延迟飙升

    排查过程中,核心监控集群突然触发大面积“Zabbix proxy is unreachable”告警。初步观察 Zabbix Server 的核心指标,发现 P99 内部处理延迟从平时的 50ms 飙升至 3s 以上,同时 History Syncer 进程利用率直线打满到 100%。

    登入其中一个出问题的 Proxy 节点抓取状态:

    # 检查 Proxy 内部进程状态
    zabbix_get -s 127.0.0.1 -k "zabbix[process,preprocessing worker,avg,busy]"
    100.000000
    
    # 查看 Proxy 日志,大量连接超时与积压
    tail -n 50 /var/log/zabbix/zabbix_proxy.log
    1345:202X1108:101231.123 Zabbix agent item "app.api.stats" on host "API-Server-01" failed: first network error, wait for 15 seconds
    1320:202X1108:101345.543 proxy data dispatching delayed by 4520 seconds
    

    更致命的是,当 Proxy 的 preprocessing worker 艰难处理完积压数据,开始向 Zabbix Server 批量推送时,Server 端的数据库层直接“躺平”。PostgreSQL 服务器的 Load Average 飙升至 120,磁盘 iowait 持续在 60% 以上。

    为什么自定义模板的预处理会拖垮整个 Proxy 分布式架构?

    在 Zabbix 的分布式架构中,Proxy 不仅仅是数据转发器。从 Zabbix 4.2 开始,为了减轻 Server 压力,所有的指标预处理(Preprocessing)都被前置到了 Proxy端执行。

    在本次故障中,业务团队新接入了一个自定义模板,通过 HTTP Agent 主动拉取某个中间件的 /metrics 接口。该接口返回一个高达 3MB 的巨型 JSON 文本。该模板定义了 1 个 Master Item,并挂载了 800 多个 Dependent Items,每个 Dependent Item 都配置了复杂的 JSONPath 提取规则,外加正则表达式(Regular Expression)进行二次清洗。

    底层原理在于:Zabbix 的预处理架构基于 Master-Worker 的进程间通信(IPC)模型。 preprocessing manager 接收到原始数据后,通过 Unix Socket 将庞大的 3MB 文本复制、分发给底层的 preprocessing worker。800 个 Dependent Item 意味着这 3MB 的文本要在内存中被拷贝并执行 800 次复杂的 JSONTree 解析与正则匹配。

    当数百台主机同时拉取该指标时,Proxy 的 CPU 缓存和 IPC 队列瞬间被挤爆:

    // zabbix/src/zabbix_proxy/preprocessing/preprocessing.c (伪代码逻辑)
    // 每次执行预处理步骤时,巨大的 values 字符串需要在 manager 和 worker 之间传递
    zbx_ipc_message_t *message;
    zbx_ipc_client_send(client, ZBX_IPC_PREPROCESSOR_REQUEST, data, data_size);
    

    单靠修改 zabbix_proxy.conf 里的 StartPreprocessors=50 根本无济于事,只会让系统的 Context Switch 飙升,加速内存 OOM。

    数据库后端崩塌:TimescaleDB IOPS 饱和与 History Syncer 夯死

    Proxy 积压了数小时的数据后,当处理完成并批量推给 Server 时,真正的灾难在数据库层爆发。Zabbix Server 的 History Syncer 进程开始向 PostgreSQL 疯狂写入 historyhistory_uint 表。

    由于这批数据带有数小时前的历史时间戳,它们触发了 TimescaleDB 最惧怕的场景:跨 Chunk 的大批量乱序写入(Out-of-order writes)。 正常情况下,TimescaleDB 写入最新的 Chunk,完全在内存中顺序追加,速度极快。但大量几小时前的积压数据涌入,导致 PostgreSQL 不得不将之前已经压缩并落盘的多个旧 Chunk 重新加载到内存中执行解压、插入、再压缩操作。

    通过 pg_stat_activity 捕获到了大量的锁争用:

    SELECT pid, wait_event_type, wait_event, query 
    FROM pg_stat_activity 
    WHERE state = 'active' AND query ILIKE '%INSERT INTO history_uint%';
    
    -- 结果显示大量进程阻塞在 IO 和 LWLock 上
    pid   | wait_event_type | wait_event     | query
    ------+-----------------+----------------+----------------------------------------
    24102 | IO              | DataFileRead   | INSERT INTO history_uint (itemid, clock, ns, value) ...
    24103 | LWLock          | buffer_mapping | INSERT INTO history_uint (itemid, clock, ns, value) ...
    

    buffer_mapping 锁的集中爆发,证明 shared buffers 正在被高频的 Chunk 换页操作击穿,底层的 NVMe 硬盘 IOPS 被完全打满。

    架构优化与防御性配置落地

    为了彻底解决这一类“监控即雪崩”的问题,我们需要从采集端、传输端和存储端进行三维阻断。

    1. 采集端:预处理逻辑下沉(Shift-Left Parsing)

    不要在 Zabbix 中处理 GB 级别的正则和 JSON 解析。改用 Zabbix Agent 的 UserParameter 或外部脚本,利用 jq 这样的底层 C 工具在客户端机器本地完成数据扁平化,仅将解析好的 Key-Value 上报给 Zabbix。 如果必须保留 HTTP Agent 拉取,强制要求研发侧提供精简版 Metrics 接口,拒绝接收超过 50KB 的 JSON 报文。

    2. 传输端:Proxy 预处理并发与积压限流

    zabbix_proxy.conf 中,防御性地配置预处理进程,并控制向 Server 同步积压数据的速率:

    # 限制预处理 Worker 数量,避免耗尽 Proxy 所在机器的 CPU
    StartPreprocessors=15
    # 避免 Proxy 恢复时向 Server 形成积压数据洪峰
    ProxyDataFrequency=1
    

    3. 存储端:TimescaleDB 的乱序写入与 Chunk 调优

    调整 PostgreSQL 配置以应对偶发的乱序历史数据。增加 max_locks_per_transaction,并调优 TimescaleDB 的 Chunk 跨度与压缩策略。 在本次故障后,将 history_uint 的 chunk 时间跨度修改为 1 天(原默认或较小值可能导致过多的小 chunk 被频繁换入换出),并推迟压缩时间,给乱序数据留出缓冲窗口:

    -- 修改 Chunk interval 为 1 天(86400000 毫秒)
    SELECT set_chunk_time_interval('history_uint', 86400000);
    
    -- 调整压缩策略,允许两天内的乱序数据直接写入未压缩的 Chunk
    SELECT remove_compression_policy('history_uint');
    SELECT add_compression_policy('history_uint', INTERVAL '2 days');
    

    同时调整 postgresql.conf,将 shared_buffers 扩大至系统内存的 25%-40%,并设置 maintenance_work_mem = 2GB,加速 Chunk 的维护操作。

    常见问题

    Q1:如何快速定位是哪个自定义模板的哪个 Item 堵死了 Proxy 的 Preprocessing Queue? 在 Zabbix Server 上执行 SQL 查询,找出包含复杂正则或长 JSONPath 的大范围应用项: SELECT h.host, i.name, p.params FROM item_preproc p JOIN items i ON p.itemid = i.itemid JOIN hosts h ON i.hostid = h.hostid WHERE p.type IN (11, 12); (11=XML XPath, 12=JSONPath)。或者通过打开 Proxy 的 DebugLevel=4,结合 grep "preprocessing worker" 过滤慢解析的 itemid。

    Q2:Proxy 在高并发 IO 下,本地的 SQLite3 数据库频繁出现 “database disk image is malformed” 损坏,如何解决? 企业级环境(特别是 NVPS > 500 的场景)严禁在 Proxy端使用 SQLite。其文件级锁极易在磁盘 IO 高负载时造成数据损坏。建议一律替换为 MySQL (InnoDB) 或 PostgreSQL,并配置合理的 innodb_buffer_pool_size

    Q3:Zabbix Server 的 History Syncer 经常出现 100% busy,但后端数据库 IO 和 CPU 利用率都很低,这是为什么? 检查 Zabbix Server 的 ValueCacheSize。如果 Value Cache 内存耗尽或命中率极低(大量触发低频冷数据查询),History Syncer 会被迫在同步写入的同时去数据库执行同步的 SELECT 读操作来刷新 Cache,由于单线程阻塞等待返回,导致进程自身 busy,但这不会在数据库层体现为高资源消耗。解决思路是大幅提高 zabbix_server.conf 中的 ValueCacheSize

  • 深入 Zabbix 监控雪崩排查:LLD 发现风暴引发的 Proxy 缓存积压与 History Syncer 夯死实战

    近期处理了一起 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 分片。

    迁移与落地步骤:

    1. 部署 PostgreSQL 14 与 TimescaleDB 插件。

    2. 导入 Zabbix 基础 Schema 后,务必执行 TimescaleDB 转换脚本:

    # Zabbix 6.0 环境下开启 TimescaleDB 支持
    zcat /usr/share/doc/zabbix-sql-scripts/postgresql/timescaledb.sql | sudo -u zabbix psql zabbix
    
    1. 在 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 发现。