有了协程库,开发DPDK应用程序第一次可以这么简单

本文涉及的产品
对象存储 OSS,20GB 3个月
云备份 Cloud Backup,100GB 3个月
文件存储 NAS,50GB 3个月
简介: 使用PhotonLibOS协程库,以多执行单元并发的代码模型代替原先的异步回调模型,简化DPDK应用程序的开发。同时使用echo server验证了 用户态TCP/IP协议栈+轮询模式驱动 对比 内核原生协议栈+中断模式驱动 的性能优势

  从2013年开源至今,DPDK已经走过了10个年头。从最初定位的一个简单的L2层高性能包转发工具,不断发展演进,到现在已经覆盖了软件定义网络(SDN)、主机虚拟化、网络协议栈、网络安全、压缩和加密、甚至是GPU设备和深度学习框架等各个方面,它在全球范围内得到了广泛的关注和研究,成为构建高性能网络应用程序的重要工具之一。


  DPDK最重要的特性就是用户态的 poll mode driver,相比于内核的 interrupt 模式,polling 的延时更低;并且由于不需要在用户态和内核态之间频繁切换,理论上也会更加节省CPU开销,从而提升系统最大的数据包处理能力。


  日子久了,DPDK的开发者们逐渐开始不满足L2/L3层的应用开发了,对DPDK支持TCP/IP协议栈的呼声自始至终就没有停息过。然而,可能是由于网络协议栈实在是太复杂,即使是在10年之后的今天,DPDK仍然没有推出官方的解决方案。开源社区有自己的一些尝试,如 mTCP、F-Stack、Seastar、dpdk-ans 等等,不一一列举。大体方案分为两类,一是移植现有的协议栈如FreeBSD,另一种就是完全重写。这些基于DPDK的开源项目的测试数据表明,它们通常能够获得比Linux内核的原生socket更好的性能。


  然而,跟普通的Linux网络应用开发相比,DPDK开发还是有一定的难度的。首先是硬件方面,通常需要物理机。由于最初 Intel 的网卡只能通过 igb_uio 或者 vfio 的方式将网卡 passthrough 透传给 DPDK,因此往往需要给物理机配置多个IP地址,并且开启 SR-IOV,一部分用来调试,另一部分用来保证管理网络正常运行。之后的 Mellanox 网卡,由于其DPDK驱动可以跟内核驱动共存,因此不用透传独占了。


  由于 Flow Birfurcation(流分叉)这种硬件辅助技术的出现,网卡现在还可以将关注的报文流量(比如某个TCP端口、某个IP地址)在还未进入系统内核之前,就通过分类过滤的方式提取出来,然后转发给DPDK应用程序,剩余的流量仍然原封不动地提交给内核。于是也不用给管理网络分配独立的IP了,简化了开发流程。这些具体的步骤会在后文提到。

