标签: Linux内核

  • 深入 NVMe 队列阻塞排查:blk-mq 调度器误用引发的 XFS 元数据锁雪崩与 sys CPU 饱和实战

    高并发写入场景下,NVMe 盘配合 XFS 极易触发 sys CPU 满载与 IO 夯死。核心原因是 NVMe 误用了 mq-deadline 调度器,导致 blk-mq 软件队列自旋锁争用,进而引发 XFS 分配元数据时在 xfs_log_commit_cil 处发生锁雪崩。直接结论:NVMe 设备的 IO 调度器必须设为 none,同时对于高并发盘,需在格式化时调大 XFS 的 agcount 以打散锁粒度。

    故障现场:数据库写入 p99 突增与 sys CPU 飙升

    某次排查过程中,一套承载核心业务的 PostgreSQL 集群(内核版本 5.10.134-el8,底层存储为裸金属物理机的 PCIe Gen4 NVMe SSD)在高并发 COPY 导入数据时,QPS 出现周期性断崖式下跌。

    通过 top 观察,CPU sys 态长期飙升至 70% 以上,iowait 反而在 10% 左右波动。这极不寻常——对于一块标称 100万 IOPS 的 NVMe 盘,IO 没有跑满,CPU 却在内核态被榨干。

    抓取当时的 iostat -x 1 核心指标:

    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
    nvme0n1        12.0 42351.0     192.0  680512.0     0.0     0.0    0.0    0.0    0.15   18.42   12.5   16.00    16.06   0.02  85.40
    

    注意 w_await 达到了惊人的 18.42ms,对于 NVMe 来说,这个延迟意味着底层已经严重阻塞。但 %util 只有 85%,设备并未完全饱和。

    使用 perf top -U 直接看内核态热点,现场如下:

      18.45%  [kernel]  [k] queued_spin_lock_slowpath
      12.31%  [kernel]  [k] dd_insert_requests
       8.52%  [kernel]  [k] xfs_log_commit_cil
       6.14%  [kernel]  [k] blk_mq_submit_bio
       5.33%  [kernel]  [k] _raw_spin_lock_irqsave
    

    热点非常集中:dd_insert_requestsxfs_log_commit_cil。这表明系统同时在块设备调度层和文件系统日志提交层发生了严重的锁争用。

    为什么 NVMe 设备使用 mq-deadline 会导致 IO 栈雪崩?

    问题出在 Linux blk-mq(Block Multi-Queue)架构的调度器选择上。

    在传统的单队列(Single Queue)时代,所有 IO 请求进入一个全局队列,需要 CFQ 或 Deadline 这种电梯算法(Elevator)进行合并和排序,以减少机械硬盘的磁头寻道。

    到了 NVMe 时代,硬件支持多达 64K 个提交/完成队列。Linux 为此重构了 blk-mq 架构,分为软件队列(Software Staging Queues,通常每个 CPU 核心一个)和硬件分发队列(Hardware Dispatch Queues)。

    排查发现,该服务器的 NVMe 被默认配置了 mq-deadline 调度器:

    $ cat /sys/block/nvme0n1/queue/scheduler
    [mq-deadline] kyber bfq none
    

    底层阻塞原理: 当调度器设置为 mq-deadline(甚至 bfq)时,IO 请求在进入硬件队列前,必须先挂载到电梯算法的软件队列中。dd_insert_requests 就是 mq-deadline 插入请求的内核函数。由于高并发下成千上万个线程试图向这个软件队列提交 BIO(Block I/O),这就不可避免地触发了自旋锁(queued_spin_lock_slowpath)。 NVMe 的纳秒级响应速度完全被软件队列的自旋锁开销抹平,导致 CPU 在 sys 态空转,IO 提交路径被硬生生卡住。

    剥茧抽丝:XFS 延迟分配与 AIL/CIL 阻塞

    块设备的延迟飙升,迅速引发了文件系统层的连锁反应,这也是为什么 perf 中出现了大量 xfs_log_commit_cil

    XFS 是一种强依赖 Allocation Group(AG)并发设计的日志文件系统(当前版本 V5)。当数据库执行大量写入时,XFS 会利用延迟分配(Delayed Allocation)机制,在内存中缓存数据,直到刷盘时才真正分配物理 Block 并更新元数据。

    1. CIL(Committed Item List)雪崩:元数据变更首先写入内存中的 CIL。当底层 NVMe 因为 mq-deadline 阻塞时,后台刷脏线程(xfsaild)将 AIL(Active Item List)刷入磁盘的速度骤降。

    2. AG 锁争用:CIL 空间被占满,前端业务线程在调用 xfs_alloc_vextent 申请新的空间块时,必须等待日志空间释放。大量 PostgreSQL 线程被迫在同一个 AG 的元数据锁上排队。

    3. 全局夯死:IO 栈的阻塞放大了 XFS 的锁临界区时间,最终导致原本并行的 IO 瀑布般退化为串行等待,形成死锁态势的雪崩。

    解决方案与防御性配置

    解决该问题不需要修改业务代码,纯属系统级架构调优,分为治标和治本两步。

    1. 立即剥离软件调度器(实时恢复)

    将 NVMe 设备的调度器强行切换为 none,绕过所有电梯算法,让 BIO 请求直接从软件多队列打入硬件队列。

    echo none > /sys/block/nvme0n1/queue/scheduler
    

    执行瞬间,sys CPU 从 70% 骤降至 8%,PostgreSQL QPS 恢复正常,w_await 回落至 0.05ms。

    为了防止重启失效,通过 udev 固化防御策略:

    # vim /etc/udev/rules.d/60-io-scheduler.rules
    ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
    

    2. XFS AG 并发度调优(底层重构)

    默认情况下,mkfs.xfs 根据磁盘大小自动计算 agcount(通常是 4 或 8)。对于大容量、极高吞吐的 NVMe 盘和数据库场景,默认 AG 数量太少,容易发生并发分配碰撞。 在节点下线重装阶段,调整格式化参数,人为扩大 AG 数量打散锁粒度:

    # 格式化 XFS:强制启用 32 个 AG,对齐 512M 日志大小
    mkfs.xfs -f -K -d agcount=32 -l size=512m,version=2,su=256k /dev/nvme0n1
    

    注:agcount 并非越大越好,过大会增加 mount 时间和内存开销,通常 16-32 针对高端 NVMe 是甜点区间。

    常见问题

    Q1: io_uring 在遇到这种 XFS 锁争用时,会退化成同步阻塞吗? 会。这是很多人使用 io_uring 踩坑的地方。虽然 io_uring 是异步 IO,但如果在文件系统层发生 metadata 锁争用(比如 XFS 分配 block),底层的 IORING_OP_WRITE 且带有 RWF_NOWAIT 标志位时,内核会直接返回 -EAGAIN。随后 io_uring 只能将这个 IO 任务推入后台的 io_worker 线程池进行同步阻塞处理,纯异步链路被击穿,高并发下依然会导致线程池耗尽。

    Q2: 调度器设置为 none 后,系统还有 IO 合并能力吗? 有,但发生在不同层级。none 确实禁用了电梯算法层的合并,但 blk-mq 在软件队列层(Software Staging Queue)和块设备硬件驱动层依然会利用 scatter-gather list 进行有限的相邻物理段合并。对于 NVMe 而言,本身 4K 随机 IO 的性能极高,强行进行复杂的 IO 合并排序带来的 CPU 锁开销远大于其收益。

    Q3: 如何在生产环境无损监控 XFS 的 AG 锁争用情况? 极力推荐使用 eBPF/bpftrace 而不是 SystemTap。可以通过挂载 tracepoint 实时监控 CIL 提交延迟:

    bpftrace -e 'tracepoint:xfs:xfs_log_commit_cil { @start[tid] = nsecs; } tracepoint:xfs:xfs_log_commit_cil_wait { if(@start[tid]) { @usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); } }'
    

    如果输出的直方图显示大量调用耗时超过 1000 微秒(1ms),说明文件系统日志提交已出现严重积压,需立即排查底层块设备延迟。

  • 深入 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 的本地访问加速,后者更加精细和灵活。

  • 深入 IPVLAN L3 模式丢包排查:非对称路由引发的 rp_filter 拦截与网络黑洞实战

    结论先行:在将容器底层网络从 Macvlan 规模化迁移至 IPVLAN L3 模式时,跨网段 RPC 调用偶尔会出现 3s 超时重传。根本原因是 IPVLAN L3 绕过了宿主机的二层协议栈直接在网络层路由,导致出入站流量路径不一致(非对称路由),触发了内核(以 Linux 5.15 为例)严格模式下的 rp_filter (反向路径过滤) 拦截丢包。修复方案:将宿主机物理网卡及 all 级别的 rp_filter1(严格校验)降级为 2(松散校验)或 0

    故障现场与指标异动

    排查过程中,某业务线反馈部分 Pod 在跨可用区调用时存在偶发性的连接超时,99线从 15ms 剧增至 3s(触发 TCP 默认初始 RTO)。 登录宿主机排查基础指标:Load Average 正常,物理网卡 eth0 的 RX/TX 并没有达到硬件瓶颈,ksoftirqd 软中断也没有打满。

    在宿主机和 Pod 内分别双向抓包,发现了一个经典的“网络黑洞”现象:

    1. Pod 内发出的 SYN 包正常离境。

    2. 远端的 SYN+ACK 响应包已经到达了宿主机的物理网卡 eth0

    3. 但这个 SYN+ACK 并没有被投递到 Pod 内的虚拟网卡 eth0,直接在宿主机内核网络栈中“消失”了。

    既然包到了宿主机却没进 Pod,且没有被 iptables/Netfilter 的 DROP 规则拦截(通过 iptables -t filter -nvL 确认),第一反应是内核底层的静默丢包。直接通过 nstat 抓取网络层异常统计:

    # 持续观察 IPReversePathFilter 计数器
    nstat -az | grep -i filter
    IpExtIPReversePathFilter        124505             0.0
    

    每当业务出现超时,IpExtIPReversePathFilter 指标就会暴增。这直接锁定了凶手:Linux 内核的反向路由过滤机制(Reverse Path Filter)。

    为什么 IPVLAN L3 会触发非对称路由拦截?

    要理解这个报错,必须先搞懂 IPVLAN 的工作机制以及 L3 模式的特殊性。

    不同于 veth pair 会创建成对的虚拟网卡,也不同于 Macvlan 会为每个容器生成独立的 MAC 地址。IPVLAN 的核心特征是“MAC 地址复用”:所有的 IPVLAN 子接口都与宿主机的物理网卡共享同一个 MAC 地址。

    IPVLAN 支持三种模式:L2、L3、L3S。

    • L2 模式:行为类似交换机,处理 ARP 广播,同网段二层互通。

    • L3 模式:行为类似路由器,完全丢弃所有广播/多播包(包括 ARP)。容器向外发包时,依赖三层路由表,出站流量直接借用宿主机物理网卡的 MAC 地址发出去;入站流量到达宿主机物理网卡后,内核根据目的 IP 在宿主机的路由表中查找,将其转发给对应的 IPVLAN 子接口。

    触发 rp_filter 丢包的条件,恰恰就在 L3 模式的“非对称路由”特性上。 在复杂的 K8S 生产环境中,通常会配置多块网卡或复杂的策略路由(Policy Routing)。当远端响应包从宿主机的 eth0 进入时,内核的 Netfilter 系统会执行源地址有效性校验(fib_validate_source)。

    如果 rp_filter=1(严格模式),内核会假设:“如果我现在要给这个发件人(源 IP)回包,根据我当前的路由表,最优出站网卡是哪一个?” 如果最优出站网卡不是当前收到包的网卡(例如收到包是 eth0,但默认路由指向了 eth1,或者由于 CNI 注入了策略路由导致反向查找不匹配主路由表),内核就会判定这是一个源地址欺骗(Spoofing)的非法数据包,直接调用 kfree_skb 丢弃。

    底层原理:内核源码级丢包追踪

    为了拿到最硬核的证据,我们可以用 eBPFperf 捕获内核网络栈的丢包点(Drop Point)。

    使用 perf 挂载 kfree_skb 跟踪点:

    perf record -g -a -e skb:kfree_skb --filter 'skbaddr != 0' sleep 10
    perf script
    

    捕获到的调用栈如下,清晰地指向了 fib_validate_source

    ksoftirqd/0  [000] 12345.678901: skb:kfree_skb: skbaddr=0xffff888123456700 protocol=2048 location=ip_rcv_core+0x2f0
        ffffffff817a1234 kfree_skb+0x74 ([kernel.kallsyms])
        ffffffff817c5678 ip_rcv_core+0x2f0 ([kernel.kallsyms]) # 丢包点
        ffffffff817c6789 ip_rcv+0x59 ([kernel.kallsyms])
        ffffffff817e890a fib_validate_source+0x12a ([kernel.kallsyms]) # 反向路由校验失败
        ...
    

    在 Linux 内核源码 net/ipv4/fib_frontend.c 中,fib_validate_source 的逻辑极其严苛。更坑的是,Linux 的 sysctl 配置中,rp_filter 的最终生效值是由 all 和具体网卡(如 eth0)的值取 最大值 (MAX) 决定的。

    // 内核宏定义:取 MAX(conf/all/rp_filter, conf/dev/rp_filter)
    #define IN_DEV_RPFILTER(in_dev) \
        (max(IN_DEV_CONF_GET((in_dev), RP_FILTER), \
             IN_DEV_CONF_GET((in_dev)->cnf.all, RP_FILTER)))
    

    这意味着,哪怕你把 net.ipv4.conf.eth0.rp_filter 设成了 0,只要 net.ipv4.conf.all.rp_filter 是 1,系统依然会执行严格校验。很多排查人员在这里踩了坑,改了单网卡配置却发现不生效。

    破局与防御性配置实践

    解决此类问题,最稳妥的方式是将物理网卡和全局的 rp_filter 设置为 2(松散模式:Loose Mode)。在松散模式下,只要内核路由表中有任何一条路由可以到达该源 IP(无论通过哪个网卡),校验就会放行。

    1. 动态修复(立即生效)

    # 必须同时修改 all 和对应的物理网卡
    sysctl -w net.ipv4.conf.all.rp_filter=2
    sysctl -w net.ipv4.conf.default.rp_filter=2
    sysctl -w net.ipv4.conf.eth0.rp_filter=2
    
    # 验证当前生效值
    cat /proc/sys/net/ipv4/conf/all/rp_filter
    

    2. 自动化配置(防御性编程)

    在 DevOps 体系中,不能指望人工救火。应该在主机初始化 Ansible 剧本或者 K8S CNI 的 initContainers 中强制声明此状态。 编辑 /etc/sysctl.d/99-kubernetes.conf

    # IPVLAN L3 Mode prerequisite
    net.ipv4.conf.all.rp_filter=2
    net.ipv4.conf.default.rp_filter=2
    net.ipv4.conf.eth0.rp_filter=2
    

    执行 sysctl --system 加载,确保机器重启后不会复发。

    常见问题 (FAQ)

    Q1:高并发场景下,veth pair 和 IPVLAN 该怎么选? 如果你的业务特点是超高 PPS(每秒包数),坚决选 IPVLAN(或 Macvlan)。veth pair 的通信路径需要穿越宿主机的虚拟网卡,对端的 rx_handler 会触发大量的软中断(softirq),在单核高 PPS 下极易打满 CPU 导致限流丢包。IPVLAN 直接挂载在物理网卡的 rx_handler 上,跳过了中间的虚拟网络层开销,网络吞吐和延迟性能无限逼近物理机。

    Q2:既然 Macvlan 也能提升性能,为什么还要花精力迁移到 IPVLAN? Macvlan 的致命缺陷是“MAC 地址泛滥”。它会为每个 Pod 申请一个 MAC 地址,当单台宿主机上运行上百个 Pod,集群规模达到数千台时,底层物理交换机的 CAM 表(MAC 地址表)极易被打爆,导致整个机房的网络瘫痪。IPVLAN 共享宿主机物理网卡的 MAC 地址,彻底根绝了 CAM 表溢出问题。

    Q3:IPVLAN 容器内可以和宿主机直接通信吗? 默认不行。由于流量被直接截获投递给虚拟子接口,宿主机的协议栈和 IPVLAN 子接口处于隔离状态。如果需要互通,经典解法是在宿主机上再创建一个 IPVLAN 接口并配置 IP,将宿主机的通信也接入到这个虚拟网络层中。

    Q4:IPVLAN L3 模式下,外部请求是怎么找到容器的? 因为 L3 模式没有 ARP,外部请求找不到容器的 MAC。生产实践中,必须结合 BGP 路由协议(如 Calico BGP)或者在上一级路由器/网关配置静态路由,将容器子网的下一跳(Next Hop)直接指向宿主机物理网卡的 IP。包到达宿主机后,宿主机的路由表再负责将包“导”入 IPVLAN 子接口。

  • 深入 NVMe IO 延迟雪崩排查:默认调度器误用引发的 blk-mq 锁争用与软中断打满实战

    某次核心分布式 KV 存储集群进行底层硬件换代,全面升级至 Gen4 NVMe SSD。本以为硬件红利能让 IO 性能起飞,结果压测一上,QPS 刚到 8 万,99线(p99 latency)就开始周期性从 2ms 剧烈抖动到 300ms 以上。Load Average 狂飙至机器核数的两倍,大量写入线程陷入 D 状态(Uninterruptible Sleep)。 一句话交代结论:这是一起典型的“旧时代运维习惯毒害新硬件”的事故。系统镜像中遗留的 udev 规则将 NVMe 设备的 IO 调度器默认设置成了 mq-deadline。在极高并发下,这个多余的软件调度层在内核 blk-mq(块设备多队列)中引发了极其严重的自旋锁争用(Spinlock Contention),直接打满 ksoftirqd 软中断,导致 IO 请求在 OS 提交队列里排队,根本没送进物理磁盘。 解决方式极其简单:echo none > /sys/block/nvme0n1/queue/scheduler

    案发现场与指标拆解

    排查过程中,第一直觉是新批次的 NVMe 盘存在固件缺陷或发生了 GC(垃圾回收)拥塞。习惯性敲下 iostat -xz 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    12.00  340.00 45000.00  5440.00 720000.00    32.00   145.50  180.20    1.50  182.10   0.01  15.20%
    

    注意看这几个核心指标的矛盾点:

    1. %util 只有 15.20%:说明磁盘底层的物理带宽和 IOPS 根本没跑满,处于极度饥饿状态。

    2. svctm(服务时间)仅为 0.01ms:说明盘的物理响应极快,一旦请求交到硬件手里,瞬间就能处理完。

    3. w_await 高达 182.10ms,avgqu-sz(平均队列长度)达到 145.5:请求虽然处理得快,但排队时间长得离谱。

    这说明一个铁打的事实:IO 根本没有卡在硬件设备上,而是卡在了 Linux 内核的 IO 栈里。

    为了抓现行,直接祭出 perf top 观察内核态的 CPU 热点,结果满屏红彤彤的自旋锁:

      18.45%  [kernel]       [k] native_queued_spin_lock_slowpath
      12.30%  [kernel]       [k] blk_mq_sched_insert_request
       9.15%  [kernel]       [k] sbitmap_get
       7.20%  [kernel]       [k] dd_insert_requests
    

    再看 CPU 状态,top 显示多核的 si(软中断)飙升,对应的进程全是 ksoftirqd/x

    愚蠢的配置与底层原理解析

    走到这一步,根因已经水落石出:dd_insert_requests 这个函数的出现,意味着系统正在使用 mq-deadline IO 调度器。

    查看设备的调度器配置,果不其然:

    $ cat /sys/block/nvme0n1/queue/scheduler
    [mq-deadline] kyber bfq none
    

    为什么在 NVMe 上用 mq-deadline 是不可原谅的低级错误?

    在 SATA/SAS 机械硬盘时代,磁盘只有一个硬件队列(Queue Depth 通常只有 32 或 256)。为了防止磁头剧烈寻道,Linux 内核设计了 IO 调度器(如 Deadline、CFQ)在软件层面对 IO 请求进行合并(Merge)和重排(Sort)。 但在 NVMe 时代,协议原生支持多达 64K 个硬件队列(Submission/Completion Queues),每个队列深度可达 64K。Linux 内核为此重构了 blk-mq(Block Multi-Queue)架构,将 CPU Core 与 NVMe 硬件队列直接建立映射关系(Software Queue -> Hardware Dispatch Queue)。

    如果在 NVMe 上强行开启 mq-deadlinebfq,相当于在原本宽阔的双向 64 车道高速公路上,硬生生设了一个收费站。 所有并发请求走到 blk_mq_sched_insert_request 时,都需要去抢调度器内部的自旋锁,把请求塞进软件队列进行徒劳的合并尝试。在高并发的 KV 存储场景中,小块随机写极多,合并命中率极低,这种操作不仅毫无意义,反而引发了致命的锁争用(native_queued_spin_lock_slowpath)。同时,底层块设备完成 IO 后抛出的中断,在唤醒软中断处理(NET_RX / BLOCK)时又被上层阻塞,最终导致 ksoftirqd 把 CPU 吃干抹净。

    极客实战:彻底打通 IO 栈

    1. 拔掉收费站(关闭调度器)

    直接将调度器设置为 none,让 blk-mq 完全旁路软件调度层,直通硬件队列。

    echo none > /sys/block/nvme0n1/queue/scheduler
    

    修改后,p99 延迟瞬间从 300ms 回落到稳定的 1.5ms,Load Average 下降 80%。

    2. 防御性配置(根治固化)

    永远不要相信手动命令,写进 udev 规则才是 SRE 的基本素养:

    # /etc/udev/rules.d/60-io-scheduler.rules
    ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
    ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
    

    (注:对 SATA SSD 依然保留 mq-deadline 是一种保守策略,但对 NVMe 必须一律为 none)。

    3. 压榨最后一滴性能(中断亲和性)

    即使调度器改成了 none,如果你发现某些 CPU 核的 hi/si 依然不均衡,那是因为 NVMe 的 MSI-X 中断没有打散。现代 NVMe 驱动通常会自动分配 IRQ,但如果运行了老旧的 irqbalance 守护进程,可能会发生“劣化漂移”。 建议针对极度依赖 IO 的节点,关掉 irqbalance,并使用内核源码自带的 set_irq_affinity 脚本将 NVMe 的完成队列中断静态绑定到 NUMA 节点的对应 CPU 核心上。

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

    1. 检查 IO 调度器状态: 快速排查集群中所有块设备的调度器配置: awk -F'[][]' '{print $2}' /sys/block/*/queue/scheduler 如果 NVMe 设备输出非 none,立刻整改。

    2. 区分 OS 队列延迟与硬件延迟: 不要只看 iostatawait。使用 eBPF 工具(如 bcc-toolsbiolatency)深入观测: biolatency -Q 若输出中 OS Queue Time (Q-time) 远大于 Device Time (D-time),说明阻塞点在 Linux Block Layer。

    3. 检查 NVMe 多队列深度配置: 确认内核是否正确识别并启用了 NVMe 硬件队列: ls -d /sys/block/nvme0n1/mq/* | wc -l 该数值应接近或等于机器的 CPU 核心数。

    4. 观测软中断打散情况watch -n 1 'cat /proc/interrupts | grep nvme' 观察各 CPU 列的数值增长率。如果只有极少数 CPU 在狂飙,说明 IRQ 绑定策略失效,正在引发单核软中断瓶颈。