F-Stack发送零拷贝介绍

数据包在服务器的处理分接收和发送两个方向,收包方向因为我们自己本身的业务场景涉及收包数据很少,后续另行介绍。

本文主要介绍F-Stack发包方向上当前的零拷贝处理方案、效果和应用场景的选择,发包方向上的数据拷贝目前主要为两个阶段,一是协议栈数据拷贝到DPDK的rte_mbuf中,二是应用层调用socket发送接口时会将数据从应用层拷贝到FreeBSD协议栈,下面将分别进行介绍。

协议栈到DPDK

该过程的零拷贝实现由 @jinhao2 提交的Pull Request #364 合并到F-Stack主线中,相关实现细节可以参考相关代码,这里仅对实现方案进行简要介绍。

方案介绍

  • 进程初始化时,通过mmap 为 BSD 堆栈分配指定大小的内存(目前默认256M),可以通过在config.ini中通过参数memsz_MB修改默认配置。
  • 通过 mlock() 固定物理内存,防止被换出到交换分区造成内存虚拟地址和物理地址对应关系的变化。
  • 计算每个页面的起始地址并保存,包括虚拟地址和物理地址,物理地址的计算可以通过DPDK的提供的相关接口进行。
  • 初始化一个堆栈结构来管理所有分配的页面。
  • 通过从已经初始化的堆栈结构中获取/释放一页来替换 ff_mmap()/ff_munmap()的实际mmap行为,而BSD协议栈调用kmem_malloc()/kmem_free()时调用ff_mmap()/ff_munmap()来获取内存页。
  • 在将BSD协议栈mbuf的数据地址赋值给DPDK的rte_mbuf时用于判断是否为初始化申请的内存池中的地址,并通过虚拟地址查找对应的物理地址,分别赋值给rte_buf结构的buf_addr/buf_physaddr,而不再实际进行内存拷贝。
  • 使用一个循环队列保存发送的mbuf的指针,队列的长度应该与NIC的tx_queue_length相同。在队列中的一项被推入新值之前,旧的 mbuf 必须由 NIC 处理并且可以安全地释放。
  • 如果mbufext_cluster类型,其中包括一个rte_mbuf,表示是收包时零拷贝附加的数据地址,则使用 rte_pktmbuf_clone()代替。

使用方式及注意事项

使用方式

该功能默认并未开启,需要通过在lib/Makefile中打开编译选项FF_USE_PAGE_ARRAY,并重新编译F-Stack lib 库和应用程序后才能生效。

其他应用编程及使用方式与常规拷贝模式没有区别,对应用层透明。

注意事项

  • 内存池初始化时在本进程通过mmapmlock申请,为进程私有地址空间,相关内存不能传递到其他进程使用。
    • 可以考虑在初始化时映射大页内存或者使用共享内存(同样需要SHM_LOCkmlock锁定内存,防止交换)来达到可以跨进程使用的目的,但是对应的地址保存和查找结构也需要进行变更,一般应用建议避免跨进程使用即可,不建议进行修改。
  • 协议栈到DPDK的零拷贝功能可以单独开启FF_USE_PAGE_ARRAY使用,也可以与零拷贝发送接口FF_ZC_SEND一起开启使用。
  • 此处减少的内存拷贝是否对应用性能有提升还需要结合具体的应用进行实际测试,数据包在一定大小且使用方式合适时则可以有一定的性能优化效果,但优化效果并不一定很明显,比如只有2-3%左右的提升。

应用层到协议栈

通过提供单独的零拷贝API,使应用层在通过socket接口发送数据时,避免应用层到BSD协议栈的数据拷贝,具体细节见提交e12886c,下面将进行较为具体的介绍。

