以下文章来源于泰晓科技 ,作者晓泰
聚焦 Linux,追本溯源,见微知著!
作者简介:刘立超,靠 Linux 内核,驱动吃饭的工程师,喜欢探究事物本质,喜欢分享。
版权声明:本文最先发表于 “泰晓科技” 微信公众号,欢迎转载,转载时请在文章的开头保留本声明。
基于 ping 流程窥探 Linux 网络子系统,同时介绍各个模块的优化方法。
这其中涉及基本的二三层转发原理,比如:直接路由、间接路由、ARP 等概念。
这部分不是本文重点,最基本的网络通信原理可以参考这篇文章:
系统调用层
ip_append_data()
函数构建 skb(这时才将报文从用户态拷贝到内核态),将报文放入 socket bufferip_push_pending_frames()
,进入 IP 层处理IP 层
dev_queue_xmit()
进入网络设备层网络设备层
__dev_xmit_skb()
尝试直接发送报文,如果网卡繁忙就将报文放入目标发送队列的 qdisc 队列,并触发 NET_TX_SOFTIRQ 软中断,在后续软中断处理中发送 qdisc 队列中的报文socket buffer
应用程序报文首先放入 socket buffer,socket buffer 空间受 wmem_default 限制,而 wmem_default 最大值受 wmem_max 限制。
调整方法:
sysctl -w net.core.wmem_max=xxxxxx -- 限制最大值
sysctl -w net.core.wmem_default=yyyyyy
qdisc 队列长度
经过网络协议栈到达网络设备层的时候,报文从 socket buffer 转移至发送队列 qdisc。
调整方法:
ifconfig eth0 txqueuelen 10000
qdisc 权重
即一次 NET_TX_SOFTIRQ 软中断最大发送报文数,默认64。
调整方法:
sysctl -w net.core.dev_weight=600
tx descriptor 数量
即,多少个发送描述符。
调整方法:
ethtool -G eth0 rx 1024 tx 1024
高级特性:TSO/GSO/XPS
TSO(TCP Segmentation Offload):超过 MTU 大小的报文不需要在协议栈分段,直接由网卡分段,降低 CPU 负载。
GSO(Generic Segmentation Offload):TSO 的软件实现,延迟大报文分段时机到 IP 层结束或者设备层发包前,不同版本内核实现不同。
开启方法:
ethtool -K eth0 gso on
ethtool -K eth0 tso on
XPS(Transmit Packet Steering):对于有多队列的网卡,XPS 可以建立 CPU 与 tx queue 的对应关系,对于单队列网卡,XPS 没啥用。
开启方法:
内核配置CONFIG_XPS
建立cpu与发送队列映射:
echo cpu_mask > /sys/class/net/eth0/queues/tx-<n>/xps_cpus
驱动层
协议栈层
驱动层调用 napi_gro_receive()
,开始协议栈处理,对于 ICMP 报文,经过的处理函数:ip_rcv()
-> raw_local_deliver()
-> raw_rcv()
-> __sock_queue_rcv_skb()
现在内核都使用 GRO 机制将驱动层 skb 上送协议栈,GRO 全称 Generic Receive Offload,是网卡硬件的 LRO 功能(Intel 手册使用 RSC 描述)的软件实现,可以将同一条流的报文聚合后再上送协议栈处理,降低 CPU 消耗,提高网络吞吐量。
送入 socket buffer
基于 poll/epoll 机制唤醒等待 socket 的进程
应用读取报文
rx descriptor 长度
瞬时流量太大,软件收取太慢,rx descriptor 耗尽后肯定丢包。增大 rx descriptor 长度可以减少因为瞬时流量大造成的丢包。但是不能解决性能不足造成的丢包。
调整方法:
查看 rx descriptor 长度:ethtool -g eth0
调整 rx descriptor 长度:ethtool -G eth0 rx 1024 tx 1024
中断
为了充分利用多核 CPU 性能,高性能(现代)网卡一般支持多队列。配合底层 RSS(Receive-Side Scaling)机制,将报文分流到不同的队列。每个队列对应不同的中断,进而可以通过中断亲和性将不同的队列绑定到不同的 CPU。
PS:只有 MSI-X 才支持多队列/多中断。
调整方法:
中断亲和性设置:echo cpu_mask > /proc/irq/<irq_no>/smp_affinity
查看队列数:ethtool -l eth0
调整队列数:ethtool -L eth0
进阶:队列分流算法设置
NAPI
网卡中断中触发 NET_RX_SOFTIRQ 软中断,软中断中调用驱动 poll 函数,进行轮询收包。NAPI 的好处在于避免了每个报文都触发中断,避免了无意义的上下文切换带来的 Cache/TLB miss 对性能的影响。
但是 Linux 毕竟是通用操作系统,NAPI 轮询收包也要有限制,不能长时间收包,不干其他活。所以 NET_RX_SOFTIRQ 软中断有收包 budget 概念。即,一次最大收取的报文数。
收包数超过 netdev_budget(默认300)或者收包时间超过2个 jiffies 后就退出,等待下次软中断执行时再继续收包。驱动层 poll 收包函数默认一次收64个报文。
netdev_budget 调整方法:
sysctl -w net.core.netdev_budget=600
驱动层 poll 函数收包个数只能修改代码。一般不需要修改。
开启高级特性 GRO/RPS/RFS
GRO(Generic Receive Offload):驱动送协议栈时,实现同条流报文汇聚后再上送,提高吞吐量。对应 napi_gro_receive
函数。
开启方法:
ethtool -K eth0 gro on
RPS(Receive Packet Steering):对于单队列网卡,RPS 特性可以根据报文 hash 值将报文推送到其它 CPU 处理(把报文压入其它 CPU 队列,然后给其它 CPU 发送 IPI 中断,使其进行收包处理),提高多核利用率,对于多队列网卡,建议使用网卡自带的 RSS 特性分流。
RFS(Receive Flow Steering):RFS 在 RPS 基础上考虑了报文流的处理程序运行在哪个 CPU 上的问题。比如报文流 A 要被运行在 CPU 2 的 APP A 处理,RPS 特性会根据报文 hash 值送入 CPU 1,而 RFS 特性会识别到 APP A 运行在 CPU 2 信息,将报文流 A 送入CPU 2。
PS: RFS 的核心是感知报文流的处理程序运行在哪个 CPU,核心原理是在几个收发包函数(inet_recvmsg()
, inet_sendmsg()
, inet_sendpage()
、tcp_splice_read()
)中识别并记录不同报文流的处理程序运行在哪个CPU。
RPS 开启方法:
echo cpu_mask > /sys/class/net/eth0/queues/rx-0/rps_cpus
RFS 设置方法:
sysctl -w net.core.rps_sock_flow_entries=32768
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
PS: RFS 有些复杂,这里只说举例配置,不讲述原理(其实就是不会。。。)
socket buffer
__sock_queue_rcv_skb()
中会将 skb 放入 socket buffer,其中会检查 socket buffer 是否溢出。所以要保证 socket buffer 足够大。
设置方法:
sysctl -w net.core.rmem_max=xxxxxx -- 限制最大值
sysctl -w net.core.rmem_default=yyyyyy
欢迎点击文末左下方 “阅读原文”。
扫 码 关 注 我 们
再 + 好 友 tinylab
进 泰 晓 技 术 群
泰 晓 科 技
关注“泰晓科技”!点“在看”