flow-bifurcation.png

  在软件方面,众所周知,DPDK程序在每个CPU核心上运行的函数都是一个大的 while 循环。在循环中,可以添加网卡收发包的代码,以及对应的业务逻辑,整体上的架构是一个异步回调的事件模型。由于是 polling,需要让一次循环尽快结束以便开启下一轮,因此代码中一般不能出现长时间的阻塞调用,如sleep。业务逻辑一般只关注网络,比如网关、防火墙这种典型应用,涉及到文件I/O的功能,可能还需要通过跨线程通信的方式,转交给专门的I/O线程去执行。比如,DPDK跟qemu的通信方式是共享内存。


  习惯了Linux系统的应用开发者可能会对此不太满意,虽然很多Linux上的网络服务器也是异步回调的(基于epoll),但通过线程池、协程池这样的技术手段,开发者们可以很方便地通过开启新的执行单元(Procedure-Unit)实现并发逻辑。不管是线程还是协程,现有业务的旧代码中可能充斥了大量的锁、信号量、条件变量等。如果突然需要移植到DPDK的while大循环内,工作量将不容小觑。举例来说,如果原先业务代码在某个线程中sleep了1秒,那么在新的DPDK程序中,可能得在每次 poll 时都检查一下当前时间,发现超过了1秒则执行后续逻辑。那么,如果有更多的sleep,又该如何是好呢?


  这些问题,归根结底是编程模型的差异。一种是基于事件驱动的,另一种是基于多个执行单元并发的。有人可能会认为异步事件模型性能更高,这实际是一种误解。Lauer 和 Needham 在1979年发表的 On the duality of operating system structures 一文中曾经证明,events 和 threads 是对偶的,具体是指:

  • 用一种模型编写的程序可以直接映射到基于另一种模型的等效程序。
  • 这两种模型在逻辑上是等效的,尽管它们使用不同的概念并提供不同的语法。
  • 假定使用相同的调度策略,用两种模型编写的程序的性能基本相同。


  于是,我们尝试使用PhotonLibOS协程库(以下简称Photon),以多执行单元并发的代码模型,简化DPDK应用程序的开发,并且为它增加更多的功能,如文件I/O等。首先需要选择一个用户态的网络协议栈,经过调研,我们选择了腾讯开源的 F-Stack 项目,它集成了 FreeBSD 11.0 的网络协议栈并做了一些裁减,提供了一组POSIX API,如socketepollkqueue等。当然,由于本质上是FreeBSD,因此它的epoll也是用kqueue模拟的。


  下面这段代码摘自 F-Stack example ,它目前的核心代码仍然是 while 循环的模式,需要提供一个loop,然后注册到大循环内部执行,网卡的收发包代码被安排在了loop的上下文附近。虽然说已经提供了事件接口(这是协程化的必备条件),但由于没有调度器,所以整体代码风格跟之前的DPDK仍然差不多。

intloop(void*arg) {
intnevents=ff_epoll_wait(epfd,  events, MAX_EVENTS, 0);
inti;
for (i=0; i<nevents; ++i) {
if (events[i].data.fd==sockfd) {
while (1) {
intnclientfd=ff_accept(sockfd, NULL, NULL);
if (nclientfd<0) {
break;
                }
ev.data.fd=nclientfd;
ev.events=EPOLLIN;
if (ff_epoll_ctl(epfd, EPOLL_CTL_ADD, nclientfd, &ev) !=0) {
printf("ff_epoll_ctl failed:%d, %s\n", errno,
strerror(errno));
break;
                }
            }
        } else { 
if (events[i].events&EPOLLERR ) {
ff_epoll_ctl(epfd, EPOLL_CTL_DEL,  events[i].data.fd, NULL);
ff_close(events[i].data.fd);
            } elseif (events[i].events&EPOLLIN) {
charbuf[256];
size_treadlen=ff_read( events[i].data.fd, buf, sizeof(buf));
if(readlen>0) {
ff_write( events[i].data.fd, html, sizeof(html) -1);
                } else {
ff_epoll_ctl(epfd, EPOLL_CTL_DEL,  events[i].data.fd, NULL);
ff_close( events[i].data.fd);
                }
            } else {
printf("unknown event: %8.8X\n", events[i].events);
            }
        }
    }
}

  

