分类: SRE实战

  • 深入 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 秒,对于老旧交换机绝对不够)。

  • 深入 TiDB 大事务雪崩排查:无脑 DELETE 引发的 Percolator 锁风暴与 TiDB 节点 OOM 惨案

    近期处理了一起极为惨烈的分布式数据库生产事故。核心业务集群(TiDB v6.1)的 P99 延迟在两分钟内从 20ms 直接飙升到 30s,随后多个 TiDB Server 节点接连触发 OOM 被内核直接 Kill,集群 QPS 跌至个位数,几乎处于瘫痪状态。

    排查到底,罪魁祸首是一条没有任何 LIMIT 限制、涉及 8000 万行数据的历史日志清理 SQL(DELETE FROM action_log WHERE create_time < '2023-01-01')。 结论先行:在基于 Percolator 模型的分布式数据库中,将单机关系型数据库的“大事务”思维直接照搬是自杀行为。TiDB 在两阶段提交(2PC)的 Prewrite 阶段需要将所有 Mutate 数据缓存在 TiDB Server 内存中,同时向 TiKV 写入海量 Lock 记录。这不仅会瞬间击穿计算节点的内存配额,还会引发大面积的锁冲突与 ResolveLock 风暴,导致整个集群的 Raft Store 与 Coprocessor 线程池耗尽。

    解决大批量数据修改,必须使用非事务 DML(BATCH ON)或按主键范围切分的批处理脚本。把分布式 DB 当无底洞垃圾桶,它就会把你的业务一起埋了。

    现场还原:从延迟突刺到死亡宣告

    监控大盘上的异动非常典型,呈现出教科书般的“雪崩”曲线:

    1. TiDB 节点内存垂直起飞:某一个 TiDB 节点的内存使用率在 60 秒内从 15% 飙升至 95%。

    2. 锁指标爆炸:TiDB Dashboard 中的 KV Backoff OPSLock Resolve OPS 激增 1000 倍。

    3. gRPC 阻塞:TiKV 的 gRPC message duration P99 飙升至 15s 以上。

    4. 死亡宣告:系统监控捕获到内核级斩首行动: text kernel: [123456.789] Out of memory: Kill process 2333 (tidb-server) score 850 or sacrifice child kernel: [123456.790] Killed process 2333 (tidb-server) total-vm:41943040kB, anon-rss:33554432kB, file-rss:0kB

    查看存活 TiDB 节点的 tidb.log,满屏的 2PC 提交失败与锁冲突报错:

    [WARN] [2pc.go:1234] ["commit failed"] [conn=889922] [error="[kv:9007]Write conflict, txnStartTS=441234567890123456 is stale"]
    [WARN] [backoff.go:234] ["txnLockNotFound"] [conn=889922] [caller="resolveLock"] 
    

    核心原理解析:为什么一条 DELETE 能干趴整个集群?

    很多开发习惯了 MySQL (InnoDB) 的行为,认为一条几千万行的 DELETE 最多就是跑得慢、产生大量 Undo/Redo log、导致主从延迟。但在 TiDB 这种计算与存储分离、基于 Percolator 事务模型的 HTAP 架构中,机制完全不同。

    一条巨型 DELETE 在 TiDB 的执行生命周期,就是一场灾难的酝酿过程:

    1. 计算节点内存撑爆 (TiDB OOM)

    TiDB 为了支持乐观/悲观事务,在事务提交前,会将所有修改(对于 DELETE,就是将被删记录的 Key 和空 Value)缓存在 TiDB Server 的内存中(memDB)。 8000 万行记录,如果每行转化出的 KV 占 200 Bytes,单条事务在内存中就需要硬吃至少 15GB 的堆内存。再加上 Go 语言在应对这种瞬间海量小对象分配时,GC 往往会严重滞后,导致实际 RSS 占用翻倍,轻松击穿 tidb_server_memory_limit 的软限制,直接被 OS OOM-Killer 带走。

    2. Prewrite 阶段的锁风暴 (Lock Storm)

    哪怕服务器内存够大扛住了第一波,在 2PC 的 Prewrite 阶段,TiDB 会向 TiKV 写入分布式的锁:

    • 从这 8000 万个 Key 中选出一个作为 Primary Key (Primary Lock)

    • 将剩余的 7999 万多条记录作为 Secondary Locks 写入 TiKV,并全部指向那个 Primary Lock。

    此时,TiKV 集群被灌入数千万个 Lock CF(Column Family)记录。如果其他正常的业务请求(哪怕是读操作)碰巧访问到了这 8000 万行数据中的任意一行,按照 Percolator 协议,读请求会被锁阻塞。

    3. ResolveLock 级联雪崩

    当正常请求遇到这些锁,且发现锁所属的事务持锁时间过长时,会尝试进行清锁操作(ResolveLock)

    • 读请求会去反查 Primary Lock 的状态,确认那个巨型事务到底提交了没有。

    • 由于巨型事务的 Primary Lock 所在 Region 可能正处于极高的负载中,反查 RPC 出现堆积和超时。

    • 海量的正常请求全部卡在 ResolveLock 阶段,TiKV 的 Coprocessor 线程池和 gRPC 线程池被彻底打满,导致全表甚至全库的请求响应卡死,这就是经典的读写相互阻塞

    防御性加固与解决方案

    修复这个烂摊子,第一步是立刻 Kill 掉那个执行 DELETE 的会话,但这只是止血。为了彻底杜绝此类问题,必须从架构配置和研发规范上进行双重封堵。

    1. 严格限制事务大小与内存配额

    不要指望开发自觉,必须在配置层面进行防御性斩断。检查并调整 TiDB 配置文件:

    [performance]
    # 限制单事务的最大容量,默认 100MB,最大不超过 1GB。绝不给跑百 GB 级别事务的机会。
    txn-total-size-limit = 104857600
    
    [mem-quota]
    # 限制单条 SQL 的内存使用,超过后触发 oom-action
    query = 1073741824 # 1GB
    oom-action = "cancel" # 默认通常是 cancel,确保内存超限时直接终止 SQL 而不是拖死节点
    

    注:在 TiDB v6.1+ 中,全局内存控制 server-memory-quotatidb_server_memory_limit 系统变量已经完善,但精细到 query 级别的 cancel 依然是防范 OOM 的最后一道防线。

    2. 使用非事务 DML 或分批处理

    对于大批量历史数据清理,正确的做法是将其切分为无数个小事务。TiDB 官方提供了一项专用于此类场景的功能:Non-transactional DML

    -- 将大 DELETE 拆分为基于主键或者时间范围的小批量操作
    BATCH ON id LIMIT 5000 
    DELETE FROM action_log WHERE create_time < '2023-01-01';
    

    这条语句会在 TiDB 内部自动按 id 划分范围,每次只在一个小范围内执行 DELETE 并独立提交,从而绕过事务大小限制,彻底避免长事务持有海量锁导致的 OOM 和锁风暴。

    3. TiKV 侧 RocksDB 与 Raft 调优

    排查中发现 TiKV OOM 或高负载,往往是因为写入量太大导致 RocksDB Write Stall。保证 block-cache 配置合理,不超过系统内存的 45%。对于高频批量删除业务,考虑调大 max-background-jobs 加速 Compaction,避免 Tombstone 过多导致后续查询扫描性能断崖式下跌。

    排查清单 (大事务与 OOM 问题速查)

    1. dmesg 与 OOM 确认:快速执行 dmesg -T | grep -i oom,确认 tidb-servertikv-server 是否被内核 Kill,排除网络分区导致的假死。

    2. 排查慢查询与内存大户:查询 INFORMATION_SCHEMA.SLOW_QUERY 或 TiDB Dashboard,按 Mem_maxProcess_time 倒序,揪出未加 LIMIT 或扫描行数极大的问题 SQL。

    3. 核对事务配额参数:检查集群的 txn-total-size-limit 参数是否被违规调大(正常业务不应超过 100MB)。

    4. 监控 Lock 冲突指标:在 Grafana -> TiDB -> KV Errors 面板中,重点观察 KV Backoff OPS (特别是 txnLocktxnLockFast),若该指标激增,说明集群存在大事务或热点记录的严重写冲突。

    5. 垃圾回收 (GC) 状态确认:大批量 DELETE 后,务必通过 mysql.tidb 表检查 GC Safe Point 是否正常推进。大量的无用版本积压会拖慢整个集群的物理读取效率。

  • 突破 OOM 死亡循环:Prometheus 高基数指标引发的 TSDB 内存雪崩与底层结构解析实战

    结论先行:Prometheus 频繁 OOM 且 WAL 截断失败,99% 的根因是高基数(High Cardinality)标签打穿了 Head Block 的倒排索引。底层 Gorilla 压缩算法只能极大地优化时序“值”的存储(16字节压缩至约1.37字节),但救不了无限膨胀的 Label 组合。解决方案:通过 promtool tsdb analyze 定位基数元凶,用 metric_relabel_configs 在抓取阶段实行防御性清洗,并合理配置 TSDB 的 Block 压缩与落盘周期。

    某次排查过程中,我们线上一套监控几十个 K8S 集群的核心 Prometheus(v2.45.0)节点陷入了 CrashLoopBackOff 的死亡循环。告警静默,监控大屏一片空白。

    查看系统内核日志,死因极其明确——被 OOM Killer 制裁:

    $ dmesg -T | grep -i oom
    [xxx] prometheus invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
    [xxx] Memory cgroup out of memory: Killed process 12345 (prometheus) total-vm:85493200kB, anon-rss:67108864kB
    

    物理机分配了 64GB 内存给这个容器,居然在几分钟内被吃干抹净。这绝不是正常的指标写入量增长,而是典型的“高基数雪崩”。

    寻找雪崩元凶:拨开 WAL 与 Head Block 的迷雾

    Prometheus 的 TSDB 设计基于内存(Head Block)与磁盘(Persistent Block)的组合。最新采集的 2-3 小时数据全部驻留在内存中,并依靠 WAL(Write-Ahead Log)保证不丢数据。每次 Prometheus OOM 重启后,第一件事就是 Replaying WAL。如果导致 OOM 的高基数数据还在 WAL 里,重启过程必将再次吃满内存,形成死循环。

    为了强行中断这个循环,我们先将该节点的内存限制临时放大到 128GB 让其启动,随后立即使用官方神器 promtool 对本地数据目录进行离线解剖:

    $ promtool tsdb analyze /prometheus/data
    ...
    # Top 10 label names with high memory usage:
    1: trace_id
    2: client_ip
    3: pod_ip
    
    # Top 10 series count by metric names:
    1: 4501230  http_request_duration_seconds_bucket
    2: 2100450  http_requests_total
    ...
    

    破案了。某业务研发在 http_requests_total 和耗时直方图里,顺手加上了 trace_idclient_ip 作为 Label。

    为什么仅仅是多加了一个 Label,就会耗尽上百 GB 的内存?

    很多开发对时序数据库有误解,认为“Prometheus 压缩率很高,多加个字段无所谓”。这就必须深入 TSDB 的底层数据结构来解释。

    在 Prometheus 中,一条时间线(Series)由 Metric Name 和一组 Label 键值对唯一确定http_requests_total{method="GET", status="200", client_ip="192.168.1.10", trace_id="abc123xxx"}

    TSDB 处理数据分为两大核心路径:数据块(Chunks)倒排索引(Inverted Index)

    1. Chunks 的极致压缩(Gorilla 算法) 对于属于同一条时间线的连续样本数据 (Timestamp, Value),Prometheus 采用了类似 Facebook Gorilla 论文中的 XOR 增量压缩算法。因为时间戳通常是规律递增的(如 15s 一次),Value 往往变化极小。通过计算差值的差值(Delta-of-Delta),一对原本需要 16 Bytes(8 Byte int64 时间戳 + 8 Byte float64 值)的样本,能被压缩到平均 1.37 Bytes。这就是大家常说的“高压缩率”。

    2. 倒排索引的内存黑洞 Gorilla 压缩对 Label 完全无效。为了能让 PromQL 飞速查询,Prometheus 必须为每一个 Label 的 Name 和 Value 建立倒排索引映射: Label (client_ip="192.168.1.10") -> [Series ID 1, Series ID 205...] 当引入 trace_id 这种几乎每次请求都不同的 Label 时,Series 的数量等于所有 Label 基数的笛卡尔积。 百万级别的 trace_id 瞬间生成了数百万条独立的 Series。每条全新 Series 的诞生,都会在 Head Block 中分配新的字符串内存(Symbols table)、新的倒排索引指针(Postings list),以及独立的数据 Chunk。内存消耗呈现指数级爆炸,且完全无法被压缩。

    当这些海量的内存结构积压在 Head Block(默认驻留时间最多达 3 小时),内存自然会瞬间被打穿。

    落地实战:防御性清洗与架构调优

    对于这种毒瘤级的指标,我们绝不妥协,必须在网关侧/采集侧直接干掉,实施“防御性运维”。

    1. 采集端防御配置(metric_relabel_configs) 在 Prometheus 的 scrape_configs 中,利用 metric_relabel_configs 在指标进入 TSDB 引擎前将其截杀。注意,不要用 relabel_configs(作用于 target 发现阶段),必须用 metric_relabel_configs

    scrape_configs:
      - job_name: 'business-api'
        ...
        metric_relabel_configs:
          # 方案一:直接丢弃整个包含了违规 Label 的指标序列(下手最狠)
          - source_labels: [trace_id]
            regex: '.+'
            action: drop
    
          # 方案二:保留指标,但抹除高基数 Label(推荐,保证监控不丢失总并发量)
          - regex: '(trace_id|client_ip)'
            action: labeldrop
    

    加载配置 (curl -X POST http://localhost:9090/-/reload) 后,新的数据洪流被清洗干净。

    2. TSDB 块生命周期(Compaction)调优 为了让 Prometheus 尽快清理掉历史遗留的庞大 Head Block,我们需要理解 TSDB 的落盘(Compaction)机制。 Head Block 中的数据达到特定条件会切分成持久化的 Block 目录(包含 meta.json, index, chunks/, tombstones)。

    如果服务器内存吃紧,我们可以适当干预落盘周期(启动参数配置):

    # 默认 min-block-duration 为 2h,决定了 Head 块多久切片一次落盘。
    # 强制保持一致,避免块过大难以合并
    --storage.tsdb.min-block-duration=2h
    --storage.tsdb.max-block-duration=24h
    

    持久化到磁盘后的 Block 会被通过 mmap 的方式映射到虚拟内存空间(VIRT),此时只要不进行全量范围的 PromQL 查询,这部分数据对物理内存(RES)的占用将大幅度降低,由 Linux 内核的 PageCache 全权接管。

    3. 清理已存在的毒瘤数据(Tombstones 机制) 对于历史的脏数据,可以使用 Admin API 软删除:

    curl -X POST -g 'http://localhost:9090/api/v1/admin/tsdb/delete_series?match[]={trace_id=~".+"}'
    

    执行后,数据不会立刻从磁盘消失,而是写入到 Block 目录下的 tombstones 文件中。后续的 Compaction 过程会读取该文件并真正剔除无用数据。如果需要强制立即清理磁盘,可调用:

    curl -X POST http://localhost:9090/api/v1/admin/tsdb/clean_tombstones
    

    常见问题 (FAQ)

    Q1:为什么通过 metric_relabel_configs 删除了高基数 Label,Prometheus 的物理内存(RES)并没有立刻下降? A:这是符合预期的。由于 Prometheus TSDB Head Block 的机制,数据通常要在内存中攒满 2 小时(加上最长允许的 1 小时 overlap)才会执行落盘并释放内存。即使新抓取的数据不再含有高基数标签,旧的庞大倒排索引依然存活在内存里。你需要耐心等待下一次 Head 切片,或者干脆重启进程,配合之前放宽的内存上限让 WAL 重放完成后,内存自然回落。

    Q2:Prometheus 发生 OOM 重启后,启动特别慢,日志一直卡在 Replaying WAL 是什么情况? A:Prometheus 只有正常退出时,才会将 Head Block 里的内容做 Checkpoint 或者全量 Flush 落盘。OOM 属于非正常崩溃,内存数据丢失。重启后,它必须逐行读取 data/wal/ 目录下的日志以在内存中重建倒排索引和 Chunks。如果 WAL 高达几十个 GB,这个过程将极其漫长(且高度依赖磁盘 IOPS)。建议将 TSDB 部署在企业级 NVMe SSD 上,这是监控系统的底线。

    Q3:我可以通过降低抓取频率(将 scrape_interval 从 15s 调整为 60s)来缓解高基数导致的 OOM 吗? A:不能,这是经典误区。scrape_interval 影响的是同一条 Series 每分钟追加的样本点数量。这部分数据被 Gorilla XOR 算法高效压缩,占用极小。导致 OOM 的是 Series 的总数(基数规模)膨胀,进而撑爆了不可压缩的倒排索引表。无论是 15s 抓取一次还是 1 分钟抓取一次,只要这几百万个带唯一 trace_id 的 Label 依然存在,生成的索引内存消耗是一模一样的。