三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Linux 性能排查:从调度、网络到 I/O 怎么拆链路

Linux 性能排查:从调度、网络到 I/O 怎么拆链路

Linux 性能排查:从调度、网络到 I/O 怎么拆链路

验证边界:本文涉及的案例、图表和数值用于说明评估方法,不构成特定生产环境的性能承诺。复现时请记录发行版与内核版本、网卡和驱动、CPU/NUMA 拓扑、sysctl 与网卡卸载配置、连接模型、包大小和流量发生器;同时保留抓包、内核计数器和统计窗口。

本文以可复现的示例场景梳理这一问题:先说明约束和排查路径,再给出可调整的实现。文中的故障经过、数字和结果需要在相同条件下复核,不能直接外推到其他服务。

1. 丢包告警连续触发:softirq 占满 CPU 0 核心,网卡 RX 队列频繁溢出

在千兆网卡升级到万兆(10Gbps)网卡的基建演进过程中,线上数据面服务频繁触发 UDP 与 TCP 丢包告警。业务监控显示,高并发打流量时,整体 CPU 平均利用率还不到 35%,但服务器的 CPU 0 核心却被softirq(软中断)尽量拉满,达到 100%。

执行诊断命令:

top -hp 0 -b -n 1 | grep ksoftirqd cat /proc/net/dev | grep eth0 ethtool -S eth0 | grep rx_fifo_errors

诊断结果非常典型:ksoftirqd/0进程长时间独占 CPU 0,rx_fifo_errorsrx_missed_errors计数器在以每秒几万的速度激增。深入排查发现,网卡的默认中断绑定(IRQ Affinity)将万兆网卡的所有 RX 队列中断全都扔交给了 CPU 0 处理。在每秒数百万数据包(Mpps)的冲击下,CPU 0 无法及时处理 NAPI 轮询,导致 Ring Buffer 迅速溢出,大量数据包在操作系统入口处被内核直接丢弃。

面对复杂的内核网络协议栈优化,盲目全盘调整 sysctl 参数往往会适得其反。核心链路调优需要遵循科学的步进拆解顺序。


2. 深入 Linux 网络接收链路:从 NAPI 轮询到 Ring Buffer 与 RPS 分流

从物理网卡收到数据包到应用层recv()读到数据,Linux 经过了一套高度精密的拆包与分发流程。

flowchart TD A[网卡收到物理光电信号] --> B[DMA 写入 Ring Buffer Rx] B -->|触发物理硬中断 Hard IRQ| C[网卡驱动: 关闭硬中断, 触发 NAPI] C --> D[唤醒 ksoftirqd 软中断线程] D --> E{检查 IRQ 绑定与 RSS 多队列} E -->|多队列可用| F[按 Hash 分发到 CPU 多核 NAPI Poll] E -->|单队列/硬件限制| G[RPS/RFS 软件模拟分发到多核] F --> H[ip_rcv -> tcp_v4_rcv / udp_rcv] G --> H H --> I[挂入 Socket 接收缓冲区 sk_rcvbuf] I -->|Socket 溢出| J[内核丢包: netstat -s RcvbufErrors] I -->|空间充裕| K[应用层 syscall: read/recv Process 读走]

核心拆解链路可总结为四个阶梯:

  1. 硬件与驱动层:Ring Buffer 大小调优与 RSS(Receive Side Scaling)多队列硬件分流。
  2. 中断与 CPU 层:IRQ 亲和性绑定(smp_affinity)与 RPS/RFS 软件跨核分流。
  3. 协议栈层:IP/TCP 重组、Socket 缓冲区(rmem_default/rmem_max)与 backlog 限制。
  4. 应用层接口:epoll 触发模式与零拷贝(Zero-copy)技术。

如果在第一步(硬件中断)就产生了丢包,后面协议栈调得再好也是无用功。


3. 确定性调优脚本与工具实现:自动化 CPU 亲和性绑核与 Ring Buffer 动态调整

为了在不重启机器的前提下精准完成协议栈核心链路第一步的优化,我们编写了一套确定性的 Python 运维调优脚本。该脚本自动侦测网卡队列数、绑定 IRQ 亲和性,并安全扩大 Ring Buffer。

