在网卡领域,多队列技术已经是一项很常用的技术,而网络报文的负载分流,常见的就是RSS。本文就索性仔细分析下RSS的逻辑实现,同时以SpinalHDL给出RSS中最为关键的Toeplitz Hash的实现。
RSS(receive side scaling)是由微软提出的一种负载分流方法,通过计算网络数据报文中的网络层&传输层二/三/四元组HASH值,取HASH值的最低有效位(LSB)用于索引间接寻址表RETA(Redirection Table),间接寻址表RETA中的保存索引值用于分配数据报文到不同的CPU接收处理。现阶段RSS基本已有硬件实现,通过这项技术能够将网络流量分载到多个CPU上,降低操作系统单个CPU的占用率。RSS的整体实现如下图所示:
RSS Hash计算所采用的Hash算法为Toeplitz Hash。Toeplitz Hash算法定义如下:
function ComputeRssHash(Input[],RSK)
ret=0;
for each bit b in Input[] do
if b==1 then
ret^=(left-most 32 bit of RSK)
shift RSK left 1 bit position
end for
Toeplitz Hash需要一个RSK。在DPDK的实现里,key往往大小为40Byte。结合上述算法,320 bit的RSK对于Toeplitz Hash实现可满足$320-32+1=289$bit大小的Input输入。在DPDK中,存有一张默认的RSK,定义如下:
uint8_t default_rss_key[] = {
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
};
当然,如果Input位宽大于289 bit,个人有观察到DPDK代码中也提供了一张RSK 大表:
static const uint8_t big_rss_key[] = {
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,
0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,
0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,
0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,
};
对比可以看到这里的大表是上面40Byte表的复制
采用上面的RSK存在一个问题就是同一个连接的不同方向使用这个默认值计算出来的hash值是不一样的。具体讲就是{src: 1.1.1.1, dst: 2.2.2.2, srcport: 123, dstport: 456}和{src: 2.2.2.2, dst: 1.1.1.1, srcport: 456, dstport: 123} 计算出来的hash值是不一样的,hash值不一样就会导致两个方向的报文被分发到不同的接收队列,由不同的CPU进行处理。
在网络应用中,如果同一个连接的双向报文在开启RSS之后被分发到同一个CPU上处理,这种RSS就称为对称RSS。对于需要为连接保存一些信息的网络应用来说,对称RSS对性能提升有很大帮助。如果同一个连接的双向报文被分发到不同的CPU,那么两个CPU之间共享这个连接的信息就会涉及到锁,而锁显然是会影响性能的。
在DPDK中,提供了一组用于实现对称RSS的RSK:
static const uint8_t toeplitz_symmetric_key[] = {
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
};
关于对称RSK是如何推导的,感兴趣的小伙伴可以去搜索这篇论文:《Scalable TCP Session monitoring with Symmetric Receive-side Scaling》,这里不做理论上的推导。
在上面所提到的Toeplitz Hash算法,其无非一个for循环按位进行异或处理。对于熟悉HLS的同学来讲或许觉得这个实现是很简单的。然而HLS并非我的选择。在for循环里面的if对于逻辑设计而言并非一个友好的实现。上面的算法可以略作修改:
function ComputeRssHash(Input[],RSK)
ret=0;
for each bit b in Input[] do
ret^=(left-most 32 bit of RSK)&{32{b}}
shift RSK left 1 bit position
end for
这个等价改动很简单,学过数字电路的同学都明白。
在SpinalHDL中可以很轻松地实现类似软件风格的可综合电路。这就是SpinalHDL这门语言的简洁优雅之处。
上面的算法实现可以说是简单的算法翻译。Toeplitz Hash算法本质上是一个FG(2)矩阵乘:
那么该电路可以实现为如下的方式:
这两种方式是等价的,其综合出来的电路也是一致的,至于倾向使用哪种方式,小伙伴们可以自行选择了。
对于Toeplitz Hash算法实现,虽然逻辑级数并不高,但如果RSK完全由软件进行配置,那么消耗的资源还是相对较多的。针对大多数场景,一般只使用对称RSK或者非对称RSK,且一般应用中选择DPDK中指定的RSK,那么在逻辑实现里将RSK给固定下来综合所使用的逻辑资源就很少了。
而针对需要选择变换对称RSK或者非对称RSK的情况,可以选择在逻辑中制作一个开关用于指定选择对称还是非对称RSK。以上面所提到的DPDK中的两个KEY为例,逻辑实现如下图所示:
值得注意的是,只例化一个ToeplitzHashCal而根据symmetric的值选择输入的rsk则消耗的资源相对较多。
原作者:玉骐
更多回帖