方案介绍

  • 提供单独的零拷贝结构体ff_zc_mbuf,用于应用层缓存结构,后续应用层的数据操作和发送都应该使用该结构体,具体类型如下所示:struct ff_zc_mbuf {
      void *bsd_mbuf;         /* 指向BSD mbuf链的头节点 */
      void *bsd_mbuf_off;     /* 指向BSD mbuf链中偏移off后的当前节点 */
      int off;               /* mbuf链中的偏移量,应用层不应该直接修改 */
      int len;               /* 申请的mbuf链缓存的总长度,小于等于mbuf链实际能承载的数据长度 */
    };
  • 提供接口ff_zc_mbuf_get(),用于应用提前申请包含可以由内核直接使用的mbuf的结构体作为应用层数据缓存,接口声明如下。int ff_zc_mbuf_get(struct ff_zc_mbuf *m, int len);该接口输入struct ff_zc_mbuf *指针和需要申请的缓存总长度,内部将通过m_getm2()分配mbuf链,首地址保存在ff_zc_mbuf结构的bsd_mbuf变量中,后续可以传递给ff_write()接口。其中m_getm2()为标准socket接口拷贝应用层数据到协议栈时分配mbuf链的接口,所以使用该接口范围的mbuf链作为应用层缓存,可以在发送数据时完全兼容。
  • 提供了缓存数据写入函数ff_zc_mbuf_write(),函数声明如下,
  • int ff_zc_mbuf_write(struct ff_zc_mbuf *m, const char *data, int len); 应用层在保存待发送的数据时,应通过接口ff_zc_mbuf_wirte()直接将数据写到ff_zc_mbuf指向的mbuf链的缓存中,ff_zc_mbuf_wirte()接口可以多次调用写入缓存数据,接口内部自动处理缓存的偏移情况,但多次总的写入长度不能超过初始申请的缓存长度
  • 应用调用ff_write()接口时指定传递ff_zc_mubf.bsd_mbufbuf参数,示例如下所示,ff_write(clientfd, zc_buf.bsd_mbuf, buf_len);在m_uiotombuf()函数中,直接使用传递的mbuf链的首地址,不再额外进行mbuf链的分配和数据拷贝,如下所示,#ifdef FSTACK_ZC_SEND
    if (uio->uio_segflg == UIO_SYSSPACE && uio->uio_rw == UIO_WRITE) {
    m = (struct mbuf *)uio->uio_iov->iov_base; /* 直接使用应用层的mbuf链首地址 */
    uio->uio_iov->iov_base = (char *)(uio->uio_iov->iov_base) + total;
    uio->uio_iov->iov_len = 0;
    uio->uio_resid = 0;
    uio->uio_offset = total;
    progress = total;
    } else {
    #endif
    m = m_getm2(NULL, max(total + align, 1), how, MT_DATA, flags); /* 拷贝模式分配mbuf链*/
    if (m == NULL)
    return (NULL);
    m->m_data += align;

    /* Fill all mbufs with uio data and update header information. */
    for (mb = m; mb != NULL; mb = mb->m_next) {
    length = min(M_TRAILINGSPACE(mb), total – progress);

    error = uiomove(mtod(mb, void *), length, uio); /* 拷贝模式拷贝应用层数据到协议栈 */
    if (error) {
    m_freem(m);
    return (NULL);
    }

    mb->m_len = length;
    progress += length;
    if (flags & M_PKTHDR)
    m->m_pkthdr.len += length;
    }
    #ifdef FSTACK_ZC_SEND
    }
    #endif
  • ff_write()函数成功返回后,之前申请的ff_zc_mbuf结构内部mbuf链数据不需要释放,该结构可以在函数ff_zc_mbuf_get()中复用重新分配BSD的mbuf
    • 不能够再次直接在ff_zc_mbuf_wirte()使用,必须重新调用ff_zc_mbuf_get()分配新的mbuf链之后才可以继续使用

使用方式及注意事项

使用方式

该功能默认并未开启,需要通过在lib/Makefile中打开编译选项FF_ZC_SEND,并重新编译F-Stack lib 库和应用程序后才能生效。

零拷贝发送接口的使用方式与标准socket接口也有区别,具体可以参考前面的方案介绍及示例代码

注意事项

  • 使用零拷贝发送接口需要对原有应用进行修改才能接入,且并不一定有很明显的性能提升,所以默认不开启。
  • 零拷贝发送接口可以单独开启FF_ZC_SEND使用,也可以与FF_USE_PAGE_ARRAY一起开启使用。
  • 与协议栈到DPDK的零拷贝类似,此处减少的内存拷贝是否对应用性能有提升还需要结合具体的应用进行实际测试,在特定应用场景下才会有一定的性能提升,但效果并不一定很明显,比如只有2-3%左右的提升。
  • 目前struct ff_zc_mbuf *的结构是对外暴露给应用层的,可以更方便的进行测试使用,后续不排除隐藏该数据结构的可能。

