Linux 网络:性能杀手 spinlock

文摘   科技   2023-05-03 08:10   新加坡  

在内核网络协议栈里,有不少地方使用到了 spinlock(此 spinlockstruct bpf_spin_lock)。如果使用不当,CPU 就会消耗在 spinlock 上。

最近正好死磕一个 Linux 网络性能问题,最终发现是 spinlock 导致的。

性能问题排查

因为是 Linux 网络性能问题,所以性能瓶颈在内核里,通过 top 等常规手段行不通了。

需要通过以下手段一顿操作:

  1. mpstat -p ALL 1
  2. perf top --cpu=3
  3. timeout 5 bpftrace -e 'kprobe:native_queued_spin_lock_slowpath{@[stack(12)] = count()}'
  4. 参考 CPU Flame Graphs[1] 制作 CPU 火焰图

最终发现,性能瓶颈在于 dev_queue_xmit() 设备层发包时的 native_queued_spin_lock_slowpath() spinlock 上。

深挖问题原因

技术背景:网卡有 N 个 txqueue,每个 txqueue 对应一个 CPU,每个 CPU 都会调用 __dev_xmit_skb() 函数通过 txqueue 发包。

先看一下在这情况下,内核协议栈的设备层是如何发包的。

dev_queue_xmit()                    // ${KERNEL}/net/core/dev.c
|-->__dev_queue_xmit() {
    |   txq = netdev_core_pick_tx(dev, skb, sb_dev);
    |   q = rcu_dereference_bh(txq->qdisc);
    |   rc = __dev_xmit_skb(skb, q, dev, txq);
    }
    |-->__dev_xmit_skb() {
        |   spinlock_t *root_lock = qdisc_lock(q);
        |   spin_lock(root_lock);   // 1. lock
        |   __qdisc_run(q);
        }
        |-->__qdisc_run()           // ${KERNEL}/net/sched/sch_generic.c
            |-->qdisc_restart()
                |-->sch_direct_xmit() {
                        spin_unlock(root_lock);
                        dev_hard_start_xmit()
                        spin_lock(root_lock);   // 2. lock
                    }

根据 bpftrace 的结果,分析出有 2 个地方调用 spin_lock() 函数上锁了。

解惑

疑问:这个 spinlock 从属于哪个 qdisc 呢?

从上面代码片段可知,这个 spinlock 来自 txqueue 上的 qdisc

继续问:txqueue 上的 qdisc 是从哪里来的呢?

似乎没有简单的答案,因为涉及到了复杂的 tc 子系统。

直接分析一下 tc 里给设备 EGRESS 创建 qdisc 的过程吧。

tc_modify_qdisc()                  // ${KERNEL}/net/sched/sch_api.c
|-->qdisc_graft() {
        for (i = 0; i < num_q; i++) {
            dev_queue = netdev_get_tx_queue(dev, i);
            old = dev_graft_qdisc(dev_queue, new);
        }
    }

这是每个 txqueue 都嫁接了同一个 qdisc

解法

似乎没有比较直接简单的解法,只能从根本上解决问题。

奇葩需求,奇葩解法:分而治之,避免所有 txqueue 共享一个 qdisc

因奇葩需求,所以需要将网络包从网卡接收到内核协议栈,在设备层特殊处理一下后,再从网卡发出去。

而网卡有 N 个 rxqueue,解法就是为每个 rxqueue 都创建一对 veth 设备、每对 veth 设备都有 N 个 txqueue,最后从 veth 对端设备将网络包转发到网卡发送出去。

如此,网卡上就没有 tc EGRESS 规则了,这里就不会有 spinlock 问题了。

奇葩小需求:在 tc-bpf 里如何根据 rxqueue index 将网络包转发到指定 veth 设备呢?

根据 eBPF Talk: tc-bpf 转发网络包,tc-bpf 不支持 bpf_redirect_map() helper 函数,所以有没有办法做到类似 bpf_redirect_map() 的功能呢?

答案是肯定的,而且有多种方法。

  1. 使用一个 bpf map 保存 veth 设备的 ifindex,使用 rxqueue index 当作 key 查询得到 veth 设备的 ifindex。
  2. 把该 bpf map 改为一个 ifindex 数组的全局变量,使用 rxqueue index 当作索引查询得到 veth 设备的 ifindex。
  3. 把该全局变量改为一个 ifindex 数组的常量,使用 rxqueue index 当作索引查询得到 veth 设备的 ifindex。

得到 ifindex 后使用 bpf_redirect(ifindex, 0) 函数将网络包转发到指定 veth 设备。

小结

  1. spinlock 会导致 CPU 消耗,特别是在多 CPU 上。
  2. 用分治法,避免多个 CPU 共享一个 spinlock

参考资料

[1]

CPU Flame Graphs: https://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html

eBPF Talk
专注于 eBPF 技术,以及 Linux 网络上的 eBPF 技术应用