关于用户态协议栈的思考

一直以来我一直以为操作系统内核是高大上的东西,但是实际上用户态的应用才是!
上周的一次技术交流中,一家网络加速卡厂商声称他们的协议栈是用户态的协议栈,用来提高性能,我对其产品直接就泄了气,然而会后,我查阅了相关的资料,找到一篇文章《 千万并发的秘密-内核是问题的根本》,写出了我的心声,原来我一直都是这么认为的,只是一直都不敢承认罢了,为何不敢承认,那是因为我酷爱内核。我对那家厂商泄气表达了我深深的虚伪,内在的分裂!
文中说”我们学的是Unix而不是网络编程“,对《 Unix网络编程》一书给出了正确的评价,告诉我们,我们被平台牵累了,我们一直在面对的都是操作系统的接口如何使用的技术,而不是真正的编程,真正的天马行空般超乎想象的编程。一个又一个的应用编程框架或者中间件在提出的时候,说的是”如何能让我们不必关注实现细节而精心处理我们自己的业务逻辑“,这篇文章能让人理解这句话的好意。
历史地看网络编程,是先有了UNIX,再有了TCP/IP和BSD socket,当然要把网络编程往UNIX里面硬塞,曾几何时,一直到现在,做网络编程的不懂UNIX会被人耻笑,当然,Linux某种意义上已经代替了UNIX。UNIX的哲学包括机制和策略分离这一真理,它在映射成这个信条之前是数据和控制的分离这个箴言,这个箴言如今已经淹没在网络设备中,比如路由器,其数据面和控制面是合在一起的,当该箴言再次唤醒其信徒的时候,SDN就出现了。是对UNIX理念的误解,导致了网络设备将控制面和数据面合在在一起而并非利益使然,这种误解甚至影响了UNIX本身。要知道UNIX理念影响了几乎所有的操作系统以及网络设备的设计,其中包括Mirosoft Windows以及Cisco的IOS。我之所以说是一种误解而并非背叛,是因为UNIX的理念可能从来就没有被真正理解过。也许,这是对宏内核的误解导致的对UNIX的误解。
UNIX的宏内核思想影响面甚大,然而它的本意并非将所有的操作都塞进内核,而是仅仅将机要操作塞进内核,保持内核的紧凑性与高效性,因为模块之间的交流是需要成本的,宏内核思想不讲解耦合(但在实现机制上,还是模块化的)。可是如何定义什么是机要操作,考虑以下的服务:
制定一套合理的经济政策;
制定一套合理的税收计划;
生产棉布;
制作一件面料考究的羊毛西服;
提供价值¥160的发型;
...
请问哪些是机要操作?对于普通民众而言,比如我,根本就没有西服,头发一年理一次,但是对于贵族而言,除了最后一条,其它的可能都是机要操作...很难定义机要操作,所以这种定义法很容易将所有的东西都塞进UNIX。《UNIX网络编程》讲述的是,你仅需要写一个很小的轻量级的服务器就可以让UNIX做一切繁复的工作。一件颇具说服力的事可以帮助《 千万并发的秘密-内核是问题的根本》的作者表达一下深深的恶意,那就是Linux曾经在内核中实现了一个WEB服务器,多么巨大的一个玩笑,或者说是对宏内核多么巨大的一个讽刺...我们要记住的是,UNIX并没有让后来者把所有的东西塞进内核,只是说,内核要保留控制权。
我们不妨换一个思路,回到UNIX最初的思路,从控制权角度来看,哪些是属于控制面的,也许你能说出一大堆,进程调度,资源管理,文件系统...网络协议栈。不过,好像我们所有人一直以来都把网络视为例外,网络IO即不是块设备IO也不是字符设备IO,按照UNIX一切皆文件的观念,我们没法给网络一个合理的位置。socket接口的是一个完整的协议栈,而不是什么设备,我们不得不面对网络参数的调整,为此我们加了多少次班,这正是网络IO和其它设备IO相比所处的尴尬位置。
直接和网卡接口是不是更自由些,从此我们摆脱了协议栈的束缚,然而我们必须自己实现协议栈,前些日子我就想过这些,主要是为了解决手机上面操作网络无权限的问题,我当时想得是一个关于自由的问题而不是性能问题,而《 秘密》一文说的正是性能问题,回顾那个厂商的介绍,他们的产品在用户态进行数据包的协议栈处理,最大限度的使用了网络协处理器等硬件加速功能,让人感叹,请问,使用操作系统内核的协议栈,如何把处理转到协处理器上?!回答使用Netfilter已经过时了!我一直都想玩玩用Netfilter将处理转到一块卡上,但是我发现我过时了,如今的回答是,直接把内核协议栈旁路掉!如今,真的有这样的技术,其中之一叫做PF_RING,实际上就是一个抓包机制,将数据包直接从链路层获取,然后你想怎么处理就怎么处理,刚刚试了一下,挺好用,和uIP结合,简直太猛了。当然,我可没有千万级并发的测试环境,我说的猛仅仅是它竟然真的可以工作!
说说性能问题。性能和内核无关,你不要指望从32位的Linux 2.6.8换到64位的Linux 2.6.32内核在性能上会有一个突破,也不要指望拼出Linux和Windows内核对性能提升效果的优劣,关键还是在于应用程序!WHY?因为性能是一个高端私人定制服务,内核这种基础设施是不负责这种高定服务的。想出高性能,实际上一种艺术行为,涉及到方方面面的微调,绝对是高端大气上档次的行为,你指望内核能帮你做到这些吗?在微内核的世界,这是可能的,但是千万别把你的偏好转向微内核,干嘛非要内核搞定一切呢?干嘛不自己搞定啊!就算自己搞不定,把思想或者想法放出去,总会有人搞定的啊!不管是宏内核还是微内核,都不宜把高定的东西往里面塞,否则,对于宏内核而言,它就变成了屎壳郎滚的球,对于微内核而言,它就变成了蜘蛛织的网...
DeepSeek 3FS解读与源码分析专栏收录该内容5 篇文章订阅专栏客户端模式3FS 实现了两套客户端模式,FUSE Client和Native Client。前者更方便适配,性能相对较差。后者适合集成性能敏感的应用程序,适配成本较高。接下来做进一步分析。Fuse ClientFuse Client 模式的原理如下图所示。和传统 FUSE 应用类似,在 libfuse 中注册了 FUSE Daemon 实现的 fuse_lowlevel_ops,之后通过 FUSE 的所有的文件操作,都会通过 libfuse 回调到 FUSE Daemon 进行处理。同时在 libfuse 中实现了一个多线程模式来高效读取请求。这种模式对于业务逻辑影响较小,可以做到无感知。但是每次 I/O 单向需要经过两次“用户态-内核态”上下文切换,以及“用户空间-内核空间”之间数据拷贝。Native Client (USRBIO)介绍这个模式之前我们先了解一下 User Space Ring Based IO(简称 USRBIO)[1],它是一组构建在 3FS 上的高速 I/O 函数。用户应用程序能够通过 USRBIO API 直接提交 I/O 请求给 FUSE Daemon 进程中的 3FS I/O queue 来和 FUSE 进程通信,从而做到 kernel bypass。两者之间通过基于共享内存 ior/iov 的机制交换数据,这部分在后面章节介绍。Native Client 模式的原理如下图所示。使用这种模式能有效避免“用户态-内核态”上下文切换,同时做到零数据拷贝,全链路基本无锁化设计,性能上要比 Fuse Client 模式提升很多。(根据我们对 3FS 开源的 fio ioengine hf3fs_usrbio 压测结果看,在不进行参数调优的情况下,USRBIO 比 **Fuse Client **模式顺序写性能提升 20%-40%,其他场景性能还在进一步验证中)3FS 使用 Pybind 定义 Python 扩展模块 hf3fs_py_usrbio,这也方便 Python 能够访问 hf3fs 的功能。以此推测 USRBIO 模式适合在大模型训练和推理等对性能有极致需求的场景中使用。另外,从上面分析我们注意到 Fuse Daemon 在两种客户端模式下都起到核心作用的重要组件。在 Fuse Client 模式中,它通过 fuseMainLoop 创建 FuseClients,注册 fuse 相关 op 的 hook,并根据配置拉起单线程(或多线程) fuse session loop 处理 fuse op。在 USRBIO 模式中,与 I/O 读写链路相关的 USRBIO API 通过共享内存和 Fuse Daemon 通信,部分与 I/O 无关的控制路径请求例如 hardlink,punchhole 等,USRBIO API 则还是通过 ioctl 直接走了内核 FUSE 路径。这可能是一个 tradeoff 的设计,后面会做讨论。基础组件ServerLauncherFuse Daemon 也就是 FuseApplication, 通过 core::ServerLauncher 拉起。同样的还有 MgmtdServer,MetaServer,StorageServer 都是类似的 Daemon。Fuse Daemon 拉起之后就创建一个 FuseClients 进行核心功能操作。FuseClients一个 FuseApplication 包含 一个 FuseClients,一个 FuseClients 和一个挂载点对应。FuseClients 主要包括下图所示组件,其中包含与其他组件(meta,mgmtd,storage)打交道的 "client for client"。FuseClients 在启动时也会初始化 mgmtdClient,创建 StorageClient,metaClient,启动周期性 Sync Runner(用来更新文件长度等元数据),创建 notifyInvalExec 线程池等。同时还为每一个 FuseClients 创建一组 IOV 和 IOR。FuseClients 最重要的部分还是在和 USRBIO 协同设计。下面我们着重分析这部分。USRBIO 的设计和思考3FS USRBIO 设计思想借鉴了 io_uring 以及 SPDK NVMe 协议栈的设计。原生 io_uring [2] 由一组 Submission Queue 和 Completion Queue 组成,每个 queue 是一个 ring buffer。用户进程提交请求到 SQ,内核选择 polling 模式或事件驱动模式处理 SQ 中的请求,完成之后内核向 CQ 队尾 put 完成 entry,应用程序根据 polling 模式或者事件驱动模式处理 CQ 队首的请求。整个过程无锁,共享内存无内存拷贝。在 polling 模式下,io_uring 接近纯用户态 SPDK polling mode 性能,但是 io_uring 需要通过额外的 CPU cost 达到这个效果。3FS USRBIO 的核心设计围绕 ior 和 iov 来开展。 ior 是一个用于用户进程与 FUSE 进程之间通信的小型共享内存环。用户进程将读/写请求入队,而 FUSE 进程从队列中取出这些请求并完成执行。ior 记录的是读写操作的元数据,并不包含用户数据,其对应的用户数据 buffer 指向一个叫做 iov 的共享内存文件。这个 iov 映射到用户进程和 FUSE 进程共享的一段内存,其中 InfiniBand 内存注册由 FUSE 进程管理。在 USRBIO 中,所有的读取数据都会被读取到 iov,而所有的写入数据应由用户先写入 iov。ior 用来管理 op 操作任务,和 io_uring 不同的是这个 queue 中既包括提交 I/O 请求(sqe)又接收完成 I/O 结果(cqe),而且不通过 kernel,纯用户态操作。ior 中包含的 sqeSection 和 cqeSection 的地址范围由创建 ring 的时候计算出来的 entries 个数确定,用来查询 sqe 和 cqe 在 ring 中的 位置。ior 中还包含一个 ringSection,这个 section 用来帮助 sqe 定位 iov id 的索引和位置。如下图所示,sqe 里包含 idx 是 IOArgs* ringSection 这个数组的下标,索引后才是真正的 io 参数。例如:seq -> ringSection[idx] -> IovId -> Iov。USRBIO 中提供了一个 API hf3fs_iorwrap 用来创建和管理 ior,其中 Hf3fsIorHandle 用来管理 ior。之后 hf3fs_iorwrap 会通过 cqeSem 解析 submit-ios 信号量的路径,并通过 sem_open 打开关联信号量,用于 I/O 任务同步。这里的信号量根据优先级被放置在不同目录中。之后在提交 IO 过程中,会 post 信号量通知 cqe section 中 available 的 slots。在 ior 中,通过 IoRingJob 分配工作,任务被拆分成 IoRingJob,每个任务会处理一定数量的 I/O 请求做批处理。和 io_uring 一样,采用 shared memory 减少用户态与内核态切换。1. IoRing 初始化资源2. 提交 I/O 请求 addSqe3. 获取待处理的 I/O 任务 IoRing::jobsToProc4. 处理 I/O 任务 IoRing::process,如上图所示。IoRing::process() -->ioExec.addWrite() --> ioExec.executeWrite()--> ioExec.finishIo()IoRing 中的 ioExec 就是 PioV。PioV::executeWrite() 执行写操作中根据是否需要 truncate chunk,选择将 truncate WriteIO 包到一个 std::vectorstorage::client::WriteIO wios2中,或者直接传输std::vectorstorage::client::WriteIO wios_,最后通过 StorageClient::batchWrite() 将 Write IO 通过发送 RPC 写请求到 Storage 端。其中,写请求 WriteReq 包括 payload,tag,retryCount,userInfo,featureFlags 等字段。FuseClients 中最核心的逻辑之一在 ioRingWorker 中。它负责从 FuseClients 的 ior job queue 中拿到一个 ior,并调用 process 处理它。在处理过程中考虑了取消任务的设计,这里使用了一个 co_withCancellation 来封装,它能够在异步操作中优雅地处理任务取消,避免不必要的计算或资源占用,并且支持嵌套任务的取消感知。有关 co_cancellation 的原理可以参考 [3]:另外,还支持可配置的对任务 job 分优先级,优先级高的 job 优先处理。这些优化都能在复杂的场景下让性能得到极致提升。值得提到的一点是,所有的 iovs 共享内存文件在挂载点 3fs-virt/iovs/ 目录下均建有 symlink,指向 /dev/shm 下的对应文件。USRBIO 代码逻辑错综复杂,偏差之处在所难免,在这里抛砖引玉一些阅读代码的思路和头绪,如有错误也请不吝批评指正。关于USRBIO的思考USRBIO 在共享内存设计上使用了映射到物理内存的一个文件上,而不是使用匿名映射到物理内存。这可能是因为用户进程和 FUSE Daemon 进程不是父子进程关系。实现非派生关系进程间的内存共享,只能使用基于文件的映射或 POSIX 共享内存对象。USRBIO 没有采用直接以 SDK 形式,放弃 Fuse Daemon,直接和元数据服务器与 Chunk Server 来通信的方式设计客户端,而采用了关键 I/O 路径使用纯用户态共享内存,非关键路径上依旧复用 libfuse 这种方式。这可能是简化控制链路设计,追求 FUSE 上的复用性,追求关键路径性能考虑。另外在 IoRing 的设计上并没有使用类似 io_uring 中的可配置的 polling 模式,而是采用信号量进行同步,这里暂时还没有理解背后的原因是什么。USRBIO 使用共享内存还是不可避免会带来一些开销和性能损耗,如此设计的本质原因还是所有核心逻辑都做在了 FUSE Daemon 进程中。如果提供重客户端 SDK,所有逻辑都实现在 SDK 中,以动态连接库形式发布给客户端,可能就不需要进行这样的 IoRing 设计,或者只需要保留 io_uring 这样的无锁设计,不再需要共享内存设计。这样的好处和坏处都很鲜明:好处是 SDK 的实现能避免跨进程的通信开销,性能能达到理想的极限;坏处是如果需要保留 FUSE 功能的话需要实现两套代码,逻辑还很雷同,带来较大的开发和维护成本。而且 SDK 的升级比较重,对客户端造成的影响相对较大。当然从工程角度上可以由 FUSE 抽象出公共函数库让 Native Client 直接调用也可以避免重复开发。
最新发布
04-08
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值