F-Stack常用配置参数介绍

目前F-Stack的配置文件中包含有以下8个部分,下面将分别进行简单的介绍:

[dpdk]、[pcap]、[portN]、[vdevN]、[bondN]、[kni]、[freebsd.boot]、[freebsd.sysctl]

[DPDK]

设置运行DPDK的相关参数,如果是DPDK也有的参数,则含义和使用方法同DPDK参数。

lcore_mask

16进制位掩码,用于设置进程运行在哪些CPU核心上。如fc表示使用CPU第2-7个核,不使用第0和1核。

建议优先使用物理核,数据尽量不要跨NUMA节点交互,可以空出前2个CPU核心给系统,且配置其他进程不调度到DPDK要使用的CPU核心上。

channel

内存通道数,一般无需修改,使用默认值即可。

base_virtaddr

指定mmap内存到主进程的虚拟地址,默认关闭。

某些特定场景下可能需要使用,如自动分配的虚地址与其他地址冲突时,可以多次尝试使用DPDK启动时的错误提示进行指定或在应用中尝试修改初始化F-Stack(DPDK)的位置。

promiscuous

0或1,是否开启网卡的混杂模式,默认开启。

建议开启,尤其是对可能需要处理多播包(如OSPF协议包)等场景。

numa_on

0或1,是否开启NUMA支持,默认开启。

建议开启。

tx_csum_offoad_skip

0或1,是否关闭发包校验和的卸载,默认否。

当网卡支持发包校验和卸载时,F-Stack正常总是开启该功能,一般不需要修改。该参数配置为1时,则不会设置发包校验和的网卡硬件卸载,用于某些特殊场景,如需要发送错误的校验和用于测试、或某些网卡宣传支持发包校验和卸载但实际并未计算校验和等。

tso

0或1,是否开启TCP分段卸载(TCP segment offload),默认关闭。

理论上开启应该有更好的性能表现,TCP协议栈无需对大包进行软件分段,交给网卡硬件进行,但目前实测并未表现出性能优势,所以默认关闭。

vlan_strip

0或1,是否开启VLAN卸载(TCP segment offload),默认开启。

开启后,网卡会将收包的VLAN头卸载剥离,某些特殊场景可能需要关闭该功能,如KNI需要VLAN的场景,详细介绍见前期文章《F-Stack vlan 的支持与使用》。

idle_sleep

当前循环未收到数据包的空闲休眠时间,单位微秒,默认0,即一直保持轮询模式,不进行休眠,CPU使用率为100%。

线上实际使用时建议设置为不超过100的值,即当本次循环没有收到数据包时,休眠不超过100微秒,主要目的是降低CPU使用率,且实际对线上业务基本无影响,但是会增加单连接小数据量的收包延迟,如果单纯想测试收发包延迟情况或不在意线上CPU使用率一直保持100%,可以设置为0

目前DPDK已经支持中断+轮询模式,但是F-Stack初始开发时(2012年)DPDK尚未支持中断模式,所以在当时的业务中引入了该参数用于降低CPU使用率,虽然后来DPDK支持了中断模式,但因为影响基本可以忽略,F-Stack目前暂未支持中断模式。

pkt_tx_delay

F-Stack发包延迟时间,单位微秒,默认为100,支持配置范围[0,100],配置超过100时强制置为100。

类似于TCP中的delay ack的概念,为了使用批量发包提升最大的并发吞吐量性能,F-Stack在发包时会先进行缓存并延迟发送,实际发包的触发条件有两个,凑够一次批量发包的包数(目前硬编码为32),或延迟发包时间超时。

默认延迟发包可以提升大并发下的吞吐量性能,但是会增加单连接小数据量的发包延迟,如果单纯想测试收发包延迟情况,可以设置为0,则每次发包都会立即实际发送。除了测试使用,一般不建议修改为0

symmetric_rss

0或1,是否开启对称RSS,默认否。