我们尝试将Photon的调度器与F-Stack的while循环集成到一起,并且目标是在同一个线程内。Photon通常会使用epoll或者io_uring作为调度器的主事件引擎,可以注册关注的fd,不管是文件fd还是socket,当I/O事件完成时,对应的协程会被唤醒,并执行后续操作。


  那么我们应该用什么样的Photon函数给F-Stack做while循环呢?首先,我们做了一个集成在Photon内部的kqueue事件引擎,使用跟原先两个引擎相同的接口,只不过在poll F-Stack的kqueue时,timeout参数传了0。这点很好理解,即如果poll的时候发现网卡有I/O,那么立刻记录到协程引擎,以备后续唤醒;否则立刻退出,让下一次poll有机会执行。接着,我们编写F-Stack的循环函数,除了每轮必须的poll 0之外,还需要一个让协程主动让出CPU的函数(photon::thread_yield())。只有这样,在busy polling的间隙里,主事件引擎的io_uring才能有机会执行自己的poll。两种poll的执行概率在调度器里是可调整的。


  起初我们担心,在同一个线程内开启两个事件引擎去poll会降低性能,然而经过测试发现,以io_uring为例,一次io_uring_wait_cqe_timeout在timeout为0时,只有一个syscall和一次原子操作,时间开销在几微秒左右,相比于网卡延迟来说还是较低的。因此,只要主事件引擎不忙,对F-Stack循环的性能影响就非常小,详细测试数据见后文。


  让我们来看一下经过Photon集成的F-Stack + DPDK代码最终是什么样子。如下所示,首先需要在当前执行的OS线程(Photon称为vcpu)上初始化FSTACK_DPDK环境,作为一个I/O engine。当 photon::init 完成之后,程序会进入busy polling模式。接下来是最有特点的地方,我们可以在代码的任意位置开启新的协程(Photon称为thread),在协程里可以睡眠,可以加锁,也可以进行文件I/O。我们还可以使用 photon::net 模块封装的 socket server,注册一个 server handler,从而实现一个类似上文 F-Stack example 那样的 echo server。对每个新的连接,server都会新建一个协程去处理。由于不再是事件触发的回调机制,因此 socket server 代码中不再需要使用大量的 if ... else 判断状态,整体代码风格简约、直观。

photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_FSTACK_DPDK);
// DPDK will start running from here, as an I/O engine.// Although busy polling, the scheduler will ensure that all threads in current vcpu// have a chance to run, when their events arrived.autothread=photon::thread_create11([] {
while (true) {
photon::thread_sleep(1);
// Sleep every 1 second and do something ...    }
});
autohandler= [&](photon::net::ISocketStream*sock) ->int {
autofile=photon::fs::open_local("file", O_WRONLY, 0644, ioengine_iouring);
charbuf[buf_size];    
while (true) {
size_tret=sock->recv(buf, buf_size);
file->write(buf, ret);
sock->send(buf, ret);
    }
return0;
};
// Register handler and configure server ...autoserver=photon::net::new_fstack_dpdk_socket_server();
server->set_handler(handler);
server->run();
// Sleep forever ...photon::thread_sleep(-1UL);


  另外,这套代码的通用性也非常高。当我们不需要使用F-Stack + DPDK时,只要关闭 photon::INIT_IO_FSTACK_DPDK 这个参数,同时将 new_fstack_dpdk_socket_server 替换成 new_tcp_socket_server,就切换回了基于Linux内核socket的普通server。


  我们使用上述代码的echo server进行性能测试,结果如下:

Server socket type Client traffic type (Ping-Pong) Client traffic type (Streaming)
Linux 6.x kernel + Photon coroutine 1.03Gb/s 2.60Gb/s
F-Stack + DPDK + Photon coroutine 1.23Gb/s 3.74Gb/s


  注意,我们将client的流量类型分为Streaming和Ping-Pong,前者用于测量网络服务器在高吞吐条件下的性能,现实中类似的场景是 RPC 和 HTTP 2.0 等多路复用技术;后者一般是为了测试多连接、大并发的场景,客户端的每个连接发送一个包之后,等待接收,然后才会开始下一个包的发送。关于测试环境,网卡是25G,服务端单线程,包大小是512字节。


  不难看出,建立在用户态协议栈和DPDK之上的Photon socket,比原生内核的Photon socket性能更优。Ping-Pong模式提高了20%,而Streaming模式则提高了40%。如果逐步提高主事件引擎(io_uring)poll的比例,比如添加更多的sleep、或者文件I/O操作,使得其跟DPDK poll的比例达到1:1,此时server的性能大约会下降5%左右,可以接受。我们还对比测试了Photon版F-Stack跟原版F-Stack的性能,也基本一致,表明协程的引入没有带来额外开销。这是因为每次Photon协程切换的时间在10纳秒以下。


  熟悉Photon的读者可能知道,自Photon从2022年开源以来,已经横向对比了众多开源项目,也在主页上列出了各个框架实现的echo server的性能测试数据,Photon凭借高效率的调度器、关键路径的高性能汇编代码、以及io_uring异步引擎的加持,目前仍然位列第一。此次集成了DPDK引擎,相当于是引入了跨界的竞争对手(interrupt vs polling),同时也是实现了一次自我超越。


  讨论完性能,让我们回到本文写作时的初衷,即如何使用协程库降低DPDK应用程序的开发难度,屏蔽底层差异,我们相信Photon在这个方面的尝试是更值得被关注的。通过使用Photon库,开发者们可以轻松地将旧代码移植到DPDK框架之上,从而把更多时间关注在业务本身。


  最后,为了让读者能够亲自验证上述测试程序,我们准备了一篇文档,详细描述了如何在Mellanox网卡上安装驱动、配置SR-IOV、安装F-STACK和DPDK、配置Flow Birfurcation、运行Photon等步骤,欢迎大家查阅。

