想看能不能完整梳理一下收消息过程。从 NIC 收数据开始,到触发软中断,交付数据包到 IP 层再经由路由机制到 TCP 层,最终交付用户进程。会尽力介绍收消息过程当中的各类配置信息,以及各类监控数据。知道了收消息的完整过程,了解了各类配置,明白了各类监控数据后才有可能在从此的工做中作优化配置。linux
全部参考内容会列在这个系列最后一篇文章中。git
Ring Buffer 相关的收消息过程大体以下:github
图片来自参考1,对 raise softirq 的函数名作了修改,改成了 napi_schedulec#
NIC (network interface card) 在系统启动过程当中会向系统注册本身的各类信息,系统会分配 Ring Buffer 队列也会分配一块专门的内核内存区域给 NIC 用于存放传输上来的数据包。struct sk_buff 是专门存放各类网络传输数据包的内存接口,在收到数据存放到 NIC 专用内核内存区域后,sk_buff 内有个 data 指针会指向这块内存。Ring Buffer 队列内存放的是一个个 Packet Descriptor ,其有两种状态: ready 和 used 。初始时 Descriptor 是空的,指向一个空的 sk_buff,处在 ready 状态。当有数据时,DMA 负责从 NIC 取数据,并在 Ring Buffer 上按顺序找到下一个 ready 的 Descriptor,将数据存入该 Descriptor 指向的 sk_buff 中,并标记槽为 used。由于是按顺序找 ready 的槽,因此 Ring Buffer 是个 FIFO 的队列。api
当 DMA 读完数据以后,NIC 会触发一个 IRQ 让 CPU 去处理收到的数据。由于每次触发 IRQ 后 CPU 都要花费时间去处理 Interrupt Handler,若是 NIC 每收到一个 Packet 都触发一个 IRQ 会致使 CPU 花费大量的时间在处理 Interrupt Handler,处理完后又只能从 Ring Buffer 中拿出一个 Packet,虽然 Interrupt Handler 执行时间很短,但这么作也很是低效,并会给 CPU 带去不少负担。因此目前都是采用一个叫作 New API(NAPI)的机制,去对 IRQ 作合并以减小 IRQ 次数。网络
接下来介绍一下 NAPI 是怎么作到 IRQ 合并的。它主要是让 NIC 的 driver 能注册一个 poll
函数,以后 NAPI 的 subsystem 能经过 poll
函数去从 Ring Buffer 中批量拉取收到的数据。主要事件及其顺序以下:数据结构
poll
函数,用于后续从 Ring Buffer 拉取收到的数据poll
函数获取收到的 Packetpoll
完全部数据以前不会再有新的 IRQ从上面的描述能够看出来还缺一些东西,Ring Buffer 上的数据被 poll
走以后是怎么交付上层网络栈继续处理的呢?以及被消耗掉的 sk_buff 是怎么被从新分配从新放入 Ring Buffer 的呢?负载均衡
这两个工做都在 poll
中完成,上面说过 poll
是个 driver 实现的函数,因此每一个 driver 实现可能都不相同。但 poll
的工做基本是一致的就是:electron
若是拿 intel igb 这个网卡的实现来看,其 poll
函数在这里:linux/drivers/net/ethernet/intel/igb/igb_main.c - Elixir - Free Electronstcp
首先是看到有 tx.ring 和 rx.ring,说明收发消息都会走到这里。发消息先无论,先看收消息,收消息走的是 igb_clean_rx_irq。收完消息后执行 napi_complete_done
退出 polling 模式,并开启 NIC 的 IRQ。从而咱们知道大部分工做是在 igb_clean_rx_irq 中完成的,其实现大体上仍是比较清晰的,就是上面描述的几步。里面有个 while 循环经过 buget 控制,从而在 Packet 特别多的时候不要让 CPU 在这里无穷循环下去,要让别的事情也可以被执行。循环内作的事情以下:
看到 budget 会影响到 CPU 执行 poll
的时间,budget 越大当数据包特别多的时候能够提升 CPU 利用率并减小数据包的延迟。可是 CPU 时间都花在这里会影响别的任务的执行。
budget 默认 300,能够调整 sysctl -w net.core.netdev_budget=600
napi_gro_receive
会涉及到 GRO 机制,稍后再说,大体上就是会对多个数据包作聚合,napi_gro_receive
最终是将处理好的 sk_buff 经过调用 netif_receive_skb,将数据包送至上层网络栈。执行完 GRO 以后,基本能够认为数据包正式离开 Ring Buffer,进入下一个阶段了。在记录下一阶段的处理以前,补充一下收消息阶段 Ring Buffer 相关的更多细节。
GRO 是 Large receive offload 的一个实现。网络上大部分 MTU 都是 1500 字节,开启 Jumbo Frame 后能到 9000 字节,若是发送的数据超过 MTU 就须要切割成多个数据包。LRO 就是在收到多个数据包的时候将同一个 Flow 的多个数据包按照必定的规则合并起来交给上层处理,这样就能减小上层须要处理的数据包数量。
不少 LRO 机制是在 NIC 上实现的,没有实现 LRO 的 NIC 就少了上述合并数据包的能力。而 GRO 是 LRO 在软件上的实现,从而能让全部 NIC 都支持这个功能。
napi_gro_receive
就是在收到数据包的时候合并多个数据包用的,若是收到的数据包须要被合并,napi_gro_receive
会很快返回。当合并完成后会调用 napi_skb_finish
,将由于数据包合并而再也不用到的数据结构释放。最终会调用到 netif_receive_skb
将数据包交到上层网络栈继续处理。netif_receive_skb
上面说过,就是数据包从 Ring Buffer 出来后到上层网络栈的入口。
能够经过 ethtool 查看和设置 GRO:
查看 GRO ethtool -k eth0 | grep generic-receive-offload generic-receive-offload: on 设置开启 GRO ethtool -K eth0 gro on
NIC 收到数据的时候产生的 IRQ 只可能被一个 CPU 处理,从而只有一个 CPU 会执行 napi_schedule 来触发 softirq,触发的这个 softirq 的 handler 也仍是会在这个产生 softIRQ 的 CPU 上执行。因此 driver 的 poll
函数也是在最开始处理 NIC 发出 IRQ 的那个 CPU 上执行。因而一个 Ring Buffer 上同一个时刻只有一个 CPU 在拉取数据。
从上面描述能看出来分配给 Ring Buffer 的空间是有限的,当收到的数据包速率大于单个 CPU 处理速度的时候 Ring Buffer 可能被占满,占满以后再来的新数据包会被自动丢弃。而如今机器都是有多个 CPU,同时只有一个 CPU 去处理 Ring Buffer 数据会很低效,这个时候就产生了叫作 Receive Side Scaling(RSS) 或者叫作 multiqueue 的机制来处理这个问题。WIKI 对 RSS 的介绍挺好的,简洁干练能够看看: Network interface controller - Wikipedia
简单说就是如今支持 RSS 的网卡内部会有多个 Ring Buffer,NIC 收到 Frame 的时候能经过 Hash Function 来决定 Frame 该放在哪一个 Ring Buffer 上,触发的 IRQ 也能够经过操做系统或者手动配置 IRQ affinity 将 IRQ 分配到多个 CPU 上。这样 IRQ 能被不一样的 CPU 处理,从而作到 Ring Buffer 上的数据也能被不一样的 CPU 处理,从而提升数据的并行处理能力。
RSS 除了会影响到 NIC 将 IRQ 发到哪一个 CPU 以外,不会影响别的逻辑了。收消息过程跟以前描述的是同样的。
若是支持 RSS 的话,NIC 会为每一个队列分配一个 IRQ,经过 /proc/interrupts
能进行查看。你能够经过配置 IRQ affinity 指定 IRQ 由哪一个 CPU 来处理中断。先经过 /proc/interrupts
找到 IRQ 号以后,将但愿绑定的 CPU 号写入 /proc/irq/IRQ_NUMBER/smp_affinity
,写入的是 16 进制的 bit mask。好比看到队列 rx_0 对应的中断号是 41 那就执行:
echo 6 > /proc/irq/41/smp_affinity 6 表示的是 CPU2 和 CPU1
0 号 CPU 的掩码是 0x1 (0001),1 号 CPU 掩码是 0x2 (0010),2 号 CPU 掩码是 0x4 (0100),3 号 CPU 掩码是 0x8 (1000) 依此类推。
另外须要注意的是设置 smp_affinity 的话不能开启 irqbalance 或者须要为 irqbalance 设置 –banirq 列表,将设置了 smp_affinity 的 IRQ 排除。否则 irqbalance 机制运做时会忽略你设置的 IRQ affinity 配置。
Receive Packet Steering(RPS) 是在 NIC 不支持 RSS 时候在软件中实现 RSS 相似功能的机制。其好处就是对 NIC 没有要求,任何 NIC 都能支持 RPS,但缺点是 NIC 收到数据后 DMA 将数据存入的仍是一个 Ring Buffer,NIC 触发 IRQ 仍是发到一个 CPU,仍是由这一个 CPU 调用 driver 的 poll
来将 Ring Buffer 的数据取出来。RPS 是在单个 CPU 将数据从 Ring Buffer 取出来以后才开始起做用,它会为每一个 Packet 计算 Hash 以后将 Packet 发到对应 CPU 的 backlog 中,并经过 Inter-processor Interrupt(IPI) 告知目标 CPU 来处理 backlog。后续 Packet 的处理流程就由这个目标 CPU 来完成。从而实现将负载分到多个 CPU 的目的。
RPS 默认是关闭的,当机器有多个 CPU 而且经过 softirqs 的统计 /proc/softirqs
发现 NET_RX 在 CPU 上分布不均匀或者发现网卡不支持 mutiqueue 时,就能够考虑开启 RPS。开启 RPS 须要调整 /sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus
的值。好比执行:
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
表示的含义是处理网卡 eth0 的 rx-0 队列的 CPU 数设置为 f 。即设置有 15 个 CPU 来处理 rx-0 这个队列的数据,若是你的 CPU 数没有这么多就会默认使用全部 CPU 。甚至有人为了方便都是直接将 echo fff > /sys/class/net/eth0/queues/rx-0/rps_cpus
写到脚本里,这样基本能覆盖全部类型的机器,无论机器 CPU 数有多少,都能覆盖到。从而就能让这个脚本在任意机器都能执行。
注意:若是 NIC 不支持 mutiqueue,RPS 不是彻底不用思考就能打开的,由于其开启以后会加剧全部 CPU 的负担,在一些场景下好比 CPU 密集型应用上并不必定能带来好处。因此得测试一下。
Receive Flow Steering(RFS) 通常和 RPS 配合一块儿工做。RPS 是将收到的 packet 发配到不一样的 CPU 以实现负载均衡,可是可能同一个 Flow 的数据包正在被 CPU1 处理,但下一个数据包被发到 CPU2,会下降 CPU cache hit 比率而且会让数据包要从 CPU1 发到 CPU2 上。RFS 就是保证同一个 flow 的 packet 都会被路由到正在处理当前 Flow 数据的 CPU,从而提升 CPU cache 比率。这篇文章 把 RFS 机制介绍的挺好的。基本上就是收到数据后根据数据的一些信息作个 Hash 在这个 table 的 entry 中找到当前正在处理这个 flow 的 CPU 信息,从而将数据发给这个正在处理该 Flow 数据的 CPU 上,从而作到提升 CPU cache hit 率,避免数据在不一样 CPU 之间拷贝。固然还有不少细节,请看上面连接。
RFS 默认是关闭的,必须主动配置才能生效。正常来讲开启了 RPS 都要再开启 RFS,以获取更好的性能。这篇文章也有说该怎么去开启 RFS 以及推荐的配置值。一个是要配置 rps_sock_flow_entries
sysctl -w net.core.rps_sock_flow_entries=32768
这个值依赖于系统指望的活跃链接数,注意是同一时间活跃的链接数,这个链接数正常来讲会大大小于系统能承载的最大链接数,由于大部分链接不会同时活跃。该值建议是 32768,能覆盖大多数状况,每一个活跃链接会分配一个 entry。除了这个以外还要配置 rps_flow_cnt,这个值是每一个队列负责的 flow 最大数量,若是只有一个队列,则 rps_flow_cnt 通常是跟 rps_sock_flow_entries 的值一致,可是有多个队列的时候 rps_flow_cnt 值就是 rps_sock_flow_entries / N, N 是队列数量。
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
Accelerated Receive Flow Steering (aRFS) 相似 RFS 只是由硬件协助完成这个工做。aRFS 对于 RFS 就和 RSS 对于 RPS 同样,就是把 CPU 的工做挪到了硬件来作,从而不用浪费 CPU 时间,直接由 NIC 完成 Hash 值计算并将数据发到目标 CPU,因此快一点。NIC 必须暴露出来一个 ndo_rx_flow_steer
的函数用来实现 aRFS。
有的 NIC 支持这个功能,用来动态的将 IRQ 进行合并,以作到在数据包少的时候减小数据包的延迟,在数据包多的时候提升吞吐量。查看方法:
ethtool -c eth1 Coalesce parameters for eth1: Adaptive RX: off TX: off stats-block-usecs: 0 .....
开启 RX 队列的 adaptive coalescing 执行:
ethtool -C eth0 adaptive-rx on
而且有四个值须要设置:rx-usecs、rx-frames、rx-usecs-irq、rx-frames-irq,具体含义等须要用到的时候查吧。
ethtool -S eh0 NIC statistics: rx_packets: 792819304215 tx_packets: 778772164692 rx_bytes: 172322607593396 tx_bytes: 201132602650411 rx_broadcast: 15118616 tx_broadcast: 2755615 rx_multicast: 0 tx_multicast: 10
RX 就是收到数据,TX 是发出数据。还会展现 NIC 每一个队列收发消息状况。其中比较关键的是带有 drop 字样的统计和 fifo_errors 的统计 :
tx_dropped: 0 rx_queue_0_drops: 93 rx_queue_1_drops: 874 .... rx_fifo_errors: 2142 tx_fifo_errors: 0
看到发送队列和接收队列 drop 的数据包数量显示在这里。而且全部 queue_drops 加起来等于 rx_fifo_errors。因此整体上能经过 rx_fifo_errors 看到 Ring Buffer 上是否有丢包。若是有的话一方面是看是否须要调整一下每一个队列数据的分配,或者是否要加大 Ring Buffer 的大小。
/proc/net/dev
是另外一个数据包相关统计,不过这个统计比较难看:
cat /proc/net/dev Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 14472296365706 10519818839 0 0 0 0 0 0 14472296365706 10519818839 0 0 0 0 0 0 eth1: 164650683906345 785024598362 0 0 2142 0 0 0 183711288087530 704887351967 0 0 0 0 0 0
ethtool -l eth0 Channel parameters for eth0: Pre-set maximums: RX: 0 TX: 0 Other: 1 Combined: 8 Current hardware settings: RX: 0 TX: 0 Other: 1 Combined: 8
看的是 Combined 这一栏是队列数量。Combined 按说明写的是多功能队列,猜测是能用做 RX 队列也能当作 TX 队列,但数量一共是 8 个?
若是不支持 mutiqueue 的话上面执行下来会是:
Channel parameters for eth0: Cannot get device channel parameters : Operation not supported
看到上面 Ring Buffer 数量有 maximums 和 current settings,因此能本身设置 Ring Buffer 数量,但最大不能超过 maximus 值:
sudo ethtool -L eth0 combined 8
若是支持对特定类型 RX 或 TX 设置队列数量的话能够执行:
sudo ethtool -L eth0 rx 8
须要注意的是,ethtool 的设置操做可能都要重启一下才能生效。
先查看当前 Ring Buffer 大小:
ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 4096 RX Mini: 0 RX Jumbo: 0 TX: 4096 Current hardware settings: RX: 512 RX Mini: 0 RX Jumbo: 0 TX: 512
看到 RX 和 TX 最大是 4096,当前值为 512。队列越大丢包的可能越小,但数据延迟会增长
设置 RX 队列大小:
ethtool -G eth0 rx 4096
NIC 若是支持 mutiqueue 的话 NIC 会根据一个 Hash 函数对收到的数据包进行分发。能调整不一样队列的权重,用于分配数据。
ethtool -x eth0 RX flow hash indirection table for eth0 with 8 RX ring(s): 0: 0 0 0 0 0 0 0 0 8: 0 0 0 0 0 0 0 0 16: 1 1 1 1 1 1 1 1 ...... 64: 4 4 4 4 4 4 4 4 72: 4 4 4 4 4 4 4 4 80: 5 5 5 5 5 5 5 5 ...... 120: 7 7 7 7 7 7 7 7
个人 NIC 一共有 8 个队列,一个有 128 个不一样的 Hash 值,上面就是列出了每一个 Hash 值对应的队列是什么。最左侧 0 8 16 是为了能让你快速的找到某个具体的 Hash 值。好比 Hash 值是 76 的话咱们能当即找到 72 那一行:”72: 4 4 4 4 4 4 4 4”,从左到右第一个是 72 数第 5 个就是 76 这个 Hash 值对应的队列是 4 。
ethtool -X eth0 weight 6 2 8 5 10 7 1 5
设置 8 个队列的权重。加起来不能超过 128 。128 是 indirection table 大小,每一个 NIC 可能不同。
分配数据包的时候是按照数据包内的某个字段来进行的,这个字段能进行调整。
ethtool -n eth0 rx-flow-hash tcp4 TCP over IPV4 flows use these fields for computing Hash flow key: IP SA IP DA L4 bytes 0 & 1 [TCP/UDP src port] L4 bytes 2 & 3 [TCP/UDP dst port]
查看 tcp4 的 Hash 字段。
也能够设置 Hash 字段:
ethtool -N eth0 rx-flow-hash udp4 sdfn
sdfn 须要查看 ethtool 看其含义,还有不少别的配置值。
经过 /proc/softirqs
能看到每一个 CPU 上 softirq 数量统计:
cat /proc/softirqs CPU0 CPU1 HI: 1 0 TIMER: 1650579324 3521734270 NET_TX: 10282064 10655064 NET_RX: 3618725935 2446 BLOCK: 0 0 BLOCK_IOPOLL: 0 0 TASKLET: 47013 41496 SCHED: 1706483540 1003457088 HRTIMER: 1698047 11604871 RCU: 4218377992 3049934909
看到 NET_RX 就是收消息时候触发的 softirq,通常看这个统计是为了看看 softirq 在每一个 CPU 上分布是否均匀,不均匀的话可能就须要作一些调整。好比上面看到 CPU0 和 CPU1 两个差距很大,缘由是这个机器的 NIC 不支持 RSS,没有多个 Ring Buffer。开启 RPS 后就均匀多了。
/proc/interrupts
能看到每一个 CPU 的 IRQ 统计。通常就是看看 NIC 有没有支持 multiqueue 以及 NAPI 的 IRQ 合并机制是否生效。看看 IRQ 是否是增加的很快。