网关或类似服务可以开启对称RSS选项,通过设置特殊的RSS hash key,使四元组中IP和端口号互换的数据包可以收到同一队列(CPU)中,主要目的是增加CPU的缓存命中率 。

pci_whitelist

F-Stack(DPDK)可以识别加载的网卡设备白名单,默认为所有支持的设备。参数值为设备号,如02:00.0或02:00.0,03:00.0,主要用于仅希望指定的网卡设备可以被DPDK识别使用时。

port_list

F-Stack(DPDK)实际要接管的网卡(网口)设备序号列表,从0开始。如00,1,20-2等。

可以与pci_whitelist配合使用,仅从白名单中的网口设备从0开始进行排序编号。

设置了接管几个网口,后面就应该配置几个对应的[portN]的地址信息配置段,N为网卡网口序号。

当使用bonding模式时,参数值应为bonding虚拟设备的网口号(从实际的设备数往上递增),不应该包含slave设备的网口号。

nb_vdev

配置有几个容器虚拟设备,设置了几个设备,后面就应该配置几个对应的[vdevN]的信息配置段,N为容器编号。

因为容器是F-Stack是第一个支持的虚拟设备,此处的vdev仅用于配置容器参数,其他虚拟设备则使用对应的设备类型来配置,如bonding。

nb_bond

配置有几个bonding虚拟设备,设置了几个设备,后面就应该配置几个对应的[bondN]的信息配置段,N为bonding设备编号。

file_prefix

文件前缀,主要用于同时启动不同的F-Stack(DPDK)进程组,通过不同的配置文件中配置不同的文件前缀,可以同时启动多个主进程及其对应的辅进程,某些特殊场景可能会用到。

no_huge

0或1,是否不使用大页内存,默认为0,即使用大页内存,一般无需修改。

[PCAP]

抓包相关配置选项,每个进程分别写入自己的抓包文件。需要注意的是开启抓包将会严重影响性能,一般仅调试时使用。

enable

0或1,是否开启抓包,默认否。

snaplen

每个包的最大抓包长度,默认96字节。

savelen

单个抓包文件的大小限制,达到限制后将重新打开新的抓包文件,默认值16777216,即16M。

savepath

抓包文件保存目录,默认为.,即程序启动目录。

[portN]

配置网口的地址等相关信息,N对应[DPDK]段的port_list值,如0,1,2,5等,每一个接管的网口都需要单独的一段[portN]来进行配置

addr

网口需要配置的IPv4地址,此处仅支持配置一个IP。

netmask

IPv4掩码。

broadcast

IPv4广播地址。

gateway

IPv4路由地址。

if_name

可选参数,配置F-Stack中的设备名称,默认为f-stack-N,N从0开始,与PortN对应。>= 1.22。

addr6

可选参数,配置本网口的IPv6地址。

prefix_len

IPv6的prefix len,配置了addr6之后才需要配置,默认64。

gateway6

配置了addr6之后的可选参数,当本地IPv6的环境不使用NDP时才需要配置(如腾讯云),如果使用NDP则不需要配置(如AWS)。

vip_ifname

虚拟IP配置到哪个网口设备,默认f-stack-N,根据实际需要可以配置到lo0等设备。>= 1.22。

vip_addr

分号分隔的IPv4虚拟地址,最大支持64个虚拟地址。目前不支持单独配置掩码和广播地址,在函数ff_veth_setvaddr中硬编码使用255.255.255.255x.x.x.255。>= 1.22。

vip_addr6

分号分隔的IPv6虚拟地址,最大支持64个虚拟地址。>= 1.22。

vip_prefix_len

虚拟IPv6地址的prefix_len,所有地址只能使用统一前缀,默认为64。>= 1.22。

lcore_list

使用哪些CPU核心处理本网口的队列,格式与port_list一致,默认为全部CPU核心都绑定处理本网口的队列。

不同进程之间是数据隔离的,如果需要在不同网口间转发数据,必须同一个CPU核心同时绑定处理多个网卡的队列或自行进行IPC,使用时需要注意,一般无特殊需求的话,无需修改配置该参数。

slave_port_list

当本网口为bonding虚拟设备的时候需要配置该参数,指定组成本bonding的slave网口,配置格式与port_list一致,如0,10-1

