标签: 延迟雪崩

  • 深入 Apache Pulsar 延迟雪崩排查:Bookie Journal 刷盘阻塞引发的 P99 飙升与 IO 瓶颈实战

    近期排查了一起 Pulsar 集群(v2.10.4)P99 写入延迟从 5ms 突增至 2s 的雪崩故障。核心根因是 BookKeeper 的 Journal 与 Ledger 未做物理盘隔离,且 RocksDB Compaction 引发大量写放大,导致 fsync 阻塞 IO 线程组。通过硬隔离 NVMe 盘、开启 Direct IO 并优化 Group Commit 参数,最终将 P99 压回 2ms 以内。

    故障现场:P99 延迟雪崩与 Broker 背压

    排查过程中,监控告警显示某核心业务 Topic 的写入耗时剧增。查看 Prometheus 监控,Broker 侧的 pulsar_broker_publish_latency P99 指标平时稳定在 5ms,突发流量下直接飙升到 2000ms 以上,甚至触发了 Client 端的 TimeoutException

    登录其中一台 Bookie 节点(BookKeeper 4.14.5),直接用 dmesgiostat 摸底硬件状态:

    # 每秒打印一次 IO 状态
    iostat -x -d 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    0.00 4521.00     0.00 74523.00    32.96    12.45   15.20    0.00   15.20   0.22 100.00
    

    可以看到 nvme0n1%util 已经被打到 100%,且 w_await 高达 15ms(对于 NVMe 来说,超过 2ms 就已经说明底层队列严重排队了)。

    翻看 Bookie 的 bookkeeper.log,果不其然,满屏的 Journal Sync 超时告警:

    22:14:05.123 [bookie-journal-DIR-sync] WARN  org.apache.bookkeeper.bookie.Journal - Sync object took 1250 ms
    22:14:05.345 [bookie-journal-DIR-sync] WARN  org.apache.bookkeeper.bookie.Journal - Sync object took 1420 ms
    22:14:06.001 [bookie-journal-DIR-sync] ERROR org.apache.bookkeeper.bookie.Journal - Journal Sync queue is full, dropping requests!
    

    为什么明明是存算分离架构,Bookie 的 Journal 刷盘依然会被阻塞?

    Pulsar 吹嘘最多的就是“存算分离”和“读写分离”。但在 BookKeeper 底层,读写分离是有前提的。

    BookKeeper 的一次写入流程(Add Entry)包含两步核心操作:

    1. Append to Journal:将数据追加到 Journal 盘(WAL),并在内存中将数据加入 MemTable。只要 Journal 刷盘(fsync)成功,Bookie 就向 Broker 返回 ACK。

    2. Flush to Ledger:后台线程定期将 MemTable 刷入 Entry Logger(数据文件)和 RocksDB(索引文件),这两个文件通常存放在 Ledger 盘。

    理论上,Journal 负责顺序写,Ledger 负责随机读和后台批量写,互不干扰。但现实很骨感:如果你的 Journal 和 Ledger 共享同一块物理盘,或者共享同一个 PCIe 通道/阵列卡带宽,灾难就会发生。

    在我们的现场,之前的运维人员为了图省事,将 /bookkeeper/data/journal/bookkeeper/data/ledger 挂载到了同一块 2TB 的 NVMe 盘上。当业务触发海量小消息写入时,Ledger 目录下的 RocksDB 疯狂进行 Compaction(文件合并),产生极其严重的写放大。底层文件系统的 Page Cache 被瞬间打满,导致 Journal 线程调用 fsync() 时,被 OS 的脏页回写机制死死卡住。

    Journal 线程一卡,Broker 发来的新请求全部堆积在 Bookie 的 Netty 队列里,进而引发 OOM 风险,最终触发 Broker 侧的背压(Backpressure),全局写入延迟雪崩。

    解决与加固:防御性 IO 隔离与参数调优

    知道了痛点,解决思路就很清晰:物理隔离与绕过 Page Cache 污染。

    1. 物理盘绝对隔离

    永远不要让 Journal 和 Ledger 抢 IO。Journal 盘需要极低的 fsync 延迟,容量不需要太大(只保存 WAL,写满后会滚动删除);Ledger 盘需要大容量,对 IOPS 也有一定要求(特别是有 Catch-up 追赶读的场景)。

    实战配置

    • Journal:挂载独立的 100G NVMe SSD。

    • Ledger:挂载大容量的 SATA SSD 或阵列。

    bookkeeper.conf 中明确拆分:

    journalDirectories=/data/journal/bk
    ledgerDirectories=/data/ledger/bk
    

    2. 启用 Ledger Direct IO,避免 Page Cache 污染

    Ledger 的后台 Flush 和 Catch-up 读非常容易污染操作系统的 Page Cache,导致真正需要 Page Cache 加速的读请求被驱逐,甚至影响其他进程的 IO 性能。 在 BookKeeper 4.14+ 中,建议开启 DbLedgerStorage 的 Direct IO,直接让读写绕过 OS 缓存。

    # 使用 DbLedgerStorage
    storage_facility=org.apache.bookkeeper.bookie.DbLedgerStorage
    
    # 针对 Entry Logger 开启 Direct IO (绕过 Page Cache)
    dbStorage_directIOEntryLogger=true
    
    # 分配给 RocksDB Block Cache 的内存(建议占节点物理内存的 10%-20%)
    dbStorage_rocksdbBlockCacheSize=4294967296
    # 增加 Write Cache 以减少高并发下的 RocksDB 刷盘频次
    dbStorage_writeCacheMaxSizeMb=1024
    

    3. Journal Group Commit (组提交) 调优

    Journal 是靠 fsync 保证数据不丢的(journalSyncData=true 绝对不能关,关了就是拿命在跑)。但高并发下,每一条消息都 fsync 会让 NVMe 直接瘫痪。 我们需要调整 Group Commit 参数,让 Bookie 在延迟和吞吐之间找到一个平衡。

    # 最大等待时间,默认 1ms。如果你的网络/磁盘稍有抖动,1ms 太激进。建议改为 2-5ms,牺牲微小延迟换取巨大吞吐。
    journalMaxGroupWaitMSec=2
    
    # 队列中积压多少字节触发一次强制刷盘,默认 512KB,可适当调大到 1MB 或 2MB
    journalBufferedWritesThreshold=1048576
    
    # 如果 Journal 盘写入排队依然严重,可以增加 Journal 目录的数量(多盘配置)
    journalDirectories=/data/journal1/bk,/data/journal2/bk
    

    重启 Bookie 节点后,再次观察 iostatnvme0n1(Journal盘)的 w_await 稳定在 0.5ms 以内,Pulsar 生产端的 P99 写入延迟死死压在 2ms,故障彻底解除。

    常见问题 (FAQ)

    Q:为什么开启了存算分离,消费端(Consumer)追赶历史数据(Catch-up Read)时,还是会导致生产端(Producer)延迟抖动? A:如果没有开启 Ledger 的 Direct IO,追赶读会把大量历史冷数据加载到 OS Page Cache,把操作系统的空闲内存耗光,触发 OS 的全局脏页回写(Global Flush)。此时哪怕 Journal 在另一块盘上,CPU 和总线也会受到系统态 IO 阻塞的影响。解决方案就是强制配置 dbStorage_directIOEntryLogger=true

    Q:journalSyncData=false 能不能用来解决 P99 高的问题? A:可以解决监控上的数字问题,但会解决掉你的饭碗。设置为 false 意味着数据写到 OS Page Cache 就直接向 Broker 返回成功,一旦 Bookie 所在机器断电或 Kernel Panic,尚未刷盘的数据将永久丢失。对于金融级或核心交易场景,必须保持为 true,靠优化硬件和 Group Commit 来提速。

    Q:如何预估和监控 RocksDB 引发的写放大? A:重点监控 Bookie 的 bookkeeper_server_DbLedgerStorage_compaction_time 和 IO 设备的写入量。如果发现 Ledger 盘的实际写入速率是业务写入速率的 3 倍以上,说明遇到了 RocksDB 的写放大瓶颈。可以通过调大 dbStorage_writeCacheMaxSizeMb 和限制后台 Compaction 线程的速率来缓解。

    Q:多租户场景下,个别异常租户的高频写入会不会打满 Bookie 导致全局故障? A:会。Bookie 底层是对所有 Topic 的 Entry 进行混合追加写的,一个 Topic 的流量突增会吃光 Journal 的 fsync 能力。必须在 Broker 端开启严格的资源配额(Quota)和按租户/Namespace 的流量限流(Rate Limiting),这叫“防御性运维”。不要指望底层存储能无限制抗住上层的不合理调用。