关键数据 per-cpu及无锁化
内核性能问题的一大原因就是资源共享和锁。所以,被频繁访问的关键数据需要尽可能的实现无锁化,其中一个方法是将数据做到 per-cpu 化,每个 CPU 只处理自己本地的数据,不需要访问其他 CPU 的数据,这样就可以避免加锁。就 DPVS 而言,连接表,邻居表,路由表等,都是频繁修改或者频繁查找的数据,都做到了 per-cpu 化。
在具体 per-cpu 的实现上,连接表和邻居表、路由表并不相同。对于连接表,高并发的情况下,不光是查找,还会被频繁地添加、删除。我们让每个 CPU 维护的是不相同的连接表,
不同的网络数据流(TCP/UDP/ICMP)按照 N 元组被定向到不同的 CPU,在此特定 CPU 上创建、查找、转发、销毁。同一个数据流的包,只会出现在某个 CPU 上,不会落到其他的 CPU 上。这样就可以做到不同的 CPU 只维护自己本地的表,无需加锁。另一方面,对于邻居和路由表,这种系统“全局”的数据,每个 CPU 都是要用到它们的。如果不采用”全局表+锁保护“的方式,而要做成 per-cpu,也需要让每个 CPU 有同样的视图,也就是每个 CPU 需要维护同样的表。对于这两个表,采用了跨 CPU 无锁同步的方式,虽然在具体实现上有小的差别,本质上都是通过跨 CPU 通信(路由是直接传递信息,邻居是克隆数数据并传递分组给别的 CPU),将表的变化同步到每个 CPU。不论用了什么方法,关键数据做到了 per-cpu 之后没有了锁的需求,性能也就能提升了。
- 跨 CPU 无锁消息
之前已经提到过这点了。首先,虽然采用了关键数据 per-cpu等优化,但跨 CPU 还是需要通信的,比如:
- Master 获取各个 Worker 的各种统计信息
- Master 将路由、黑名单等配置同步到各个 Worker
- Master 将来自 KNI 的数据发送到 Worker(只有 Worker 能操作 DPDK 接口发送数据)
- 既然需要通信,就不能存在互相影响、相互等待的情况,因为那会影响性能。为此,我们使用了 DPDK 提供的无锁 rte_ring 库,
从底层保证通信是无锁的,并且我们在此之上封装一层消息机制来支持一对一,一对多,同步或异步的消息。
- 网卡队列 /CPU 绑定
现代的网卡支持多个队列,队列可以和 CPU 绑定,让不同的 CPU 处理不同的网卡队列的流量,分摊工作量,实现并行处理和线性扩展。
DPVS是由各个 Worker 使用 DPDK 的 API 处理不同的网卡队列,每个 Worker 处理某网卡的一个接收队列,一个发送队列,实现了处理能力随CPU核心、网卡队列数的增加而线性增长。