[vdevN]

配置容器的相关信息,N对应[DPDK]段的nb_vdev值,如0,1,2,5等,每一个虚拟设备都需要单独的一段[vdevN]来进行配置

iface

默认值/usr/local/var/run/openvswitch/vhost-userN,不应该设置修改。

path

必选参数,容器内的vhost user设备路径,如/var/run/openvswitch/vhost-userN,

queues

可选参数, vuser的最大队列数,应等于或大于F-Stack的进程数,默认为1。

queue_size

可选参数,队列大小,默认值256。

mac

可选参数,vuser设备的MAC地址,默认值为随机地址。

如果vhost使用物理网卡,则vuser的MAC地址应设置为物理网卡的MAC地址。

cq

可选参数,如果队列数queues为1,则设置为0,默认值。如果队列数queues大于1,则设置为1。

[bond0]

配置bonding虚拟设备的相关信息,N对应[DPDK]段的nb_vdev值,如0,1,2,5等,每一个虚拟设备都需要单独的一段[bondN]来进行配置。

此处仅简单介绍下配置项,bonding的具体信息可以参考DPDK的帮助文档 http://doc.dpdk.org/guides/prog_guide/link_bonding_poll_mode_drv_lib.html

需要注意的时,当前DPDK的bonding驱动不支持多进程模式,而F-Stack目前仅支持多进程模式,多线程模式需要使用方自行修改测试。

mode

bonding模式,默认为模式4,该模式需交换机配置支持。

slave

子设备号列表,多个子设备时需设置多个k=v格式,逗号分隔,如slave=0000:0a:00.0,slave=0000:0a:00.1

primary

主设备号,如0000:0a:00.0

mac

bonding设备的MAC地址,一般可以设置为主网口的MAC地址。

其他可选参数

具体含义可以参考DPDK相关文档

  • socket_id=0
    • NUMA节点号,根据实际设置
  • xmit_policy=l23
    • 转发负载均衡策略
  • lsc_poll_period_ms=100
  • up_delay=10
  • down_delay=50

[kni]

配置kni数据包转发到内核相关参数,配置文件中默认未开启kni段,如需要需自行取消注释并配置相关参数。

enable

0或1,是否开启kni。

method

rejectaccept,配置kni转发的默认策略。

如果设置为reject,则下面tcp_portudp_port指定的数据转发到F-Stack进程协议栈,除此之外其他数据包都转发到内核。

如果设置为accept,则下面tcp_portudp_port指定的数据转发到内核,除此之外其他数据包都转发到F-Stack进程协议栈。

tcp_port

kni转发过滤器过滤的TCP端口,配置格式与port_list一致,如80,443

udp_port

kni转发过滤器过滤的UDP端口,配置格式与port_list一致,如53,443

kni_action

defaultalltoknialltoff,可选参数,可以通过工具knictl分进程控制不同进程的kni转发策略。>= 1.22。

default,默认值,使用上面的通用kni转发配置。

alltokni,所有数据包通过kni转发到内核。