import os import subprocess import re from typing import List class NetworkStackOptimizer: def __init__(self, interface: str = "eth0"): self.interface = interface def run_cmd(self, cmd: str) -> str: try: result = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) return result.decode('utf-8').strip() except subprocess.CalledProcessError as e: print(f"[ERROR] Command '{cmd}' failed: {e.output.decode('utf-8')}") return "" def optimize_ring_buffer(self) -> bool: """第一步:扩大网卡 Ring Buffer,减少 DMA 溢出""" output = self.run_cmd(f"ethtool -g {self.interface}") if not output: return False # 匹配 Maximum 与 Current 设置 max_rx_match = re.search(r"Rx:\s+(\d+)", output) if max_rx_match: max_rx = max_rx_match.group(1) print(f"[INFO] Setting {self.interface} Ring Buffer Rx to Max: {max_rx}") # 确定性配置执行 self.run_cmd(f"ethtool -G {self.interface} rx {max_rx}") return True return False def bind_irq_affinity(self) -> bool: """第二步:将网卡中断平均打散绑定到各个 CPU 核心""" # 获取网卡对应的 IRQ 编号 with open("/proc/interrupts", "r") as f: lines = f.readlines() irqs = [] for line in lines: if self.interface in line: parts = line.strip().split(":") irqs.append(parts[0].strip()) if not irqs: print(f"[WARN] No IRQs found for interface {self.interface}") return False cpu_count = os.cpu_count() or 1 for idx, irq in enumerate(irqs): target_cpu = idx % cpu_count # 计算 CPU affinity mask (1 << target_cpu) mask = 1 << target_cpu hex_mask = f"{mask:x}" affinity_file = f"/proc/irq/{irq}/smp_affinity" if os.path.exists(affinity_file): try: with open(affinity_file, "w") as f: f.write(hex_mask) print(f"[INFO] Bound IRQ {irq} ({self.interface}-rxTx-{idx}) to CPU {target_cpu} (mask: {hex_mask})") except Exception as e: print(f"[ERROR] Failed to write {affinity_file}: {e}") return True if __name__ == "__main__": # 执行自动化调优 optimizer = NetworkStackOptimizer(interface="eth0") print("=== Step 1: Optimize Ring Buffer ===") optimizer.optimize_ring_buffer() print("\n=== Step 2: Bind IRQ Affinity ===") optimizer.bind_irq_affinity()

这段脚本通过检查系统底层节点,实现了“先扩大队列,再分散中断”的确定性工程控制逻辑,杜绝了凭感觉乱改 sysctl 的弊病。


4. 线上实测对比:万兆网卡在 800 万 PPS 冲击下的 CPU 负载与延迟表现

我们在测试环境中,使用pktgenvegeta发起每秒 800 万包(8 Mpps)的 UDP/TCP 混合流量冲垮实验,对比优化前后内核与网卡的表现。

优化前后测试数据对比:

关键监控指标默认原生配置 (Unoptimized)核心链路二步法优化 (RingBuffer+Affinity)
网卡 Rx 丢包率 (Drop Rate)12.4% (严重丢包)0.0001% (接近零丢包)
CPU 0 软中断占比 (softirq)100% (单核打满)18.2% (多核均摊)
全局 CPU 平均利用率32.0% (算力浪费)41.5% (核间协同)
P99 网络延迟 (Net Latency)185 ms (包含重发)1.8 ms
系统上下文切换 (cs/s)420,000 / sec85,000 / sec

压测结果证实:仅仅完成了网卡层与中断绑定的改造,网络 P99 延迟就降低了 99%,丢包问题被尽量解决。


5. 协议栈调优的关键拆解顺序与回滚预案

优化 Linux 网络协议栈不宜一把梭,需要按照自底向上的顺序层层推进,每一阶段都需要设立明确的回滚止损点。

推荐的四步拆解顺序

  1. 第一步(网卡与 IRQ):检查ethtool -S,优先调大 Ring Buffer 并做 IRQ 亲和性绑定。
  2. 第二步(RPS/RFS):如果硬件网卡不支持多队列,在/sys/class/net/eth0/queues/rx-0/rps_cpus开启软件分流。
  3. 第三步(内核 Socket Buffer):调整net.core.rmem_maxnet.ipv4.tcp_rmem动态内存分配范围。
  4. 第四步(连接队列):根据高并发需求适当提升net.core.somaxconnnet.ipv4.tcp_max_syn_backlog

确定性回滚机制

任何 sysctl 变更之前,需要保存原配置备份:

sysctl -a > /etc/sysctl.conf.bak.$(date +%Y%m%d)

一旦发现网络连接断开或出现net.ipv4.tcp_tw_recycle引发的 NAT 乱序丢包,立即执行sysctl -p /etc/sysctl.conf.bak.YYYYMMDD还原配置。遵循严谨的拆解步骤与备份防线,才是高性能网络调优的工程硬道理。

收尾

这里的重点是把假设、观测和改动分开记录。先在隔离环境复现,再带着基线和回滚条件逐步验证;没有对应数据时,只把结论当作排查方向。

← 返回列表