提高UDP交互性能
这是一篇个人认为非常非常厉害的文章,取自这里。讲述了如何提升UDP流的处理速率,但实际涉及的技术点不仅仅限于UDP。这篇文章中涉及的技术正好可以把前段时间了解的知识串联起来。作者:Toshiaki Makita
讲述内容
作者介绍
Toshiaki Makita
- NTT开源软件中心的Linux内核工程师
- NTT集团公司的技术支持
- 内核网络子系统的活跃补丁提交者
背景
因特网上UDP事务
以太网带宽和交互
- 以太网带宽演进:
- 10M -> 100M -> 1G -> 10G -> 40G -> 100G -> ...
- 10G(或更大)的NIC在商用服务器上越来越普遍
- 10G网络上的交互:
- 最小报文的场景下:最大 14,880,952 个报文/s (最小的以太帧为64字节+ preamble+IFG 20bytes = 84 bytes = 672 bits,10,000,000,000 / 672 = 14,880,952)
- 难以在单个服务器中处理
需要处理多少交互
- UDP 负载大小
- DNS
- A/AAAA请求:40~字节
- A/AAAA响应:100~字节
- RADIUS
- Access-Request:70~字节
- Access-Accept:30~字节
- 通常带有100个字节的属性
- 大部分场景下为100个字节
- 10G网络上100字节数据的交互
- 最大7,530,120次交互/s (100 bytes + IP/UDP/Ether headers 46bytes + preamble+IFG 20bytes = 166 bytes = 1328 bits ,即10,000,000,000 / 1328 = 7,530,120)
- 即使在少于最短的报文的情况下,但仍具有挑战性
提升网络性能的基本技术
TSO/GSO/GRO
- 报文分割/聚合
- 减少报文在服务中的处理
- 适用于TCP 字节流(使用UDP隧道的TCP也可以)
- 不适用于UDP数据报(除了UFO,其他都依赖物理NICs)
- UDP在数据报之间有明确的界限
- 不能分割/聚合报文
TSO/GSO用于发送报文时,将上层聚合的数据进行分割,分割为不大于MTU的报文;GRO在接受侧,将多个报文聚合为一个数据,上送给协议栈。总之就是将报文的处理下移到了网卡上,减少了网络栈的负担。TSO/GSO等可以增加网络吞吐量,但有可能造成某些连接上的网络延迟。
RSS
- 在多核服务器上扩展了网络接收侧的处理
- RSS本身是一个NIC特性
- 将报文分发到一个NIC中的多个队列上
- 每个队列都有一个不同的中断向量(不同队列的报文可以被不同的核处理)
- 可以运用于TCP/UDP
- 通常10G的NICs会支持RSS
RSS是物理网卡支持的特性,可以将NIC的多个队列映射到多个CPU核上进行处理,增加处理的效率,减少CPU中断竞争。
启用RSS的NIC的性能
- 100字节UDP交互性能
- 使用简单的echo多线程(线程数与核数相同,每个线程运行
recvfrom()
和sendto()
服务器进行测试
- OS:内核4.6.3(RHEL 7.2环境)
- 具有20个核心和10G NIC的中型商用服务器:
- NIC:Intel 82599ES (含RSS, 最大64 个队列)
- CPU:Xeon E5-2650 v3 (2.3 GHz 10 cores) * 2 sockets,禁用超线程
- 结果:270,000 transactions/s (tps) (大概 360Mbps)
如何提升
确认瓶颈
- softirq仅在NUMA的Node0上运行,为什么?
可以在/proc/zoneinfo中查看NUMA的node信息。使用mpstat也可以看到类似的现象,%irq
表示硬中断,%soft
表示软中断。
RSS下的softirq
- RSS会将报文分发到接收队列
- 每个队列的中断目的地由
/proc/irq/<irq>/smp_affinity
确定
RSS会将报文分发到不同的队列,smp_affinity会设置中断亲和性,将不同队列产生的中断上送给不同的CPU核。
- 通常由
irqbalance
设置smp_affinity
校验smp_affinity
irqbalance
仅使用了Node 0(核0-4, 10-14),如何修改?
检查affinity_hint
affinity_hint
是均匀分布的
- 为了显示该hint,可以在
irqbalance
(通过/etc/sysconfig/irqbalance)中添加"-h exact"选项
修改irqbalance选项
- 添加"-h exact"并重启
irqbalance
服务
- 可以看到irqs分布到了所有的核上。
- 虽然irqs看起来分布均匀,但16~19核却没有分配softirq
检查rx-queue状态
RSS 间接表
使用RPS
- 现在给接收队列69上的流分配CPU69 和16~19,这两组CPU都位于Node1 • rx-queue 6 -> core 6, 16 • rx-queue 7 -> core 7, 17 • rx-queue 8 -> core 8, 18 • rx-queue 9 -> core 9, 19
# echo 10040 > /sys/class/net/ens1f0/queues/rx-6/rps_cpus
# echo 20080 > /sys/class/net/ens1f0/queues/rx-7/rps_cpus
# echo 40100 > /sys/class/net/ens1f0/queues/rx-8/rps_cpus
# echo 80200 > /sys/class/net/ens1f0/queues/rx-9/rps_cpus
RSS & affinity_hint & RPS
- 多亏了affinity_hint 和RPS,现在可以将流均匀地分发到不同的CPU核上。
- 性能变化:
- Before: 270,000 tps (大概 360Mbps)
- After: 17,000 tps (大概 23Mbps)
变的更差了。。
- 可能的原因是软中断太多导致的
- 软中断几乎占了100%的CPU
- 需要更好地分析手段
分析软中断
- perf
- 举例:
perf record -a -g -- sleep 5
- 火焰图
- CPU0的火焰图(结果经过了过滤)
queued_spin_lock_slowpath
:锁竞争
udp_queue_rcv_skb
:要求socket锁
socket锁竞争
- echo服务器在一个特定端口上仅绑定了一个socket
- 每个内核的softirq同时将报文推入socket队列
避免锁竞争
- 使用
SO_REUSEPORT
选项分割sockets
- 该选项在内核3.9引入,默认使用流(报文首部)哈希来选择socket
使用SO_REUSEPORT
- 此时软中断消耗的CPU就比较合理了,从下面火焰图可以看到中断处理消耗的CPU缩短了
- 性能变化
- RSS: 270,000 tps (大概 360Mbps)
- +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
- +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
- 进一步分析:
- 可以看到,仍然有socket锁竞争。
SO_REUSEPORT
默认使用流哈希来选择队列,不同的CPU核可能会选择相同的sockets,导致竞争。
避免socket锁竞争
- 根据CPU核号选择socket
- 通过
SO_ATTACH_REUSEPORT_CBPF/EBPF
实现
- 在内核4.5引入上述功能
- 此时软中断之间不再产生竞争
- 用法可以参见内核源码树中的例子:tools /testing/selftests /net /reuseport_bpf_cpu.c
- 启用
SO_ATTACH_REUSEPORT_EPBF
前后的火焰图如下,可以看到中断消耗的CPU更少了
- 性能变化:
- RSS: 270,000 tps (approx. 360Mbps)
- +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
- +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
- +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
固定用户线程
- 用户线程数:sockets数 == 1:1,但不一定与软中断处于同一CPU核
- 将用户现场固定到相同的核,获得更好的缓存亲和性。可以使用cgroup, taskset, pthread_setaffinity_np()等方式
- 性能变化
- RSS: 270,000 tps (approx. 360Mbps)
- +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
- +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
- +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
- +Pin threads: 5,050,000 tps (大概 6710Mbps)
输出方向的锁
- 到目前为止解决的问题都处在接收方向上
- 发送方向是否有锁竞争?
Tx队列
- 内核具有Qdisc(默认的Qdisc为
pfifo_fast
)
- 每个Qdisc都连接到NIC的tx队列
- 每个Qdisc都有自己的锁
Tx队列的锁竞争
- Qdisc默认通过流哈希进行选择
- 因此可能会发送锁竞争
避免Tx队列的锁竞争
- 这是因为
ixgbe
(Intel 10GbE NIC驱动)可以自动设置XPS
- XPS允许内核选择根据CPU核号选择Tx队列(Qdisc)
XPS的影响如何
重新启用XPS
优化单个核 1
- 为了完全利用多核,并避免竞争,性能达到了5,050,000 tps (大概 6710Mbps)
- 为了进一步提高性能,需要降低单个核的开销
禁用GRO
- 性能变化 • RSS (+XPS): 270,000 tps (大概 360Mbps) • +affinity_hint+RPS: 17,000 tps (大概 23Mbps) • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps) • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps) • +Pin threads: 5,050,000 tps (大概 6710Mbps) • +Disable GRO: 5,180,000 tps (大概 6880Mbps)
优化单个核 2
优化单个核3
优化单个核4
优化单个核5
超线程
- 目前还没有启用超线程
- 启用之后的逻辑核为40个
- 需要给40个核配置RPS
- 启用超线程,并在所有的接收队列上设置RPS • queue 0 -> core 0, 20 • queue 1 -> core 1, 21 • ... • queue 10 -> core 10, 16, 30 • queue 11 -> core 11, 17, 31 • ...
- 性能变化 • RSS (+XPS): 270,000 tps (大概 360Mbps) • +affinity_hint+RPS: 17,000 tps (大概 23Mbps) • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps) • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps) • +Pin threads: 5,050,000 tps (大概 6710Mbps) • +Disable GRO: 5,180,000 tps (大概 6880Mbps) • +Unload iptables: 5,380,000 tps (大概 7140Mbps) • +Disable validation: 5,490,000 tps (大概 7290Mbps) • +Disable audit: 5,860,000 tps (大概 7780Mbps) • +Skip ID calculation: 6,010,000 tps (大概 7980Mbps) • +Hyper threading: 7,010,000 tps (大概 9310Mbps)
- 猜测,如果更多的rx队列可能会获得更好的性能
更多热点1
- Tx Qdisc锁(_raw_spin_lock)的消耗比较严重
- 没有竞争,但出现了很多原子操作
- 在Linux netdev社区中进行优化
更多热点2
- slab内存申请和释放
- 在Linux netdev社区中进行优化
其他挑战
- UDP服务器的环境为guest
- Hypervisor可能使CPU饱和或丢弃报文
总结
- 对于100字节的数据,可以达到几乎10G的速率
- 从:270,000 tps (approx. 360Mbps)
- 到:7,010,000 tps (approx. 9310Mbps)
- 提高UDP性能
- 应用(最关键)
- 实现
SO_REUSEPORT
- 实现
SO_ATTACH_REUSEPORT_EBPF/CBPF
- 对TCP监听socket同样有效
- OS设置
- 检查smp_affinity
- 如果rx队列不足,则使用RPS
- 确保配置了XPS
- 考虑如下降低单核开销的方法 • Disable GRO • Unload iptables • Disable source IP validation • Disable auditd
- 硬件
- 如果可能,使用具有足够RSS接收队列的NICs(如核数相同的队列)