`alltoff,所有数据包转发到F-Stack协议栈。

FreeBSD

网络调优配置,包含一些F-Stack独有的配置,其他为FreeBSD的配置项,绝大部分FreeBSD的配置项都支持,但此处仅列举了少数配置,详细的配置项可以通过工具ff_sysctl -a获取,配置项的详细信息则可以参考FreeBSD的man page。

[freebsd.boot]

hz

定时器每秒扫描频率,默认为100,即10ms精度,无特殊需求一般无需修改。

调大该值可以提高定时器精度,但是不一定会提高性能,目前建议不要设置太高,如不超过1000。

注意:目前F-Stack 1.22版本(尚未正式发布)使用的FreeBSD 13.0,支持开启RACK和BBR,而RACK和BBR都依赖高精度定时器,目前该版本的RACK和BBR暂时都无法正常工作,不排除会受定时器精度影响,后续将进行调试排查。

physmem

一个进程使用的内存大小,单位字节,默认256M,无特殊需求无需修改。

memsz_MB

开启编译选项FF_USE_PAGE_ARRAY之后有效,每进程mmap的页面数组内存大小,单位M字节,默认256M,无特殊需求无需修改。

FF_USE_PAGE_ARRAY编译选项用于开启发送数据包时FreeBSD协议栈到DPDK的零拷贝,虽然减少了内存数据拷贝,但是因为多了一些其他操作,性能不一定提升,如小数据包发送时,开启该选项是否能提升性能需要使用方在自己的使用场景单独进行对比测试

目前应用层到FreeBSD协议栈的socket接口的发包零拷贝也已经支持,正在测试中,在某些特定场景会有一定的性能提升,同样的对特定应用场景是否有提升需使用方单独开启测试,预计很快将提交代码到1.22版本(dev分支),但该功能需要修改应用层的socket接口使用行为,由使用方自行选择是否使用。

fd_reserve

屏蔽一系列描述符以避免与内核的描述符空间重叠,默认1024,即应用层从1024开始分配fd。 您可以根据您的应用增加此值。

特别的,某些较老应用支持的fd范围有限,移植到F-Stack之后可能无法正常运行,需要减小该值。

其他协议栈选项

根据F-Stack调优过的协议栈选项,无特殊需求一般无需修改,相关限制数值都为进程级,非全局限制,因为F-Stack每个进程启动了一个独立的协议栈。部分参数值设置错误可能导致F-Stack进程的协议栈异常,如部分参数值要求必须为2的N次幂。

  • kern.ipc.maxsockets=262144
  • net.inet.tcp.syncache.hashsize=4096
  • net.inet.tcp.syncache.bucketlimit=100
  • net.inet.tcp.tcbhashsize=65536
  • kern.ncallout=262144
  • kern.features.inet6=1
    • 开启IPv6支持,IPv6的部分参数也可以参考前期文章《F-Stack IPv6 的支持与使用》。
  • net.inet6.ip6.auto_linklocal=1
  • net.inet6.ip6.accept_rtadv=2
  • net.inet6.icmp6.rediraccept=1
  • net.inet6.ip6.forwarding=0

[freebsd.sysctl]

  • kern.ipc.somaxconn=32768
    • 等待连接数,应用层可能也可以根据需要配置backlog
  • kern.ipc.maxsockbuf=16777216
  • net.link.ether.inet.maxhold=5
  • net.inet.tcp.fast_finwait2_recycle=1
  • net.inet.tcp.sendspace=16384
  • net.inet.tcp.recvspace=8192
  • #net.inet.tcp.nolocaltimewait=1
    • 开启该参数可能导致某些场景的IPv6异常,所以关闭。
  • net.inet.tcp.cc.algorithm=cubic
    • 设置拥塞算法为cubic,FreeBSD的默认拥塞算法为new reno。当参数net.inet.tcp.functions_default设置为freebsd时有效.
  • net.inet.tcp.sendbuf_max=16777216
  • net.inet.tcp.recvbuf_max=16777216
  • net.inet.tcp.sendbuf_auto=1
  • net.inet.tcp.recvbuf_auto=1
  • net.inet.tcp.sendbuf_inc=16384
  • net.inet.tcp.recvbuf_inc=524288
  • net.inet.tcp.sack.enable=1
  • net.inet.tcp.blackhole=1
  • net.inet.tcp.msl=2000
  • net.inet.tcp.delayed_ack=1
    • 早期版本F-Stack默认没有开启dealy ack,当前版本修改为默认开启,可以提高大并发场景的吞吐量性能,但是会增加单连接小数据量的延迟,如需测试相关场景,可以关闭该功能,参考dpdk.pkt_tx_delay选项。
  • net.inet.udp.blackhole=1
  • net.inet.ip.redirect=0
  • net.inet.ip.forwarding=0
    • 当需要进行IP转发,数据不需要到应用层时需要开启该选项。
  • net.inet.tcp.functions_default=freebsd
    • freebsdrackbbr,设置使用FreeBSD支持的传统拥塞算法(通过参数net.inet.tcp.cc.algorithm设置),还是使用rack或bbr。>= 1.22。
    • 注意:当前尚未正式发布的1.22版本中的rack和bbr尚不能正常工作,需要进一步调试,对希望使用bbr拥塞算法的同学可以一起来调试并提交Pull Request。