Linux 内核网络栈是整台机器数据平面的心脏。无论是容器之间的 veth 转发、云主机的虚拟网卡收包,还是裸金属上的十万级 QPS 服务,最终都要经过同一套收包、协议解析、连接跟踪与发送调度机制。理解这套机制的内部结构,是把网络性能从「能用」推到「极致」的前提。
本文以 struct sk_buff 的生命周期为主线,依次讲解网卡硬中断到 NAPI poll 的接收路径、dev_queue_xmit 到 qdisc 的发送路径,深入分析 GRO/RPS/RFS/XPS 这组多核扩展技术、netfilter 五钩子与 conntrack 状态表、struct tcp_sock 与拥塞控制框架,并覆盖 XDP/AF_XDP 快速路径、netns 与 veth/bridge 容器网络,最后给出一份基于 perf 与 tracepoint 的丢包定位清单。
一、sk_buff 结构与生命周期
struct sk_buff(简称 skb)是内核网络栈中承载一个数据包的通用容器。它不复制报文数据本身,而是通过一组指针描述数据在内存缓冲区中的位置,从而支持在协议栈各层之间零拷贝地推入与弹出头部。
1.1 关键字段
| 字段 | 含义 |
|---|---|
head / end | 缓冲区首尾,二者之间是已分配的存储空间 |
data / tail | 当前有效数据的首尾,头部入栈时 data 前移 |
len / data_len | 线性区长度 / 分片(paged data)长度 |
dev / sk | 关联的网络设备 / 关联的 socket |
cb[48] | 每层协议私有的控制块,TCP/IP 各自复用 |
_skb_refdst | 路由缓存引用(dst entry) |
protocol | 三层协议类型(ETH_P_IP 等) |
struct sk_buff {
struct sk_buff *next, *prev;
union { struct net_device *dev; unsigned long dev_scratch; };
char cb[48] __aligned(8); /* 各层私有控制块 */
unsigned int len, data_len; /* 线性区 + 分片长度 */
__u16 mac_len, hdr_len, protocol;
sk_buff_data_t tail, end, head; /* 用偏移量定位,节省指针 */
unsigned char *data;
...
};
注意 head/data/tail/end 中除 data 外都是偏移量(sk_buff_data_t)而非指针,这是为了缩小结构体、提升缓存命中率。
1.2 分配与释放
struct sk_buff *alloc_skb(unsigned int size, gfp_t priority); /* 普通分配 */
struct sk_buff *__dev_alloc_skb(unsigned int len, gfp_t mask); /* 软中断中分配 */
void kfree_skb(struct sk_buff *skb); /* 引用计数归零时真正回收 */
void consume_skb(struct sk_buff *skb); /* 消费一个引用 */
skb->users 是引用计数:skb_get() 加一,kfree_skb() 减一。计数降到 0 时,内核先调用 skb_release_all() 释放 frags、dst 引用、secpath 等附属资源,再把缓冲区归还给 skbuff_fclone_cache 或 skbuff_head_cache。
1.3 线性区与分片
skb 的数据分为两部分:线性区(data 到 tail,协议头解析必然访问的连续区域)与分片区(由 skb_shared_info 的 frags[] 描述,每个 frag 指向一个 page 的区间)。
struct skb_shared_info {
__u8 nr_frags; /* frag 数量 */
__u8 tx_flags;
unsigned short gso_size; /* GSO 分段大小 */
unsigned short gso_segs;
struct sk_buff *frag_list; /* 分片链表 */
skb_frag_t frags[MAX_SKB_FRAGS];
};
发送大包时,应用数据直接放进 frags(零拷贝路径),协议头留在线性区,内核无需为每个报文复制整个 payload。skb_headlen(skb) 返回线性区长度,skb_pagelen(skb) 返回分片长度。
1.4 复制与克隆
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t mask); /* 共享数据,仅复制头部 */
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t mask); /* 数据也复制 */
struct sk_buff *skb_share_check(struct sk_buff *skb, gfp_t mask);
skb_clone() 是零拷贝的,克隆体与原 skb 共享 skb_shared_info,其中 dataref 计数递增。因此克隆体不可修改数据,需要修改时必须先 pskb_expand_head() 或 skb_copy()。
二、收发路径与 NAPI 软中断
2.1 接收路径总览
一个报文从网卡到 socket 的完整路径:
网卡硬件
└─ DMA 写入 RX ring buffer
└─ 触发硬中断(MSI-X)
└─ 中断处理程序调用 napi_schedule()
└─ 触发 NET_RX_SOFTIRQ 软中断
└─ napi_poll() 收包到 netdev_budget 上限
└─ napi_gro_receive() 做 GRO 合并
└─ netif_receive_skb()
└─ 协议栈:ip_rcv -> tcp_v4_rcv
└─ sk_data_ready() 唤醒 socket 等待队列
传统路径 netif_rx() 会把 skb 放入 per-CPU 的 backlog 队列,等待 NET_RX_SOFTIRQ 处理;现代 NAPI 驱动则直接在软中断上下文调用 napi_gro_receive()。
2.2 NAPI 与软中断
NAPI(New API)的核心是「中断 + 轮询」的混合模型:第一个包触发中断,随后关闭该队列的中断改为轮询收包,直到队列空或达到预算,再重新开启中断。
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
int (*poll)(struct napi_struct *, int), int weight);
void napi_schedule(struct napi_struct *n); /* 中断上下文中调度 */
int (*poll)(struct napi_struct *napi, int budget); /* 轮询回调,返回处理报文数 */
NET_RX_SOFTIRQ 的处理函数是 net_rx_action(),它遍历 softnet_data.poll_list 上的 NAPI 实例逐个调用 poll()。当软中断运行超过 netdev_budget_usecs(默认 2000 微秒)或处理报文数超过 netdev_budget(默认 300)时,剩余工作交给 ksoftirqd 内核线程,避免长时间占用软中断上下文导致用户态饥饿。
2.3 发送路径
int dev_queue_xmit(struct sk_buff *skb); /* 交给设备(经 qdisc) */
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq); /* 直接调用驱动发送 */
dev_queue_xmit() 先做 qdisc 分类(__dev_xmit_skb()),再交给 qdisc 排队,qdisc 出队时调用 ndo_start_xmit()。若设备支持 GSO,dev_hard_start_xmit() 会把大包切分成多个 MTU 大小的段。发送完成后驱动通过 netif_tx_wake_queue() 唤醒被阻塞的发送队列。
2.4 netdev_budget 与 softnet_data
每个 CPU 都有一个 softnet_data 结构:
struct softnet_data {
struct list_head poll_list; /* 待轮询的 NAPI 实例 */
struct sk_buff_head input_pkt_queue; /* backlog 队列 */
struct napi_struct backlog;
unsigned int dropped; /* 因 backlog 满丢弃的计数 */
unsigned int time_squeeze; /* 因预算耗尽被迫退出的次数 */
...
};
sysctl net.core.netdev_budget # 单次软中断处理的最大报文数(默认 300)
sysctl net.core.netdev_budget_usecs # 单次软中断的最大耗时(默认 2000)
sysctl net.core.netdev_max_backlog # per-CPU backlog 队列上限(默认 1000)
netdev_budget 过小会导致 time_squeeze 升高、包处理延迟增大;netdev_max_backlog 过小则在突发流量下直接丢包(dropped 计数增长)。
三、GRO、RPS、RFS 与 XPS
单核处理能力有限,Linux 用一组技术把收包工作分摊到多核:GRO 减少报文数量,RPS/RFS 把处理分散到多个 CPU,XPS 则优化发送侧的队列选择。
3.1 GRO 合并
GRO(Generic Receive Offload)在软中断中把同一 TCP 流的连续小包合并成一个大包再上送协议栈,显著减少 ip_rcv()/tcp_v4_rcv() 的调用次数。
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb);
合并条件是:同一五元组、序列号连续、时间间隔小于阈值。合并后的大包由 napi_gro_complete() 送入协议栈。
nstat -az | grep -E 'GRO|IpInDelivers'
ethtool -k eth0 | grep -E 'gro|gso'
ethtool -K eth0 gro off # 需要抓包精确分析时关闭
3.2 RPS
RPS(Receive Packet Steering)在软件层模拟多队列:根据报文哈希计算目标 CPU,把 skb 放入该 CPU 的 input_pkt_queue,由目标 CPU 的软中断处理。
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus # 让 8 核都参与收包
cat /sys/class/net/eth0/queues/rx-0/rps_cpus
RPS 适用于单队列网卡。若网卡本身支持多队列(RSS),优先使用硬件分流,RPS 收益有限。
3.3 RFS
RFS(Receive Flow Steering)在 RPS 基础上进一步优化:它记录每条流的应用层处理 CPU,把报文尽量投递到运行该应用线程的 CPU 上,提升 CPU 缓存命中率。
sysctl net.core.rps_sock_flow_entries=32768 # 全局流表大小
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
RFS 需要应用侧配合:内核在 sendmsg/recvmsg 时通过 rps_record_sock_flow() 记录 socket 与 CPU 的关联。
3.4 XPS
XPS(Transmit Packet Steering)作用于发送侧:为每个 CPU 选择固定的发送队列,减少跨 CPU 的队列锁竞争。
echo f > /sys/class/net/eth0/queues/tx-0/xps_cpus # CPU 0-3 使用 tx 队列 0
| 技术 | 方向 | 核心作用 |
|---|---|---|
| GRO | 接收 | 合并小包,减少协议栈调用 |
| RPS | 接收 | 软件层多核分摊收包 |
| RFS | 接收 | 按应用亲和性投递到目标 CPU |
| XPS | 发送 | 固定 CPU 到发送队列映射 |
四、netfilter 与 conntrack
4.1 五个 hook 点
netfilter 在 IP 层的五个位置注册钩子,iptables/nftables 规则即挂载于此:
| Hook | 触发位置 | 典型用途 |
|---|---|---|
NF_INET_PRE_ROUTING | 报文进入本机、路由决策之前 | DNAT、raw 表 |
NF_INET_LOCAL_IN | 路由判定为发给本机之后 | 防火墙 INPUT |
NF_INET_FORWARD | 路由判定为转发 | 转发过滤、K8s 转发 |
NF_INET_LOCAL_OUT | 本机产生的报文 | OUTPUT 过滤 |
NF_INET_POST_ROUTING | 报文即将离开本机 | SNAT、MASQUERADE |
static unsigned int my_hook(void *priv, struct sk_buff *skb,
const struct nf_hook_state *state)
{
/* 返回 NF_ACCEPT 继续,NF_DROP 丢弃,NF_STOLEN 接管 */
return NF_ACCEPT;
}
static const struct nf_hook_ops my_ops = {
.hook = my_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
};
nf_register_net_hook(net, &my_ops);
4.2 iptables 与 nftables
iptables 的规则按表(table)组织,每张表挂在特定 hook 上:
iptables -t filter -L -n -v --line-numbers # 查看规则与计数器
nft list ruleset # nftables 等价操作
nft add rule ip filter input tcp dport 22 accept
iptables-save > rules.v4 # 迁移到 nftables
iptables-restore-translate -f rules.v4 > rules.nft
nftables 用统一字节码虚拟机取代了 iptables 的多表多链遍历,规则匹配复杂度更低,在大规模规则集下性能优势明显。
4.3 conntrack 状态表
连接跟踪(conntrack)为每条流维护一个 struct nf_conn,记录双向的元组、状态与超时。这是 NAT 与有状态防火墙的基础。
conntrack -L # 查看当前连接跟踪表
conntrack -L -p tcp --state ESTABLISHED
conntrack -C # 当前条目数
conntrack -S # 统计信息(插入失败、无效包等)
sysctl net.netfilter.nf_conntrack_max # 表容量上限
sysctl net.netfilter.nf_conntrack_tcp_timeout_established
TCP 连接状态机在 conntrack 中维护:NEW -> ESTABLISHED -> FIN_WAIT -> TIME_WAIT -> CLOSE。若 nf_conntrack_max 被占满,新连接会被直接丢弃并打印 nf_conntrack: table full, dropping packet——这是高并发服务器最常见的隐蔽丢包源之一。
4.4 调优建议
sysctl -w net.netfilter.nf_conntrack_max=1048576 # 提升表容量(每条约 300 字节)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600 # 加快条目回收
# options nf_conntrack hashsize=262144 # 提升哈希桶,重启生效
对于纯转发或负载均衡场景,可考虑用 eBPF 的 bpf_ct_* 接口或 XDP 层的无状态处理绕开 conntrack,把每包开销降到最低。
五、socket 层与 TCP 拥塞控制
5.1 struct sock 与 tcp_sock
struct sock 是所有协议族 socket 的公共基类,struct tcp_sock 在其基础上扩展 TCP 专属状态:
struct tcp_sock {
struct inet_connection_sock inet_conn;
u32 rcv_nxt, snd_nxt; /* 期望接收 / 下一个发送序号 */
u32 snd_una, snd_wnd; /* 未确认序号 / 发送窗口 */
u32 snd_cwnd, snd_ssthresh; /* 拥塞窗口 / 慢启动阈值 */
u32 srtt_us, mdev_us; /* RTT 估计值 */
u32 rcv_wnd, rcv_ssthresh;
struct tcp_congestion_ops *ca_ops; /* 拥塞控制算法 */
...
};
收发队列通过 sk_receive_queue(已到达、待应用读取)与 sk_write_queue(已发送、待 ACK)管理,配合 sk_rmem_alloc/sk_wmem_alloc 做内存记账。
5.2 tcp_sendmsg 与 tcp_recvmsg
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size);
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,
int nonblock, int flags, int *addr_len);
tcp_sendmsg() 把用户数据按 MSS 切分、构造成 skb 挂入 sk_write_queue,然后调用 tcp_push() 触发发送。tcp_recvmsg() 从 sk_receive_queue 取数据;若队列为空且为阻塞模式,进程会睡在 sk_sleep() 等待队列上,由 sk_data_ready() 唤醒。
ss -tnm # 查看 socket 收发队列积压
ss -s # 全局汇总
ss -tni # 显示 cwnd、rtt、retrans 等 TCP 内部信息
5.3 拥塞控制框架
TCP 拥塞控制通过 struct tcp_congestion_ops 抽象,算法以可插拔模块形式注册:
struct tcp_congestion_ops {
struct list_head list;
u32 (*ssthresh)(struct sock *sk);
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
void (*set_state)(struct sock *sk, u8 new_state);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
u32 (*undo_cwnd)(struct sock *sk);
char name[TCP_CA_NAME_MAX];
};
慢启动阶段 snd_cwnd 指数增长,达到 snd_ssthresh 后进入拥塞避免改为线性增长;检测到丢包时执行 ssthresh() 缩减窗口。
5.4 cubic、bbr 与 sysctl
sysctl net.ipv4.tcp_available_congestion_control # cubic reno bbr
sysctl -w net.ipv4.tcp_congestion_control=bbr # 切换全局默认算法
tc qdisc replace dev eth0 root fq # BBR 需配合 fq 队列
# setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", 4); # 单连接指定
| 算法 | 核心思想 | 适用场景 |
|---|---|---|
cubic | 三次函数增长窗口,丢包驱动 | 通用默认,长肥管道 |
bbr | 基于带宽与 RTT 建模,不依赖丢包 | 高丢包链路、跨洲传输 |
reno | 经典 AIMD | 兼容性测试 |
5.5 TCP_NODELAY 与 TCP_CORK
Nagle 算法会把小包攒起来合并发送,降低网络利用率但对延迟敏感的应用有害:
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); /* 禁用 Nagle */
int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork)); /* 攒包 */
/* ... 多次 write ... */
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork)); /* flush */
延迟敏感(如 RPC、游戏)用 TCP_NODELAY;批量拼包(如 HTTP 响应头 + 文件)用 TCP_CORK。二者互斥,同时设置时 TCP_NODELAY 优先。
六、XDP 与 AF_XDP 快速路径
6.1 XDP 动作
XDP(eXpress Data Path)让 eBPF 程序在驱动收到报文的最早时刻执行,早于 sk_buff 分配,是内核中性能最高的可编程数据路径:
| 返回值 | 含义 |
|---|---|
XDP_PASS | 交给正常协议栈处理 |
XDP_DROP | 立即丢弃(可用于 DDoS 清洗) |
XDP_TX | 从同一网卡原路发回 |
XDP_REDIRECT | 重定向到其他网卡或 AF_XDP socket |
XDP_ABORTED | 程序出错,丢弃 |
SEC("xdp")
int xdp_drop_udp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
if (ip->protocol == IPPROTO_UDP) return XDP_DROP;
return XDP_PASS;
}
6.2 三种运行模式
ip link set dev eth0 xdp obj prog.o sec xdp # native:驱动直接调用,性能最高
ip link set dev eth0 xdpgeneric obj prog.o sec xdp # generic:skb 之后执行,全兼容
ip link set dev eth0 xdpoffload obj prog.o sec xdp # offload:卸载到智能网卡
ip link show eth0 ; ip link set dev eth0 xdp off # 查看与卸载
| 模式 | 执行时机 | 性能 | 兼容性 |
|---|---|---|---|
| native | 驱动 RX 路径,skb 之前 | 最高 | 需驱动支持 |
| generic | netif_receive_skb 之后 | 中等 | 全兼容 |
| offload | 网卡硬件 | 最高 | 需专用网卡 |
6.3 AF_XDP zero-copy
AF_XDP 是一种 socket 类型,允许用户态直接读写网卡的 UMEM 区域,绕过内核协议栈实现零拷贝收包:
int fd = socket(AF_XDP, SOCK_RAW, 0);
struct sockaddr_xdp sxdp = {
.sxdp_family = AF_XDP,
.sxdp_ifindex = ifindex,
.sxdp_queue_id = 0,
.sxdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST,
};
bind(fd, (struct sockaddr *)&sxdp, sizeof(sxdp));
收发通过四个环形队列(RX/TX/FILL/COMPLETION)在用户态与内核之间传递描述符,数据本身不复制。
6.4 与 DPDK 对比
| 维度 | AF_XDP | DPDK |
|---|---|---|
| 网卡占用 | 与内核共享 | 完全接管(需绑定 vfio) |
| 零拷贝 | 支持(需驱动配合) | 支持 |
| 内核协议栈 | 可共存,按需旁路 | 完全旁路 |
| 部署复杂度 | 低,随内核版本交付 | 高,需大页、独占核 |
| 生态 | eBPF 生态 | 成熟 DPDK 生态 |
AF_XDP 适合「大部分流量走内核、少量热点流量旁路」的场景;DPDK 适合整机专职转发的场景。
七、netns 与 veth/bridge 容器网络
7.1 ip netns 与 veth pair
ip netns add ns1 ; ip netns add ns2 # 创建两个网络命名空间
ip netns exec ns1 ip link show # 在命名空间内查看设备
ip netns exec ns1 ip link set lo up
ip link add veth1 type veth peer name veth1-peer # 创建 veth 对
ip link set veth1-peer netns ns1 # 一端放入 ns1
ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1-peer
ip netns exec ns1 ip link set veth1-peer up ; ip link set veth1 up
每个 netns 拥有独立的路由表、iptables、conntrack 与端口空间,这正是容器网络隔离的基础。veth 是一对虚拟网卡,从一端发出的报文会从另一端收到,用于连接两个 netns。
7.2 bridge 与 tc qdisc
Linux bridge 是软件交换机,把多个 veth 端口接入同一广播域;tc 决定报文在发送队列中的排队与调度策略:
ip link add br0 type bridge ; ip link set br0 up # 创建网桥并接入端口
ip link set veth1 master br0
brctl show # 旧工具等价查看
tc qdisc show dev eth0 # 查看当前 qdisc
tc qdisc replace dev eth0 root fq_codel # 缓解 bufferbloat
tc qdisc add dev eth0 root tbf rate 100mbit burst 32kbit latency 400ms
tc qdisc add dev eth0 root netem delay 100ms loss 1%
常用 qdisc:pfifo_fast(默认)、fq(BBR 伴侣)、fq_codel(抗 bufferbloat)、tbf(令牌桶限速)、netem(网络模拟)。Docker 默认创建的 docker0 就是一个 bridge,容器 veth 的一端接入其中,与宿主机的 NAT 规则配合实现外网访问。
7.3 容器网络数据路径
容器进程 write()
└─ 容器内 eth0(veth 端)-> veth pair
└─ 宿主机 docker0/br0 bridge 转发
└─ netfilter POST_ROUTING 做 SNAT
└─ 宿主机物理网卡 dev_queue_xmit -> qdisc -> 驱动
排查容器网络问题时,需要分别进入容器 netns 与宿主机视角检查:nsenter -t <pid> -n ip route、conntrack -L | grep <容器IP>、iptables -t nat -L -n -v。
八、丢包定位与排查清单
丢包可能发生在 ring buffer、backlog、qdisc、conntrack、socket 缓冲区中的任意一环。以下是一套从底层到上层的定位方法。
8.1 /proc/net/softnet_stat
每行对应一个 CPU,字段为十六进制:
cat /proc/net/softnet_stat
# 第 1 列:处理的报文总数
# 第 2 列:因 backlog 满而丢弃的报文数(dropped)
# 第 3 列:time_squeeze,预算耗尽被迫退出次数
# 第 9 列:CPU 冲突次数(collision)
# 第 14 列:收到的 RPS 报文数
第 2 列或第 3 列持续增长,说明软中断处理能力不足,应提升 netdev_budget、netdev_max_backlog 或启用 RPS。
8.2 netstat -s 与 nstat
netstat -s | grep -i -E 'drop|overflow|error|retrans'
nstat -az | grep -E 'Drop|Prune|Backlog|Retrans'
ss -s # socket 汇总
# 关键指标:TcpExtListenDrops / ListenOverflows(accept 队列满)
# TcpRetransSegs(重传次数)
# UdpInErrors / RcvbufErrors(UDP 接收缓冲区溢出)
8.3 dropwatch 与 perf trace
dropwatch -l kas # 交互式输入 start 查看 kfree_skb 调用位置
perf trace -e net:* sleep 5 # 追踪网络 tracepoint
perf trace -e skb:kfree_skb -a sleep 5
perf record -e skb:kfree_skb -a -g -- sleep 10 ; perf report # 按调用栈统计
8.4 tracepoint skb:kfree_skb 与 GRO
skb:kfree_skb 是最有价值的丢包 tracepoint,它的调用点即为丢包发生处。配合 napi_gro_* tracepoint 可观察 GRO 合并行为:
echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe
bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack] = count(); }' # 丢包原因分布
8.5 常见丢包点排查清单
| 丢包点 | 观测指标 | 排查手段 |
|---|---|---|
| 网卡 ring buffer | ethtool -S 中 rx_dropped/rx_no_buffer | ethtool -G eth0 rx 4096 增大环形缓冲 |
| backlog 队列 | /proc/net/softnet_stat 第 2 列 | 提升 netdev_max_backlog |
| 软中断预算 | /proc/net/softnet_stat 第 3 列 | 提升 netdev_budget,启用 RPS |
| qdisc 队列 | tc -s qdisc show 中 dropped | 调整限速或改用 fq_codel |
| conntrack 表满 | dmesg 中 table full | 提升 nf_conntrack_max |
| socket 接收缓冲 | ss -tnm 中 Recv-Q 与 sk_rmem_alloc | 提升 net.core.rmem_max/rmem_default |
| UDP 缓冲溢出 | nstat 中 UdpRcvbufErrors | 提升 net.core.rmem_max,加快应用读取 |
| accept 队列 | TcpExtListenOverflows | 提升 somaxconn 与 tcp_max_syn_backlog |
相关阅读
- https://plumephp.com/os-linux-network/ —— Linux 网络子系统整体架构与协议栈分层解析
- https://plumephp.com/os-interrupts/ —— 硬中断、软中断与 NAPI 收包的中断处理机制
- https://plumephp.com/os-ebpf-observability/ —— 用 eBPF 观测内核网络路径与丢包点
延伸阅读
- Linux Kernel Documentation:
Documentation/networking/(driver.rst、scaling.rst 等) - Linux Kernel Documentation:
Documentation/networking/filter.rst(BPF/XDP 过滤器) - Christian Benvenuti, 《Understanding Linux Network Internals》(O’Reilly)
- LWN.net: “The return of the AF_XDP” 与 “Network receive path” 系列文章
- Linux 源码:
net/core/dev.c、net/core/skbuff.c、net/ipv4/tcp_input.c
# ============================================================
# 完整可运行示例:内核网络栈健康检查与丢包定位(需 root)
# ============================================================
set -u
IFACE="${1:-eth0}"
echo "=== 目标网卡: $IFACE ==="
echo "=== 1. 网卡驱动丢包统计 ==="
ethtool -S "$IFACE" 2>/dev/null | grep -iE 'drop|error|no_buffer|missed' || echo " (无匹配)"
echo "=== 2. ring buffer 大小 ==="
ethtool -g "$IFACE" 2>/dev/null | sed -n '1,10p'
echo "=== 3. 每 CPU 软中断统计(processed / dropped / squeeze)==="
awk '{ printf "CPU%-3d %-12d %-12d %-12d\n", NR-1, strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3) }' \
/proc/net/softnet_stat
echo "=== 4. 关键 sysctl 参数 ==="
for k in net.core.netdev_max_backlog net.core.netdev_budget net.core.netdev_budget_usecs \
net.core.rmem_max net.core.rmem_default net.core.wmem_max net.core.somaxconn \
net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_congestion_control; do
printf " %-38s = %s\n" "$k" "$(sysctl -n "$k" 2>/dev/null || echo n/a)"
done
echo "=== 5. conntrack 表使用情况 ==="
if [ -r /proc/sys/net/netfilter/nf_conntrack_count ]; then
cnt=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
max=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
echo " 条目数: $cnt / $max ($(( cnt * 100 / max ))%)"
else
echo " (conntrack 未启用)"
fi
echo "=== 6. qdisc 队列与丢包 ==="
tc -s qdisc show dev "$IFACE" 2>/dev/null | grep -E 'qdisc|dropped|backlog' || echo " (无)"
echo "=== 7. 协议层错误统计 ==="
nstat -az 2>/dev/null | grep -E 'Drop|Prune|Overflow|Retrans|RcvbufErrors' || \
netstat -s 2>/dev/null | grep -iE 'drop|overflow'
echo "=== 8. socket 收发队列积压(Recv-Q > 0 的 TCP 连接)==="
ss -s 2>/dev/null
ss -tnm 2>/dev/null | awk 'NR>1 && $2+0 > 0 { print " " $0 }' | head -10
echo "=== 9. 仍无法定位时追踪丢包调用栈 ==="
echo " perf record -e skb:kfree_skb -a -g -- sleep 10 && perf report"
echo " bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack] = count(); }'"
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。