相关文章
|
5月前
|
Java 开发者
Java一分钟之-Quasar:协程库
【6月更文挑战第12天】Quasar是Java的高性能协程库,通过字节码增强实现轻量级并发模型——协程和通道,降低并发处理的复杂性和资源消耗。本文探讨了Quasar的常见问题,如内存泄漏、死锁和过度使用,提出相应避免策略,并提供了一个简单的协程间数据交换的代码示例。正确使用Quasar能提升程序性能和可维护性。
269 1
|
6月前
|
Java 数据库 Android开发
【专栏】Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
162 5
|
2月前
|
调度 Android开发 开发者
探索安卓开发中的新技术:Kotlin协程
【9月更文挑战第9天】本文将深入探讨Kotlin协程在安卓开发中的应用,揭示其如何优雅地处理异步任务。我们将从基础概念入手,逐步深入到实际开发场景,最后通过代码示例直观展示协程的魔力。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往更高效、更简洁代码的大门。
|
4月前
|
开发者 Kotlin Android开发
Kotlin协程在Android开发中的应用
【7月更文挑战第10天】Kotlin协程简化了Android异步编程,提供轻量级并发。挂起函数让异步代码看起来同步,不阻塞线程,便于管理。在项目中,添加Kotlin和协程依赖,如`kotlinx.coroutines-core`和`kotlinx-coroutines-android`。使用`CoroutineScope`和`launch`处理耗时任务,如网络请求,避免主线程阻塞。挂起函数和调度器控制执行上下文,适应不同任务需求。
|
4月前
|
存储 调度 Python
异步编程概述在 Python中,`asyncio`库提供了对异步I/O、事件循环、协程(coroutine)和任务的支持。
异步编程概述在 Python中,`asyncio`库提供了对异步I/O、事件循环、协程(coroutine)和任务的支持。
|
6月前
|
存储 Python
python使用gevent库来创建协程,并通过协程实现并发执行不同的任务
```markdown 这段Python代码利用`gevent`库实现并发执行协程。定义了两个打印函数`f1`和`f2`,分别输出&quot;csdn&quot;和&quot;yyds&quot;。代码首先创建列表`t_l`,并启动5个`f1`协程,将其加入列表并等待所有协程完成。随后,同样方式启动5个`f2`协程,存入`t1_l`列表并等待执行完毕。整体展示了`gevent`的协程并发操作。 ```
56 1
|
6月前
|
程序员 调度 数据库
Python中的异步编程:asyncio库和协程的深入解析
Python中的异步编程:asyncio库和协程的深入解析
177 0
|
JavaScript Linux 编译器
c++开源协程库libgo介绍及使用
c++开源协程库libgo介绍及使用
|
6月前
|
程序员 Linux 调度
用ucontext库创建简单的协程
用ucontext库创建简单的协程
122 0
|
API Python
Gevent----非官方的python协程库
Gevent----非官方的python协程库
100 0