连接跟踪的设计
包处理系统中,一般会涉及到连接跟踪。连接跟踪系统,很多人都希望设计成为per-core session table/shared nothing,因为这种设计,不需要考虑共享和锁竞争,业务逻辑简单。这种设计的前提是,同流同核,也就是能保证流的两个方向的包可以落到一个核上。实际上,要保证这一点,在通用平台上,是困难的。因此,通用系统,比如内核中的IPVS,一般都不是这种设计,而是shared session table + RCU + timer + locks。因为IPVS涉及到了timer,而timer可能发生在中断上下文,因此当使用时,会涉及到大量的Kernel Domain knowledge,尤其是mod_timer
,mod_timer_if_pending
这些函数的使用场景,包传输即可能是中断上下文也可能是别的上下文,以及它们之间的竞态关系,而userspace-based系统则不需要考虑这些。
vSwitch连接跟踪设计
vSwitch,一般无法做到per-core/shared nothing架构。原因是要兼容vhostuser/virtio系统。Virtio接口和内核没有假设,同一条流一定会保证从一个口出。因为XPS/调度的原因,Virtio很难保证同流同核。当然,在DPU时代,这种假设可能会被打破。
共享流表的设计模式
Timer设计在PMD上
设计类似于内核的timer结构体,内嵌在session结构体中,提供mod_timer
的接口,以及超时回调函数。这种模式由于是仿造内核,且存在多个核之间对全局时间轮timer_list的锁竞争,需要好好设计。
单线程进行超时扫描
这种设计类似于netfilter
,即利用一个单一线程进行超时回收;同时在进行查找的PMD线程,在哈希查找时,顺便进行session超时,即在扫描冲突链表时,进行回收。这种设计的好处就是简单实用。
实际系统中,因为我们追求哈希冲突很小,实际上PMD上回收的session很少,扫描线程回收的很多;除非是严格控制冲突桶的数量。内核netfiler
的注释里,说大量的回收操作发生在查找时。我们的经验其实正好相反,除非超时时间很短,大部分都是扫描线程回收。
jitter问题
但是这种单线程超时扫描的方案,有一个很难想到的弱点,就是这个单一扫描线程,一般不会独占CPU,因为这个单一线程一般是比较闲的,如果独占CPU是比较浪费的。而这个单一线程,由于也会发生session回收,因此必然会持有共享session的锁。此时,由于这个单一线程可能会发生调度,因此会出现PMD线程试图创建/回收session,而此时扫描线程持锁但是被OS调度出去,造成PMD抢锁失败,产生巨大jitter,因为调度延迟一般是10~100ms级别的。
此时最好的解决方案就是将session表改成无锁哈希表 (lockless)。我们也试着将扫描线程改成实时线程,设计调度优先级为最高,但是这些都无济于事。当jitter发生时,会造成ping包有10ms~100ms的延迟。这个对一般延迟不敏感客户来说,不是问题,但是如果客户开始较真,怀疑你的系统有bug时,往往很难进行解释。
但是无锁哈希表,则需要较高的实现复杂度,且很容易出错。在C-based codebase里也比较难见到设计的比较好的无锁哈希表。
改回到timer系统?
我在上家公司的时候,尝试一种sub-timer的设计,这个设计利用了一个观察就是,流一般在同一个方向是基本可以保证同核,难点在于正反两个方向都落在同一个核。因此规避全局timer_list的锁竞争的方案就是,一个timer拆分成两个sub-timer,挂在两个不同的timer_list上。
但是当时在设计的时候也遇到了很多困难的竞态问题。第一,操作timer_list也要加锁,只是锁冲突降低了很多;第二,就是两个timer_list,却共享一个超时时间,在设计mod_timer
接口时,还是需要考虑很多。
另外,将session回收放在pmd上,会造成额外的CPU消耗,降低转发效能;而超时线程方案,由于可以利用非PMD CPU的资源,通常情况下,效率更好一点,尤其现代CPU的效率巨高,多用点CPU效果有时候好于精巧的设计。
利用Queue保证同流同核?
还有一种方案就是利用MPMC的无锁队列实现同流同核,即将包处理变成多个阶段,收到包的时候,通过解析包,将一条流的正反向的包通过队列放在同一个核上,然后在下个阶段,从queue里取包进行处理。但是这种方案一般实践下来,效果都比较差。原因就是将包放在queue上之后,包的header就变得cache-cold,另外,你需要额外空间存储parsed data,否则当线程从queue里取包时,又要重新解析一次,而且包也cache cold了,一般不适合独享cache越来越大的现代CPU。
这种方案有点类似Software Pipeline (SPL),在现代CPU里,很少看到真正有效的SPL模式。一般都是NP上,使用的是这种流水线模式。
总结
感觉没有特别好的设计模式,每一种模式都有弱点。扫描线程这种模式,还是比较简单实用,除了jitter这种缺点外,其他的问题可能是,不够scale,如果你的session表有几亿表项,可能单一线程会很吃亏。当然,vSwitch常见场景没有那么多session,就还好。