标签: Page Cache

  • 深入 Kafka 零拷贝陷阱排查:sendfile 阻塞引发的 ISR 频繁伸缩与 Leader 选举雪崩实战

    近期处理了一起 Kafka 集群(v2.8.1)核心业务 Produce 请求 P99 延迟突增至 500ms 以上的故障。核心结论是:落后消费者(Lag Consumer)的大量历史数据拉取,导致 Page Cache 严重颠簸;零拷贝 sendfile 系统调用退化为阻塞的同步磁盘读,耗尽了 Broker 的 Network Processor 线程,最终引发 Follower 同步超时、ISR 频繁伸缩甚至 Leader 重新选举。核心解法是通过 Quota 限制拉取速率,并调整 num.network.threads 与 OS 预读参数。

    现场还原:延迟突增与 ISR 震荡

    监控告警最先报出的是业务侧 Producer 发送超时。切到 Grafana 面板,几个核心指标的异动非常明显:

    1. Broker 负载:CPU Load Average 飙升,但主要集中在 iowait,磁盘 util 长时间顶在 100%。

    2. Kafka 线程池池NetworkProcessorAvgIdlePercent 指标从正常的 0.6(60% 空闲)断崖式跌落至 0.05 以下。

    3. Controller 日志:出现了大量的 ISR Shrink 和 Expand,紧接着部分 Partition 发生了 Leader 选举。

    查看 Controller 节点的 controller.log,满屏都是类似以下的日志:

    [202X-XX-XX 10:14:22,105] INFO [Controller id=1] Shrinking ISR from 1,2,3 to 1,2 for partition topic-core-order-15 (kafka.controller.KafkaController)
    [202X-XX-XX 10:14:52,431] INFO [Controller id=1] Expanding ISR from 1,2 to 1,2,3 for partition topic-core-order-15 (kafka.controller.KafkaController)
    

    直觉告诉我,磁盘 I/O 瓶颈拖垮了网络层,导致 Follower 的 Fetch 请求没能及时响应。用 iotopiostat -dxm 1 抓现场,发现 Broker 正在疯狂进行物理磁盘读(Read 吞吐达到 300MB/s,远超平时)。

    定位消费端,发现有一个大数据团队的离线补数任务,正在用几十个并发消费一周前的数据(Offset 极度落后)。

    为什么零拷贝(Zero-Copy)会退化为阻塞的磁盘 I/O?

    大家都背过八股文:Kafka 高性能的核心之一是 Zero-Copy。在 Linux 下,这依赖 sendfile 系统调用,数据流转路径是:磁盘 DMA -> Page Cache -> 网卡 Buffer,全程不需要 CPU 介入将数据拷贝到 User Space。

    但在真实高压场景下,Zero-Copy 是有陷阱的。

    使用 strace 跟踪 Kafka 的 Network Processor 线程:

    # 找到占用 CPU 最高的 Network 线程
    top -H -p <kafka_pid>
    # strace 跟踪该线程的系统调用,统计耗时
    strace -T -e trace=sendfile -p <network_thread_pid>
    

    输出令人绝望:

    sendfile(114, 256, [1453049102], 1048576) = 1048576 <0.452132>
    sendfile(114, 256, [1454097678], 1048576) = 1048576 <0.381204>
    

    单次 sendfile 调用耗时竟然高达 300~400ms!

    底层原理剖析: Kafka 架构中,Fetch 请求(无论是 Consumer 还是 Follower Replica)最终会在 NetworkProcessor 线程中执行数据发送。调用链为:KafkaApis.handleFetchRequest -> ReplicaManager.fetchMessages -> NIO FileChannel.transferTo -> 触发 OS sendfile

    当 Consumer 消费的是实时数据时,数据都在 OS Page Cache 中(Hot Data),sendfile 瞬间完成,Network 线程极速返回,继续处理下一个 Socket 请求。

    但是,当遇到落后极多的离线拉取任务时,要读取的数据早已被从 Page Cache 中驱逐。此时,sendfile 触发了 Page Cache Miss。OS 必须发起同步阻塞的磁盘 I/O,将数据从磁盘加载到 Page Cache。在这个漫长的物理寻道和读取过程中,Kafka 的 Network Processor 线程被死死卡住(Blocked)

    Kafka 默认的 num.network.threads 通常为 CPU 核数。一旦这几个线程全被 sendfile 阻塞在地狱里,Broker 就彻底丧失了处理网络请求的能力,新的 Produce 请求、甚至心跳请求都在 Socket 缓冲区排队,最终超时。

    ISR 频繁伸缩与选举的级联雪崩

    Network 线程耗尽,直接引发了集群内部状态机崩溃。

    1. Follower 同步中断:Follower Broker 后台的 ReplicaFetcherThread 会不断向 Leader 发送 Fetch 请求同步数据。Leader 的 Network 线程因为处理离线任务的 sendfile 卡死,无法响应 Follower。

    2. 触发 ISR 剔除:当 Follower 的请求在 Leader 端超时超过 replica.lag.time.max.ms(默认 30000ms),Leader 的 ZK 协调机制会认为 Follower 挂了,将其从 ISR(In-Sync Replicas)列表中踢出(Shrink)。

    3. 恢复与震荡:等阻塞稍微缓解,Follower 成功拉取到数据追平了 LEO(Log End Offset),又会被加回 ISR(Expand)。

    4. Leader 崩溃假象:如果 Broker 拥塞过于严重,导致与 Zookeeper 的心跳(Session Timeout 默认 18s)断开,Controller 会认为该 Leader Broker 宕机,强行触发 Leader Election,将流量切向其他 Broker,引发全量元数据更新,导致 P99 彻底爆炸。

    解决与防御性配置实践

    面对这种架构上的“硬伤”(除非重构底层网络模型,否则 Kafka 很难彻底分离冷热数据的网络发送),我们需要在运维和配置侧进行防御。

    1. 强制客户端限流(Client Quotas)

    防范落后消费者的最有效手段是限制其网络吞吐,避免单点打爆。

    # 限制客户端 client-id=offline-batch-job 的拉取速率为 20MB/s
    bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'consumer_byte_rate=20971520' --entity-type clients --entity-name offline-batch-job
    

    注:Quota 的限流机制是在 Network 线程处理完后增加 Delay,虽然不能彻底阻止 sendfile 的初次阻塞,但能显著降低冷读并发频率,给其他热请求留出喘息窗口。

    2. 增加 Network 线程池水位

    对于磁盘性能一般、但经常有回溯消费场景的集群,默认的 Network 线程数是不够用的。修改 server.properties

    # 默认通常为 3 或 CPU 核数。对于大内存/高并发冷读场景,建议调大至 CPU 核数的 2-3 倍
    num.network.threads=32
    # 适当增加 I/O 线程
    num.io.threads=16
    

    核心逻辑是:既然部分线程注定要被冷数据的 sendfile 阻塞,那就多开一些线程,保证总有空闲线程能处理快速的 Produce 和热 Fetch 请求。

    3. OS 层面的 Page Cache 与预读调优

    避免冷读打满 IOPS,可以适当调整块设备的预读(Read-Ahead)窗口。Kafka 的顺序读特性非常依赖这个参数。

    # 查看当前预读扇区数(通常默认 256 = 128KB)
    blockdev --getra /dev/sdb
    # 调大至 8192 (4MB),利用顺序磁盘 I/O 带宽换取 IOPS,减少缺页中断次数
    blockdev --setra 8192 /dev/sdb
    

    同时调整内核刷脏策略,避免后台写 I/O 挤占读 I/O:

    sysctl -w vm.dirty_background_ratio=5
    sysctl -w vm.dirty_ratio=80
    

    常见问题

    Q1:为什么调大 num.io.threads 对解决零拷贝卡顿没有明显效果? Kafka 的请求处理模型中,Produce 请求是由 Network 线程放入 RequestChannel,再交由 IO 线程真正写盘。但对于采用零拷贝的 Fetch 请求,Kafka 为了极致性能,是由 NetworkProcessor 线程直接通过 FileRecords.writeTo(底层 sendfile)将数据灌入 Socket 的。因此,sendfile 的阻塞发生在 Network 线程,调整 num.io.threads 对此无能为力。

    Q2:如何监控集群是否正在发生严重的 Page Cache 颠簸? 除了直接看磁盘 IO Util 和 NetworkProcessor 闲置率,可以通过 node_exporter 抓取 node_vmstat_pgpgin(缺页换入)和 node_memory_Buffers_bytes / node_memory_Cached_bytes 的波动幅度。如果 Cache 命中率骤降伴随 pgpgin 飙升,说明发生严重的冷读。更底层可以用 bcc-tools 的 cachestat 命令实时追踪命中率。

    Q3:升级到 Kafka 3.x 使用 KRaft 模式能解决这个问题吗? 不能直接解决。KRaft 模式移除了 Zookeeper,极大优化了 Controller 的选举速度和元数据恢复耗时(将级联雪崩的恢复时间从分钟级降到秒级甚至毫秒级)。但 Data Plane 的 sendfile 阻塞本质是 NIO 网络模型与 OS 文件系统的耦合问题,KRaft 并未改变网络处理的底层模型。解决冷热数据分离的根本途径还是向 Tiered Storage(分层存储,如 KIP-405)演进。

  • 深入 io_uring 陷阱排查:Buffered IO 回退引发的 io_wq 耗尽与 XFS D状态假死实战

    某次核心存储网关进行底层 IO 引擎重构,开发团队信誓旦旦地将传统的线程池同步读写替换为 io_uring,期望实现吞吐量的降维打击。结果上线灰度不到半小时,节点 Load Average 直接飙到 300+,QPS 从 5w 呈断崖式暴跌至 300,P99 延迟更是惨不忍睹地拉平到了 8 秒以上。

    最终结论先行: 这是一起典型的“只懂调用 API,不懂内核 IO 栈”引发的惨案。开发团队在使用 io_uring 提交读写任务时,文件句柄没有开启 O_DIRECT,导致 IO 操作回退为 Buffered IO。 在高并发写压迫下,大量 io_uring 任务因触发内核 Page Cache 分配阻塞或 XFS inode 元数据锁(xfs_ilock),被强行剥离给 io-wq 工作线程池同步执行。最终,所有 io-wq 线程全部陷入 D 状态(Uninterruptible Sleep),整个异步 IO 引擎彻底退化为大规模的同步锁竞争现场,引发系统级假死。

    修复方案: 文件打开必须携带 O_DIRECT 标志位,并确保应用层内存按照块设备扇区大小(通常 512B 或 4KB)对齐,彻底绕过 Page Cache,让 io_uring 真正走到底层块设备的异步 DMA。

    故障现场:安静的 CPU 与暴走的 Load

    排查过程中,第一眼看监控面板是非常诡异的:CPU 整体使用率(us+sy)不到 15%,但 wa (iowait) 却高达 60%。同时,Load Average 突破天际。

    通过 iostat -x 1 观察到底层 NVMe 盘的状态:

    Device            r/s     w/s     rkB/s     wkB/s   rrqm/s   wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util
    nvme1n1          12.0  2540.0     192.0  128500.0     0.0     0.0    0.0    0.0    0.80  145.20  280.50    16.00    50.59   0.38 100.00
    

    IOPS 只有 2000 出头,带宽 120MB/s 远未达到 NVMe 的瓶颈,但 %util 已经被打满到 100%,aqu-sz (队列深度) 严重积压。

    切到机器上,揪出一个处于 D 状态的业务进程,直接看它的内核调用栈:

    cat /proc/`pidof storage-gateway | awk '{print $1}'`/task/*/stack | grep -A 5 -B 5 "xfs_ilock"
    

    满屏类似的堆栈:

    [<0>] xfs_ilock+0x135/0x250 [xfs]
    [<0>] xfs_file_buffered_write+0xe4/0x300 [xfs]
    [<0>] new_sync_write+0x114/0x1a0
    [<0>] vfs_write+0x1d5/0x270
    [<0>] io_write+0xe5/0x340
    [<0>] io_issue_sqe+0x5c/0x1c0
    [<0>] io_wq_submit_work+0x8f/0x290
    [<0>] io_worker_handle_work+0x153/0x2e0
    [<0>] io_wqe_worker+0x2c6/0x370
    [<0>] ret_from_fork+0x1f/0x30
    

    深度扒皮:io_uring 的异步伪装术

    看到 io_wqe_workerxfs_file_buffered_write 同时出现,基本就可以断定这是一场由 Buffered IO 引起的血案了。

    很多开发者对 io_uring 存在不切实际的幻想,认为只要把 read/write 系统调用换成 io_uring_enter,内核就会施展魔法让一切变成异步。在 Linux VFS 层,如果文件未按 Direct IO 打开,所有的写入都要先进入 Page Cache。

    当内存充足且未触发回写时,Buffered Write 确实快;但当系统内存吃紧,或者高并发引发 XFS 锁竞争时(比如同时追加写入同一个文件需要获取 xfs_ilock 排他锁),当前上下文就会被阻塞。

    io_uring 设计之初就考虑到这一点。为了防止提交队列(SQ)的轮询线程被内核级锁卡死,内核态的 io_uring 调度器做了一个妥协:当发现当前操作可能阻塞(比如 Page Cache 未命中或需要拿 VFS/文件系统锁)时,它会立刻返回 -EAGAIN,并将这个 SQE(提交队列实体)打包扔给后台的 io-wq(内核工作线程池)。

    这就是为什么我们在堆栈里看到了 io_wq_submit_work。开发者的本意是实现数万并发的无阻塞 IO,结果全被内核降级成了 io-wq 线程池的同步阻塞。当 io-wq 的线程被全部卡死在 xfs_ilock 等待上时,新的 IO 请求无处安放,应用层 io_uring 队列被打满,最终导致全局雪崩。

    为什么 XFS 锁竞争如此激烈? 在该场景中,业务是海量的小包追加写(Append Write)。在 XFS/Ext4 中,Append 写不仅需要分配新的块,还要更新 Inode 的 i_size 元数据。这意味着必须要拿 xfs_ilock 的互斥锁(Exclusive Lock)。没有 Direct IO,没有对齐的并发大块写入,海量的小 Buffered Write 硬生生把高性能文件系统逼成了串行处理机。

    避坑与防御性配置

    把 F1 赛车开进泥潭,然后抱怨车速慢,这是对现代内核 IO 栈最大的侮辱。要真正榨干 io_uring 和 NVMe 的性能,必须遵循严苛的规则。

    1. 强制开启 O_DIRECT: 这是使用 io_uring 处理文件 IO 的基石。 c int fd = open("data.db", O_RDWR | O_DIRECT | O_DSYNC); 只有使用 O_DIRECT,内核才会跳过 Page Cache,通过 get_user_pages 锁定用户态内存,直接将其映射给块设备的 DMA 引擎。这时候,io_uring 才会在块层真正的异步回调结束时产生 CQE(完成队列实体),绝对不会阻塞提交线程。

    2. 严格的内存对齐(Memory Alignment): 开启 Direct IO 后,用户态用于承载读写的 Buffer 内存地址,以及写入的 offset 和 length,必须是底层块设备逻辑扇区大小(通常为 512B 或 4096B)的整数倍c void *buffer; // 假设 4K 扇区对齐 if (posix_memalign(&buffer, 4096, 4096) != 0) { // error handling }

    3. 配合 XFS 的 Extent 预分配: 如果是频繁追加写,即便用了 O_DIRECT,扩展文件大小依然会触发元数据锁争用。极客的做法是使用 fallocate 预分配大块文件空间,将 Append Write 转变为 Overwrite,彻底消除写入过程中的 xfs_ilock 竞争。

    同类问题速查排查清单 (Checklist)

    1. 核对 D 状态调用栈:使用 cat /proc/$(pidof YOUR_APP)/task/*/stack | grep -i wqe,如果大量出现 io_wqe_workerfile_buffered_writexfs_ilockfolio_wait_bit,百分百是未开启 O_DIRECT 导致的回退阻塞。

    2. 审查 O_DIRECT 标记:使用 strace -p $PID -e openat 或通过 lsof -p $PID 观察 FD,确认核心数据文件是否均带有 O_DIRECT 标志。

    3. 排查系统 io-wq 线程数量:当 io_uring 降级时,会创建大量 iou-wrk 线程。ps -ef | grep iou-wrk 若输出成百上千行,说明你的异步引擎已经彻底退化为同步线程池。

    4. 验证 XFS 元数据碎片:执行 xfs_bmap -v /path/to/file,如果一个文件的 extents 碎片高达几十万个,说明缺少 fallocate 预分配,导致文件系统元数据分配严重拖慢 IO 提交。

  • 深入 NUMA 内存失衡排查:zone_reclaim_mode 引发的 THP 压缩阻塞与局域 OOM 击穿实战

    结论先行。针对 Elasticsearch/Kafka 等重度依赖 mmap 和 Page Cache 的应用,彻底关闭 THP(never)、设置 vm.zone_reclaim_mode=0 并强制 numactl --interleave=all 是规避 NUMA 局域 OOM 的铁律。跨 NUMA 访问的纳秒级延迟惩罚,远低于本地 Node 深度回收(Direct Reclaim)与大页压缩(Compaction)带来的秒级 I/O 夯死。

    现场还原:Load 飙升与诡异的毛刺

    某次排查中,业务反馈一个基于 Elasticsearch 7.17(底层系统为 Ubuntu 20.04,Kernel 5.4.0)的日志集群 P99 写入延迟出现极规律的剧烈抖动。平时延迟在 10ms 左右,但每隔几小时就会突发飙升至 2000ms+,伴随 Load Average 瞬间冲高到 80 以上。

    登录机器初步勘查,物理内存 256GB,JVM Heap 配置为 31GB(为了利用指针压缩),理论上剩余的 200GB+ 都会被 OS 用于 Page Cache 加速 mmap 读写。通过 free -g 查看,系统整体还有近 80GB 的 available 内存。

    然而,在查阅 /var/log/syslog 时,却发现了明确的 OOM Killer 介入日志:

    [51234.567890] java invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
    [51234.567895] CPU: 12 PID: 14532 Comm: java Tainted: G        W         5.4.0-122-generic #138-Ubuntu
    [51234.567901] Node 0 Normal free:45056kB min:45056kB low:56320kB high:67584kB
    [51234.567902] Node 0 Normal: 452*4kB (UME) 310*8kB (UME) ...
    [51234.567905] Node 1 Normal free: 83886080kB min:45056kB low:56320kB high:67584kB
    

    注意看日志里的致命细节:Node 0 的 free 内存已经触底(约 45MB,达到了 min watermark),而 Node 1 竟然还有 80GB 的空闲内存!

    性能观测:找出幕后黑手

    为了弄清为什么系统宁愿 OOM 也不用 Node 1 的内存,我拉起了常规的观测工具链。

    通过 numastat -m 查看 NUMA 节点的内存分布:

    $ numastat -m
                                 Node 0          Node 1           Total
                     --------------  --------------  --------------
    MemTotal                 128000          128000          256000
    MemFree                      43           81920           81963
    MemUsed                  127957           46080          174037
    Active                   110540           20480          131020
    Inactive                  12400           22500           34900
    

    Node 0 已经被彻底榨干。在延迟飙升期间,使用 perf top -p 抓取内核态调用栈,发现 CPU 极度密集地消耗在以下几个函数上:

    1. compaction_alloc

    2. isolate_freepages

    3. shrink_page_list

    同时,通过 /proc/vmstat 观察系统计数器,发现 compact_stallthp_fault_fallback 两个指标在毛刺期间呈现出几乎垂直的增长。

    为什么整体内存充足,却依然触发了局域 OOM 与 mmap 阻塞?

    这是一个典型的由 NUMA 架构默认分配策略、zone_reclaim_mode 回收机制以及 THP(透明大页)碎片整理共同酿成的惨剧。我们层层剖析。

    1. NUMA 的 Local Allocation 陷阱

    现代多路服务器默认开启 NUMA(Non-Uniform Memory Access)。Linux 内核默认的内存分配策略是 default,即优先在当前进程运行所在的 NUMA 节点上分配内存。 Elasticsearch 的主进程启动后,如果被调度器主要分配在 Node 0 的 CPU 上执行,它产生的大量 mmap 缺页中断(Page Faults)会疯狂吃掉 Node 0 的内存构建 Page Cache。最终,Node 0 被填满,而 Node 1 在旁边“看戏”。

    2. zone_reclaim_mode 引发的 Direct Reclaim 阻塞

    当 Node 0 的内存达到 low 水位线时,内核有两种选择:

    • A: 去 Node 1 借用空闲内存。

    • B: 强行在 Node 0 本地进行内存回收(驱逐 Page Cache 或 Swap)。

    内核如何决策?取决于 vm.zone_reclaim_mode 的值(以及节点间的距离 node_distance)。 在部分发行版或 BIOS 设置下,当 NUMA 节点距离较远时,系统倾向于在本地强行回收。此时如果业务正在高并发地写入,后台的 kswapd0 回收速度跟不上分配速度,内核就会挂起当前申请内存的用户态线程,进入Direct Reclaim(直接回收)路径。 shrink_page_list 就是在疯狂扫描和驱逐 Node 0 上的 Page Cache。这对于极度依赖 mmap 的 ES 和 Kafka 来说,相当于把热数据从内存里生生挖掉,下一次访问直接产生严重的磁盘 I/O 停顿。

    3. THP(Transparent Huge Pages)的致命一击

    如果只是缺内存,驱逐 Page Cache 最多带来 I/O 延迟。但 perf top 中的 compaction_alloc 揭示了更严重的问题:透明大页(THP)正在进行内存碎片压缩。 内核默认开启了 THP(madvisealways),试图为进程分配 2MB 的连续物理大页以减少 TLB Miss。当 Node 0 内存碎片化严重,没有连续的 2MB 空间时,内核的 khugepaged 或者触发 Direct Compaction 的线程会强行移动内存页面,试图“拼凑”出 2MB 的连续空间。 这个过程需要获取 Zone 级别的锁,会完全阻塞该 NUMA 节点上的其他内存分配请求。此时,业务看到的现象就是:机器负载瞬间飙到 80+,所有的写请求全部卡死(Hang),直到压缩超时或失败回退(thp_fault_fallback),随后由于 Node 0 实在挤不出哪怕 4KB 的内存,触发 OOM Killer 杀掉进程。

    核心调优实战与防御性配置

    不要迷信 OS 的默认配置,对于高吞吐的 DB/存储类应用,以下三步是必须落地的防御性基线:

    1. 强制 NUMA 内存交错分配(Interleave)

    通过 numactl 覆盖默认的本地分配策略,让应用在所有 NUMA 节点上均匀分配内存,彻底打散 Page Cache。 修改 ES 或 Kafka 的 systemd service 文件:

    [Service]
    # 将原来的 ExecStart 替换为带 numactl 的版本
    ExecStart=/usr/bin/numactl --interleave=all /usr/share/elasticsearch/bin/elasticsearch
    

    注:很多老鸟会担心 Interleave 带来的跨节点访问延迟(约增加 10~20 纳秒)。但在存储类系统中,因为局域内存耗尽引发的磁盘 I/O 阻塞(毫秒级甚至秒级),其代价是纳秒级跨节点延迟的 100,000 倍以上。

    2. 关闭 THP 与调整 zone_reclaim_mode

    透明大页对于 Redis/ES/Kafka 这类内存访问极度随机、频繁分配释放的应用,百害而无一利。必须在内核层彻底关闭,同时禁止本地激进回收。

    写入 /etc/sysctl.d/99-sysctl.conf

    # 优先去其他 Node 借用内存,绝不强行在本地发起深度回收
    vm.zone_reclaim_mode = 0
    # 降低 Swap 倾向,保护 Page Cache
    vm.swappiness = 1
    # 预留总内存的 1%-2% 给内核态,防止网络突发包导致网卡/内核分配内存失败触发直接回收
    # 256G 内存建议设置为 2G (2097152) 到 4G
    vm.min_free_kbytes = 2097152
    

    关闭 THP(不要只改 sysfs,建议写到 grub 引导参数里彻底干掉): 编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 中追加: transparent_hugepage=never 执行 update-grub 并重启系统。若不重启,可通过以下命令即时生效:

    echo never > /sys/kernel/mm/transparent_hugepage/enabled
    echo never > /sys/kernel/mm/transparent_hugepage/defrag
    

    3. OOM Score 防御性保护

    对于关键存储进程,适当调低其 OOM Score,防止在极端情况下被内核误杀。可以在启动脚本中注入:

    echo -500 > /proc/$$/oom_score_adj
    

    常见问题 (FAQ)

    Q1:如何判断我现在的系统有没有受到 THP 的性能毒害? 查看 /proc/vmstat 中的关键计数器增量。执行 watch -n 1 "grep -e compact_stall -e thp_fault_fallback -e pgmigrate_success /proc/vmstat"。如果在你的业务高峰期,这几个指标在疯狂跳动,说明系统正在花费大量 CPU 周期进行内存整理,你的 P99 延迟绝对已经出问题了。

    Q2:vm.min_free_kbytes 设置得越大越好吗? 绝对不是。如果设置得太大(例如超过总内存的 5%),会导致系统提前触碰 low 甚至 high 水位线,触发后台 kswapd 极其频繁地唤醒,一直在做无用的 Page Cache 回收,反而降低了内存利用率并推高 CPU sys 使用率。一般 256G 内存给 2G~4G 足矣。

    Q3:除了 numactl --interleave=all,修改 BIOS 里的 Node Interleaving 有什么区别? BIOS 级别的 Node Interleaving 是从硬件层把 NUMA 给屏蔽掉(UMA 模式),OS 看到的只有一个大的 NUMA 节点。这种方式虽然简单粗暴,但所有进程都被迫交错访问。而使用 numactl 可以在 OS 保留 NUMA 感知的前提下,仅针对特定的吃内存大户(如 JVM / DB)进行交错,其他对 CPU 缓存敏感的轻量级计算进程(如 Nginx/Envoy)依然可以享受 NUMA 的本地访问加速,后者更加精细和灵活。