网络性能调优
RSS
当前多数据网卡支持多个接收和发送队列(multi-queue),在接收方,NIC 可以将不同的 packet 分发给不同的 CPU。NIC 通过一个 filter 将每个 packet 分到不同的 flows 中,每个 flow 的 packet 都被分到同一个接收队列中,而每个接收队列可以由一个独立的 CPU 来处理。这个技术即是 Receive-Side Scaling, RSS 。
RSS 实现
这里的 filter 一般是一个 hash 函数,它以网络数据包的头文件为 key,比如说以 IP 地址和 TCP 端口的 4 元组为 key 进行 hash。RSS 最常见的硬件实现是一个 128-entry 的 indirection table,每个 entry 存储了一个 queue number。一个 packet 所属的接收队列是由 hash (通常是 Toeplitz hash) 计算出来的低 7bit 作为 key,从 indirection table 中拿到 queue number。有一些更高级的网卡支持 programmable filter,比如对 80 端口的 webserver 映射到固定的接收队列。这种 n-tuple 可以通过 ethtool 的 --config-ntuple 配置。
参考 Intel 82599 的 datasheet,看看它是如何实现 RSS 的,如下图:
- Parsed receive packet 解析数据包,获取五元组等信息
- RSS hash 根据五元组的某些信息计算 hash 值
- Packet Descriptor 将 hash 值保存到接收描述符中,最终会保存到 skb->hash 中,后续可以直接使用 hash 值,比如 RPS 查找 cpu 时使用这个 hash 值
- 7 LS bits 使用 hash 值低 7 位索引 redirection table 的一项,每项包含四位(所以最多支持 16 个队列)
- RSS output index table 的指定项就是接收队列。
那 redirection table 中每一项中的队列 id 是如何设置的呢?在驱动初始化时,根据使能的队列个数,依次填充到每一项,达到队列最大值后,从 0 开始循环填充。比如使能了 4 个队列,则 table 的 0-127 项依次为:0,1,2,3,0,1,2,3 …
看下 ixgbe 中使用到的和 redirection table 相关的寄存器,使用 32 个 IXGBE_RETA 寄存器,每个寄存器的 3:0, 11:8,19:16 和 27:24 分别表示一个 table 的 entry,而且是 4 位,所以使能 RSS 时最多支持 16 个队列。
|
|
设置多队列 IRQ 绑核
每一个接收队列都有自己的 IRQ number,NIC 通过 IRQ 通知 CPU 数据包到来,对于 PCIe 类型设备使用 MSI-X 类型中断。我们可以通过配置 /proc/irq/IRQ_NUMBER/smp_affinity 来配置 IRQ 与 CPU 的 affinity,具体可以参考 SMP IRQ affinity。
对称多处理器(symmetric multiprocessing)是通过多个处理器处理程序的方式。smp_affinity文件处理一个 IRQ 号的中断亲和性。在smp_affinity文件结合每个 IRQ 号存储在/proc/irq/IRQ_NUMBER/smp_affinity文件。这个文件中的值是一个 16 进制位掩码表示系统的所有 CPU 核。
|
|
smp_affinity是 16 进制表示,f 就是二进制的1111 ,表示 4 个 cpu 都会参与处理中断,这里ff表示有 8 个 cpu 核心同时处理中断。
这个中断分布的 cpu 核也可以从 smp_affinity_list 看到(是数字表示)
|
|
如果我想将 IRQ 18 的 SMP Affinity 设置为 5 号 CPU,则可以操作如下,因为十六进制 20 的二进制对应着 00100000
|
|
每个 IRQ 的默认的 smp affinity 在这里:cat /proc/irq/default_smp_affinity
查看网卡对应的 IRQ 号
一般情况下,我们可以通过 /proc/interrupts 查看 IRQ 和与网卡的对应关系,比如这里的 eth0 对应着 IRQ Number 为 90,并且在 CPU0 上产生了 1070374 次中断。
|
|
当 CPU 数目很多的时候,而且有的时候一张网卡有多个网卡队列,直接查看 /proc/interrupts 很不直观,可以通过其他方法查看。
对于 virtio interface,我们可以查看网卡对应的 PCI 接口下的 msi_irqs 得到本网卡对应的 IRQ Numbers。
|
|
对于容器场景,比如 TKE 的独立网卡网络方案,如果 Pod 将网卡绑定到它的 netns 中,在主 netns 中看不到对应的网卡,则需要进入到 Pod netns
|
|
可以看到,这里主要是通过$(readlink -e /sys/class/net/eth0) 查找 PCI 设备 /sys/devices/pci<domain>:<bus>/ 下的 msi_irqsorirq` 文件,更多可以参考 这里 。
为了使你配置的 irq smp affinity 生效,要注意关掉节点上的 irqbalance 进程。irqbalance 会自动将 IRQs 平衡到各个 CPU,可能会覆盖你的 smp_affinity 配置。
适用场景
一般来说,我们会对多队列网卡进行一对一 CPU 中断绑核,在 这里 可以看到相关脚本。
注意,我们一般不会前几号 CPU 用作网络收包,因为他们一般都会有一些定时扫描,任务平衡等任务
如果将接收队列中断绑定到这些核上面去,可能会导致 ping flood 抖动延时等问题。
默认情况下,使用
/proc/irq/default_smp_affinity设置到全 F 可能就会选中到前几个核
建议设置 RSS 的时候,可以将不同网卡队列的 IRQ 均分到不同 CPU,实现每个 CPU 处理各自的硬中断, 这样每个 CPU 的负载不会过大。如果想要查看每个 CPU 的负载,可以通过 mpstat 工具查看,具体参考 Shell 笔记。
RPS
Receive Packet Steering, RPS 是 RSS 的软件实现。因为是软件实现,所以 RPS 在 data path 的较后面实现,而 RSS 是直接在中断前就通过硬件分发给不同的网卡队列。RPS 相对于 RSS 由以下特点:
- RPS 可以被用在任何 NIC 上,不依赖于 NIC 的硬件能力
- software filter 可以很容易的加入来对新的协议进行 hash,而 RSS 需要 NIC 硬件实现 filter
- RPS 不会增大硬件的 interrupt rate,虽然它确实会引入 IPIs,Inter-Processor Interrupts
RPS 实现
RPS 在网络中断底半部被调用,在 netif_rx_internal (传统中断模式)或者 netif_receive_skb_internal (NAPI 模式下),如果使能了 RPS,则调用 get_rps_cpu 选择合适的 cpu,将 skb 放入此 CPU 的 backlog 队列中,然后 waking up the CPU for processing。这样就可以让多个 CPU 来处理协议栈的工作,避免一个 CPU 负载过大。
|
|
设置 /sys/class/net/<dev>/queues/rx-<n>/rps_cpus 时:
|
|
RPS 选择 CPU 的第一步是计算 flow 的 hash。这个 hash 作为一致性 hash,可以直接使用 hardware 算出来并保存在 skb 的 hash,一般也是 RSS 使用的 hash (即 computed Toeplitz hash)。如果没有硬件算出来的 hash 的话,可以使用软件计算 hash。
|
|
At the end of the bottom half routine, IPIs are sent to any CPUs for which packets have been queued to their backlog queue. The IPI wakes backlog processing on the remote CPU, and any queued packets are then processed up the networking stack.
get_rps_cpu 函数中也涉及到了 RFS 的流程,这里先忽略 RFS 流程,只关注 RPS 相关的。因为 RPS 设置的是某个队列对应的 CPU 列表,所以需要先获取队列 id,再获取此队列对应的 CPU 列表。
|
|
RPS 配置
RPS 要求 kernel 开启了 CONFIG_RPS 的选项,这对于 SMP 系统是默认的。为了打开 RPS 的能力,需要通过 sysfs 配置
|
|
对于单队列网卡,RPS 将会设置 rps_cpus 到接收中断的 CPU 的同一个 memory domain 中,这里的 memory domain 说的是一个 CPU 集合
a memory domain is a set of CPUs that share a particular memory level (L1, L2, NUMA node, etc.)
如果 NUMA 局部性不是问题,那么就会 rps_cpus 配置的就是所有的 CPU。当收发包速率较高时,一般会把接收中断的 CPU 从这个 rps_cpus 的 map 中去掉,因为它已经在处理很多的任务。
对于多队列网卡,如果已经配置了 RSS,那么 RPS 的配置可能是冗余和没必要的。如果 接收队列的数目比 CPU 的数目少,那么当 CPU 的处理能力不够时,可以通过 RPS 将不同的接收队列分配到各自的 memory domain 上。
RPS Flow Limit
RPS 在不同 CPU 之间分发 packet,但是,如果一个 flow 特别大,会出现单个 CPU 被打爆,而其他 CPU 无事可做(饥饿)的状态。因此引入了 flow limit 特性,放到一个 backlog 队列的属于同一个 flow 的包的数量不能超过一个阈值。这可以保证即使有一个很大的 flow 在大量收包 ,小 flow 也能得到及时的处理。
|
|
1 2 3 4 5 6 7 8 9 10Once a CPU's input packet queue exceeds half the maximum queue length (as set by sysctl net.core.netdev_max_backlog), the kernel starts a per-flow packet count over the last 256 packets. If a flow exceeds a set ratio (by default, half) of these packets when a new packet arrives, then the new packet is dropped. Packets from other flows are still only dropped once the input packet queue reaches netdev_max_backlog. No packets are dropped when the input packet queue length is below the threshold, so flow limit does not sever connections outright: even large flows maintain connectivity.
默认,flow limit 功能是关掉的。要打开 flow limit,需要指定一个 bitmap(类似于 RPS 的 bitmap)。
|
|
监控:由于 input_pkt_queue 打满或 flow limit 导致的丢包,在/proc/net/softnet_stat 里面的 dropped 列计数。
如果使用了 RPS,或者驱动调用了 netif_rx,那增加 netdev_max_backlog 可以改善在 enqueue_to_backlog 里的丢包:
例如:
increase backlog to 3000 with sysctl.
|
|
默认值是 1000。
backlog 处理逻辑和设备驱动的 poll 函数类似,都是在软中断(softirq)的上下文中执行,因此受整体 budget 和处理时间的限制。
Tuning: Enabling flow limits and tuning flow limit hash table size
|
|
默认值是 4096
这只会影响新分配的 flow hash table。所以,如果你想增加 table size 的话,应该在打开 flow limit 功能之前设置这个值。打开 flow limit 功能的方式是,在/proc/sys/net/core/flow_limit_cpu_bitmap 中指定一 个 bitmask,和通过 bitmask 打开 RPS 的操作类似。
RFS
从 RPS 选择 CPU 方法可知,就是使用 skb 的 hash 随机选择一个 CPU,没有考虑到应用层运行在哪个 CPU 上,如果执行软中断的 CPU 和运行应用层的 CPU 不是同一个 CPU ,势必会降低 CPU Cache 命中率,降低性能。一般来说,高性能场景下都会为应用设置 CPU Affinity,将应用和 CPU 绑核。
为了解决这个问题,RFS 通过指派应用程序所在的 CPU 来在内核态处理报文,以此来增加 CPU 的缓存命中率。RFS 主要是通过两个流表来实现的:
- 设备流表,记录的是上次在内核态处理该流中报文的 CPU
- 全局的 socket 流表,记录的是流中的报文渴望被处理的目标 CPU
原理是将运行应用的 CPU 保存到一个表中,在 get_rps_cpu 时,从这个表中获取 CPU,即可保证执行软中断的 CPU 和运行应用层的 CPU 是同一个 CPU。
全局 socket 流表
全局 socket 流表 rps_sock_flow_table 的定义如下:
|
|
mask 成员存放的就是 ents 这个柔性数组的大小,该值也是通过配置文件的方式指定的,相关的配置文件为 /proc/sys/net/core/rps_sock_flow_entries,可以通过 sysctl 修改 net.core.rps_sock_flow_entries 配置:
|
|
|
|
rps_sock_flow_table 是一个全局的数据流表,这个表中包含了数据流渴望被处理的 CPU。这个 CPU 是当前处理流中报文的应用程序所在的 CPU。全局 socket 流表会在调 recvmsg,sendmsg (特别是 inet_accept(), inet_recvmsg(), inet_sendmsg(), inet_sendpage() and tcp_splice_read()),被设置或者更新。
全局 socket 流表会在调用 recvmsg()等函数时被更新,而在这些函数中是通过调用函数 sock_rps_record_flow() 来更新或者记录流表项信息的,而sock_rps_record_flow() 中最终又是调用函数 rps_record_sock_flow() 来更新 ents 柔性数组的,该函数实现如下:
|
|
设备流表
|
|
struct rps_dev_flow 类型弹性数组大小由配置文件 /sys/class/net/(dev)/queues/rx-(n)/rps_flow_cnt 进行指定的。这个表可以记录之前 cpu backlog上数据包何时处理完,等数据包都处理完后就可以将流迁移到新的 CPU 上了,这样就可以避免调度到新的 CPU 时候出现乱序。
|
|
RFS 实现
下面再次分析 get_rps_cpu,看看 RFS 是如何生效的:
|
|
更新 rflow->cpu 为 next_cpu,并且记录 next_cpu 队列的 input_queue_head 到 rflow->last_qtail 中,后续数据包入队到 next_cpu 队列上时,rflow->last_qtail 都会加 1,通过判断 input_queue_head 和 rflow->last_qtail 来判断 next_cpu 队列是否为空。
|
|
Accelebrated RFS
RFS 是将 skb 放在运行应用的 CPU 的 backlog 中处理的,而且我们知道默认情况下哪个 CPU 处理硬件中断,就由哪个 CPU 处理软件中断,即 who trigger, who run,那能不能通过网卡的 fdir 功能(流重定向) 将数据流重定向到运用应用的 CPU 所处理的队列上呢?这就是 Accelerated RFS 的作用。
aRFS 之于 RFS 就像 RSS 之于 RPS,是一种硬件加速的负载均衡机制,直接将 flows 发送给接收 packet 的应用所在的 CPU。具体的实现是,网络协议站会调用驱动中的 ndo_rx_flow_steer 来将 flow 分发到 desired hardware queue。每次在 rps_dev_flow_table 的 flow entry 更新后,网络协议栈会调用 ndo_rx_flow_steer 。
除了使能 RFS 的两个表,没其他需要使能的,前提是网卡驱动得支持函数 ndo_rx_flow_steer,不过貌似支持的网卡没几个。
|
|
XPS
前面的几种技术都是接收方向的,XPS 是针对发送方向的,即从网卡发送出去时,如果有多个发送队列,选择使用哪个队列。
可通过如下命令设置,此命令表示运行在 f 指定的 cpu 上的应用调用 socket 发送的数据会从网卡的 tx-n 队列发送出去。
|
|
虽然设置的是设备的 tx queue 对应的 cpu 列表,但是转换到代码中保存的是每个 cpu 可使用的 queue 列表。因为查找 xps_cpus 时,肯定是已知 cpu id,寻找从哪个 tx queue 发送。
选择 tx queue 时,优先选择 xps_cpu 指定的 queue,如果没有指定就使用 skb hash 计算出来一个。当然也不是每个报文都得经过这个过程,只有 socket 的第一个报文需要,选择出 queue 后,将此 queue 设置到 sk->sk_tx_queue_mapping,后续报文直接获取 sk_tx_queue_mapping 即可。
和 XPS 相关的几个结构体
|
|
设置 xps_cpus 时调用如下函数
|
|
设置完 xps_cpus 后,就可以在发送数据时选择指定的 queue
|
|
根据当前运行的 cpu raw_smp_processor_id,来查找 tx queue。 如果此 cpu 只对应一个 queue,就使用这个 queue,如果设置了多个 queue,还得使用 skb hash 选择一个。
|
|
XPS,全称为 Transmit Packet Steering,是软件支持的发包时的多队列,于 kernel 2.6.38 添加此特性。
通常 RPS 和 XPS 同 id 的队列选择的 CPU 相同,这也是防止不同 CPU 切换时性能消耗。
Linux 通过配置文件的方式指定哪些 cpu 核参与到报文的分发处理,配置文件存放的路径是:/sys/class/net/(dev)/queues/tx-(n)/xps_cpus。例如:
|
|
内核中有关 xps 最主要的函数就是 get_xps_queue (关于配置如何映射到内核可参考 RPS)。
|
|
GRO
Large Receive Offloading (LRO) 是一个硬件优化,Generic Receive Offloading (GRO) 是 LRO 的一种软件实现。
两种方案的主要思想都是:通过合并“足够类似”的包来减少传送给网络栈的包数,这有助于减少 CPU 的使用量。例如,考虑大文件传输的场景,包的数量非常多,大部分包都是一段文件数据。相比于每次都将小包送到网络栈,可以将收到的小包合并成一个很大的包再送到网络栈。GRO 使协议层只需处理一个 header,而将包含大量数据的整个大包送到用户程序。
这类优化方式的缺点是信息丢失:包的 option 或者 flag 信息在合并时会丢失。这也是为什么大部分人不使用或不推荐使用 LRO 的原因。
LRO 的实现,一般来说对合并包的规则非常宽松。GRO 是 LRO 的软件实现,但是对于包合并的规则更严苛。如果用 tcpdump 抓包,有时会看到机器收到了看起来不现实的、非常大的包, 这很可能是系统开启了 GRO。接下来会看到,tcpdump 的抓包点(捕获包的 tap )在 GRO 之后。
使用 ethtool 的 -k 选项查看 GRO 配置:
|
|
-K 修改 GRO 配置:
|
|
注意:对于大部分驱动,修改 GRO 配置会涉及先 down 再 up 这个网卡,因此这个网卡上的连接 都会中断。
GSO/TSO
计算机网络上传输的数据基本单位是离散的网包,既然是网包,就有大小限制,这个限制就是 MTU(Maximum Transmission Unit)的大小,(以太网)一般是 1500 字节(这里的 MTU 所指的是无需分段的情况下,可以传输的最大 IP 报文(包含 IP 头部,但不包含协议栈更下层的头部))。比如我们想发送很多数据出去,经过 os 协议栈的时候,会自动帮你拆分成几个不超过 MTU 的网包。然而,这个拆分是比较费计算资源的(比如很多时候还要计算分别的 checksum),由 CPU 来做的话,往往会造成使用率过高。
那可不可以把这些简单重复的操作 offload 到网卡上呢?于是就有了 LSO(Large Segment Offload ),在发送数据超过 MTU 限制的时候(太容易发生了),OS 只需要提交一次传输请求给网卡,网卡会自动的把数据拿过来,然后进行切片,并封包发出,发出的网包不超过 MTU 限制。
现在基本上用不到 LSO,已经有更好的替代。
- TSO (TCP Segmentation Offload): 是一种利用网卡来对大数据包进行自动分段,降低 CPU 负载的技术。 其主要是延迟分段。
- **GSO **(Generic Segmentation Offload): GSO 是协议栈是否推迟分段,在发送到网卡之前判断网卡是否支持 TSO,如果网卡支持 TSO 则让网卡分段,否则协议栈分完段再交给驱动。 如果 TSO 开启,GSO 会自动开启。
以下是 TSO 和 GSO 的组合关系:
- GSO 开启, TSO 开启:协议栈推迟分段,并直接传递大数据包到网卡,让网卡自动分段。
- GSO 开启, TSO 关闭:协议栈推迟分段,在最后发送到网卡前才执行分段。
- GSO 关闭, TSO 开启:同 GSO 开启, TSO 开启。
- GSO 关闭, TSO 关闭:不推迟分段,在 tcp_sendmsg 中直接发送 MSS 大小的数据包。
开启 GSO/TSO
驱动程序在注册网卡设备的时候默认开启 GSO: NETIF_F_GSO
|
|
驱动程序会根据网卡硬件是否支持来设置 TSO: NETIF_F_TSO
|
|
是否推迟分段
GSO/TSO 是否开启是保存在 dev->features 中,而设备和路由关联,当我们查询到路由后就可以把配置保存在 sock 中。
比如在 tcp_v4_connect 和 tcp_v4_syn_recv_sock 都会调用 sk_setup_caps 来设置 GSO/TSO 配置。
需要注意的是,只要开启了 GSO,即使硬件不支持 TSO,也会设置 NETIF_F_TSO,使得 sk_can_gso(sk)在 GSO 开启或者 TSO 开启的时候都返回 true。
|
|
sk_setup_caps
|
|
从上面可以看出,如果设备开启了 GSO,sock 都会将 TSO 标志打开,但是注意这和硬件是否开启 TSO 无关,硬件的 TSO 取决于硬件自身特性的支持。
sk_can_gso
|
|
net_gso_ok
|
|
由于 tcp 在 sk_setup_caps 中 sk->sk_route_caps 也被设置有 SKB_GSO_TCPV4,所以整个 sk_can_gso 成立。
GSO 的数据包长度
对紧急数据包或 GSO/TSO 都不开启的情况,才不会推迟发送, 默认使用当前 MSS。开启 GSO 后,tcp_send_mss 返回 mss 和单个 skb 的 GSO 大小,为 mss 的整数倍。
tcp_send_mss
|
|
tcp_xmit_size_goal
|
|
tcp_sendmsg
应用程序 send() 数据后,会在 tcp_sendmsg 中尝试在同一个 skb,保存 size_goal 大小的数据,然后再通过 tcp_push 把这些包通过 tcp_write_xmit 发出去。
(代码涉及较多,以后进行分析,TBD)
最终会调用 tcp_push 发送 skb,而 tcp_push 又会调用 tcp_write_xmit。tcp_sendmsg 已经把数据按照 GSO 最大的 size,放到一个个的 skb 中, 最终调用 tcp_write_xmit 发送这些 GSO 包。tcp_write_xmit 会检查当前的拥塞窗口,还有 nagle 测试,tsq 检查来决定是否能发送整个或者部分的 skb, 如果只能发送一部分,则需要调用 tso_fragment 做切分。最后通过 tcp_transmit_skb 发送, 如果发送窗口没有达到限制,skb 中存放的数据将达到 GSO 最大值。
tcp_write_xmit
|
|
其中 tcp_init_tso_segs 会设置 skb 的 gso 信息后文分析。我们看到 tcp_write_xmit 会调用 tso_fragment 进行“tcp 分段”。而分段的条件是 skb->len > limit。这里的关键就是 limit 的值,我们看到在 tso_segs > 1 时,也就是开启 gso 的时候,limit 的值是由 tcp_mss_split_point 得到的,也就是 min(skb->len, window),即发送窗口允许的最大值。在没有开启 gso 时 limit 就是当前的 mss。
tcp_init_tso_segs
|
|
tcp_write_xmit 最后会调用 ip_queue_xmit 发送 skb,进入 ip 层。
流程图如下:
UFO
UFO(UDP fragmentation offload),UPD 的 offload。
GRE 及 VXLAN 接口初始化的时候,会置此位。
|
|
还有其他 driver 也支持,例如 macvlan、tun、virtnet 等。
总结
- 接收侧:
- RSS是网卡驱动支持的多队列属性,队列通过中断绑定到不同的 CPU,以实现流量负载。
- RPS是以软件形式实现流量在不同 CPU 之间的分发。
- RFS是报文需要在用户态处理时,保证处理的 CPU 与内核相同,防止缓存 miss 而导致的消耗。
- LRO 和 GRO,多个报文组成一个大包上送协议栈。
- 发送侧:
- XPS 软件多队列发送。
- TSO 是利用网卡来对大数据包进行自动分段,降低 CPU 负载的技术。
- GSO 是协议栈分段功能。分段之前判断是否支持 TSO,支持则推迟到网卡分段。 如果 TSO 开启,GSO 会自动开启。
- UFO 类似 TSO,不过只针对 UDP 报文。
参考资料
-
No backlinks found.