【RPC 专栏】深入理解 RPC 之传输篇

本文深入探讨RPC通信机制,包括使用Socket和Netty实现的阻塞与非阻塞通信,同步与异步的区别,并通过示例代码展示具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

点击上方“芋道源码”,选择“置顶公众号”

技术文章第一时间送达!

源码精品专栏

 


  • RpcRequest 和 RpcResponse

  • Socket传输

  • Netty 传输

  • 同步与异步 阻塞与非阻塞

  • 总结


RPC 被称为“远程过程调用”,表明了一个方法调用会跨越网络,跨越进程,所以传输层是不可或缺的。一说到网络传输,一堆名词就蹦了出来:TCP、UDP、HTTP,同步 or 异步,阻塞 or 非阻塞,长连接 or 短连接…

本文介绍两种传输层的实现:使用 Socket 和使用 Netty。前者实现的是阻塞式的通信,是一个较为简单的传输层实现方式,借此可以了解传输层的工作原理及工作内容;后者是非阻塞式的,在一般的 RPC 场景下,性能会表现的很好,所以被很多开源 RPC 框架作为传输层的实现方式。

RpcRequest 和 RpcResponse

传输层传输的主要对象其实就是这两个类,它们封装了请求 id,方法名,方法参数,返回值,异常等 RPC 调用中需要的一系列信息。

public class RpcRequest implements Serializable {
   private String interfaceName;
   private String methodName;
   private String parametersDesc;
   private Object[] arguments;
   private Map<String, String> attachments;
   private int retries = 0;
   private long requestId;
   private byte rpcProtocolVersion;
}
public class RpcResponse implements Serializable {
   private Object value;
   private Exception exception;
   private long requestId;
   private long processTime;
   private int timeout;
   private Map<String, String> attachments;// rpc协议版本兼容时可以回传一些额外的信息
   private byte rpcProtocolVersion;
}

Socket传输

Server

public class RpcServerSocketProvider {
   public static void main(String[] args) throws Exception {
       //序列化层实现参考之前的章节
       Serialization serialization = new Hessian2Serialization();
       ServerSocket serverSocket = new ServerSocket(8088);
       ExecutorService executorService = Executors.newFixedThreadPool(10);
       while (true) {
           final Socket socket = serverSocket.accept();
           executorService.execute(() -> {
               try {
                   InputStream is = socket.getInputStream();
                   OutputStream os = socket.getOutputStream();
                   try {
                       DataInputStream dis = new DataInputStream(is);
                       int length = dis.readInt();
                       byte[] requestBody = new byte[length];
                       dis.read(requestBody);
                       //反序列化requestBody => RpcRequest
                       RpcRequest rpcRequest = serialization.deserialize(requestBody, RpcRequest.class);
                       //反射调用生成响应 并组装成 rpcResponse
                       RpcResponse rpcResponse = invoke(rpcRequest);
                       //序列化rpcResponse => responseBody
                       byte[] responseBody = serialization.serialize(rpcResponse);
                       DataOutputStream dos = new DataOutputStream(os);
                       dos.writeInt(responseBody.length);
                       dos.write(responseBody);
                       dos.flush();
                   } catch (Exception e) {
                       e.printStackTrace();
                   } finally {
                       is.close();
                       os.close();
                   }
               } catch (Exception e) {
                   e.printStackTrace();
               } finally {
                   try {
                       socket.close();
                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }
           });
       }
   }
   public static RpcResponse invoke(RpcRequest rpcRequest) {
       //模拟反射调用
       RpcResponse rpcResponse = new RpcResponse();
       rpcResponse.setRequestId(rpcRequest.getRequestId());
       //... some operation
       return rpcResponse;
   }
}

Client

public class RpcSocketConsumer {
   public static void main(String[] args) throws Exception {
       //序列化层实现参考之前的章节
       Serialization serialization = new Hessian2Serialization();
       Socket socket = new Socket("localhost", 8088);
       InputStream is = socket.getInputStream();
       OutputStream os = socket.getOutputStream();
       //封装rpc请求
       RpcRequest rpcRequest = new RpcRequest();
       rpcRequest.setRequestId(12345L);
       //序列化 rpcRequest => requestBody
       byte[] requestBody = serialization.serialize(rpcRequest);
       DataOutputStream dos = new DataOutputStream(os);
       dos.writeInt(requestBody.length);
       dos.write(requestBody);
       dos.flush();
       DataInputStream dis = new DataInputStream(is);
       int length = dis.readInt();
       byte[] responseBody = new byte[length];
       dis.read(responseBody);
       //反序列化 responseBody => rpcResponse
       RpcResponse rpcResponse = serialization.deserialize(responseBody, RpcResponse.class);
       is.close();
       os.close();
       socket.close();
       System.out.println(rpcResponse.getRequestId());
   }
}

dis.readInt() 和 dis.read(byte[] bytes) 决定了使用 Socket 通信是一种阻塞式的操作,报文头+报文体的传输格式是一种常见的格式,除此之外,使用特殊的字符如空行也可以划分出报文结构。在示例中,我们使用一个 int(4字节)来传递报问题的长度,之后传递报文体,在复杂的通信协议中,报文头除了存储报文体还会额外存储一些信息,包括协议名称,版本,心跳标识等。

在网络传输中,只有字节能够被识别,所以我们在开头引入了 Serialization 接口,负责完成 RpcRequest 和 RpcResponse 与字节的相互转换。(Serialization 的工作机制可以参考之前的文章)

使用 Socket 通信可以发现:每次 Server 处理 Client 请求都会从线程池中取出一个线程来处理请求,这样的开销对于一般的 Rpc 调用是不能够接受的,而 Netty 一类的网络框架便派上了用场。

Netty 传输

Server 和 ServerHandler

public class RpcNettyProvider {
   public static void main(String[] args) throws Exception{
       EventLoopGroup bossGroup = new NioEventLoopGroup();
       EventLoopGroup workerGroup = new NioEventLoopGroup();
       try {
           // 创建并初始化 Netty 服务端 Bootstrap 对象
           ServerBootstrap bootstrap = new ServerBootstrap();
           bootstrap.group(bossGroup, workerGroup);
           bootstrap.channel(NioServerSocketChannel.class);
           bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
               @Override
               public void initChannel(SocketChannel channel) throws Exception {
                   ChannelPipeline pipeline = channel.pipeline();
                   pipeline.addLast(new RpcDecoder(RpcRequest.class)); // 解码 RPC 请求
                   pipeline.addLast(new RpcEncoder(RpcResponse.class)); // 编码 RPC 响应
                   pipeline.addLast(new RpcServerHandler()); // 处理 RPC 请求
               }
           });
           bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
           bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
           ChannelFuture future = bootstrap.bind("127.0.0.1", 8087).sync();
           // 关闭 RPC 服务器
           future.channel().closeFuture().sync();
       } finally {
           workerGroup.shutdownGracefully();
           bossGroup.shutdownGracefully();
       }
   }
}
public class RpcServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
   @Override
   public void channelRead0(final ChannelHandlerContext ctx, RpcRequest request) throws Exception {
       RpcResponse rpcResponse = invoke(request);
       // 写入 RPC 响应对象并自动关闭连接
       ctx.writeAndFlush(rpcResponse).addListener(ChannelFutureListener.CLOSE);
   }
   private RpcResponse invoke(RpcRequest rpcRequest) {
       //模拟反射调用
       RpcResponse rpcResponse = new RpcResponse();
       rpcResponse.setRequestId(rpcRequest.getRequestId());
       //... some operation
       return rpcResponse;
   }
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
       cause.printStackTrace();
       ctx.close();
   }
}

Client 和 ClientHandler

public class RpcNettyConsumer {
   public static void main(String[] args) throws Exception{
       EventLoopGroup group = new NioEventLoopGroup();
       try {
           // 创建并初始化 Netty 客户端 Bootstrap 对象
           Bootstrap bootstrap = new Bootstrap();
           bootstrap.group(group);
           bootstrap.channel(NioSocketChannel.class);
           bootstrap.handler(new ChannelInitializer<SocketChannel>() {
               @Override
               public void initChannel(SocketChannel channel) throws Exception
{
                   ChannelPipeline pipeline = channel.pipeline();
                   pipeline.addLast(new RpcEncoder(RpcRequest.class)); // 编码 RPC 请求
                   pipeline.addLast(new RpcDecoder(RpcResponse.class)); // 解码 RPC 响应
                   pipeline.addLast(new RpcClientHandler()); // 处理 RPC 响应
               }
           });
           bootstrap.option(ChannelOption.TCP_NODELAY, true);
           // 连接 RPC 服务器
           ChannelFuture future = bootstrap.connect("127.0.0.1", 8087).sync();
           // 写入 RPC 请求数据并关闭连接
           Channel channel = future.channel();
           RpcRequest rpcRequest = new RpcRequest();
           rpcRequest.setRequestId(123456L);
           channel.writeAndFlush(rpcRequest).sync();
           channel.closeFuture().sync();
       } finally {
           group.shutdownGracefully();
       }
   }
}
public class RpcClientHandler extends SimpleChannelInboundHandler<RpcResponse> {
   @Override
   public void channelRead0(ChannelHandlerContext ctx, RpcResponse response) throws Exception {
       System.out.println(response.getRequestId());//处理响应
   }
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
       cause.printStackTrace();
       ctx.close();
   }
}

使用 Netty 的好处是很方便地实现了非阻塞式的调用,关键部分都给出了注释。上述的代码虽然很多,并且和我们熟悉的 Socket 通信代码大相径庭,但大多数都是 Netty 的模板代码,启动服务器,配置编解码器等。真正的 RPC 封装操作大多集中在 Handler 的 channelRead 方法(负责读取)以及 channel.writeAndFlush 方法(负责写入)中。

public class RpcEncoder extends MessageToByteEncoder {
   private Class<?> genericClass;
   Serialization serialization = new Hessian2Serialization();
   public RpcEncoder(Class<?> genericClass) {
       this.genericClass = genericClass;
   }
   @Override
   public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) throws Exception
{
       if (genericClass.isInstance(in)) {
           byte[] data = serialization.serialize(in);
           out.writeInt(data.length);
           out.writeBytes(data);
       }
   }
}
public class RpcDecoder extends ByteToMessageDecoder {
   private Class<?> genericClass;
   public RpcDecoder(Class<?> genericClass) {
       this.genericClass = genericClass;
   }
   Serialization serialization = new Hessian2Serialization();
   @Override
   public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception
{
       if (in.readableBytes() < 4) {
           return;
       }
       in.markReaderIndex();
       int dataLength = in.readInt();
       if (in.readableBytes() < dataLength) {
           in.resetReaderIndex();
           return;
       }
       byte[] data = new byte[dataLength];
       in.readBytes(data);
       out.add(serialization.deserialize(data, genericClass));
   }
}

使用 Netty 不能保证返回的字节大小,所以需要加上 in.readableBytes() < 4 这样的判断,以及 in.markReaderIndex() 这样的标记,用来区分报文头和报文体。

同步与异步 阻塞与非阻塞

这两组传输特性经常被拿来做对比,很多文章声称 Socket 是同步阻塞的,Netty 是异步非阻塞,其实有点问题。

其实这两组并没有必然的联系,同步阻塞,同步非阻塞,异步非阻塞都有可能(同步非阻塞倒是没见过),而大多数使用 Netty 实现的 RPC 调用其实应当是同步非阻塞的(当然一般 RPC 也支持异步非阻塞)。

同步和异步关注的是消息通信机制
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

如果需要 RPC 调用返回一个结果,该结果立刻被使用,那意味着着大概率需要是一个同步调用。如果不关心其返回值,则可以将其做成异步接口,以提升效率。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

在上述的例子中可以看出 Socket 通信我们显示声明了一个包含10个线程的线程池,每次请求到来,分配一个线程,等待客户端传递报文头和报文体的行为都会阻塞该线程,可以见得其整体是阻塞的。而在 Netty 通信的例子中,每次请求并没有分配一个线程,而是通过 Handler 的方式处理请求(联想 NIO 中 Selector),是非阻塞的。

使用同步非阻塞方式的通信机制并不一定同步阻塞式的通信强,所谓没有最好,只有更合适,而一般的同步非阻塞 通信适用于 1.网络连接数量多 2.每个连接的io不频繁 的场景,与 RPC 调用较为契合。而成熟的 RPC 框架的传输层和协议层通常也会提供多种选择,以应对不同的场景。

总结

本文堆砌了一些代码,而难点主要是对 Socket 的理解,和 Netty 框架的掌握。Netty 的学习有一定的门槛,但实际需要掌握的知识点其实并不多(仅仅针对 RPC 框架所涉及的知识点而言),学习 Netty ,个人推荐《Netty IN ACTION》以及 https://waylau.gitbooks.io/netty-4-user-guide/Getting%20Started/Before%20Getting%20Started.html 该网站的例子。

参考资料:

http://javatar.iteye.com/blog/1123915 – 梁飞

https://gitee.com/huangyong/rpc – 黄勇

666. 彩蛋

如果你对 RPC 并发感兴趣,欢迎加入我的知识一起交流。

知识星球


目前在知识星球(https://t.zsxq.com/2VbiaEu)更新了如下 Dubbo 源码解析如下:

01. 调试环境搭建
02. 项目结构一览
03. API 配置(一)之应用
04. API 配置(二)之服务提供者
05. API 配置(三)之服务消费者
06. 属性配置
07. XML 配置
08. 核心流程一览
...
一共 60 篇++

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
### DeepSeek 3FS 架构设计与源码分析 #### 1. 概述 DeepSeek 3FS 是一种专门为大规模分布式存储系统设计的文件系统,其核心设计理念围绕着高性能读带宽展开。为了实现这一目标,3FS 在多个方面进行了权衡,例如牺牲随机写性能、元数据操作效率以及部分 POSIX 兼容性来换取更高的吞吐量和更低的延迟[^1]。 #### 2. 架构设计特点 3FS 的架构主要分为以下几个模块: - **FUSE Daemon**: 提供了一个用户空间接口,允许开发者通过标准文件系统 API 访问底层对象存储服务。 - **I/O Queue**: 用户应用可以直接向 FUSE Daemon 中的 I/O 队列提交请求,减少传统文件系统的开销。 - **Shared Memory Mechanism**: 利用共享内存技术在客户端和服务端之间传递数据,避免了不必要的上下文切换和数据复制。 #### 3. USRBIO 性能优化原理 User Space Ring Based IO (USRBIO) 是建立在 3FS 基础上的高效 I/O 子系统。它的主要优势在于绕过了内核层处理逻辑,实现了真正的 zero-copy 和 lock-free 设计。具体来说: - **Kernel Bypass**: 应用程序无需经过操作系统内核即可完成 I/O 操作,显著降低了 CPU 使用率并提高了响应速度[^2]。 - **Zero Copy**: 数据传输过程中不需要额外缓冲区参与,减少了内存占用和访问次数。 - **Lock-Free Design**: 整条路径几乎没有任何同步原语介入,极大提升了并发能力。 以下是 USRBIO 实现的一个简化伪代码示例: ```c struct usrbio_request { char *buffer; size_t length; }; void submit_io(struct usrbio_queue *queue, struct usrbio_request req) { spin_lock(&queue->lock); list_add_tail(&req.list_entry, &queue->pending_requests); spin_unlock(&queue->lock); wake_up(queue->worker_thread); // Notify worker thread to process the request. } ``` #### 4. Fuse Client vs Native Client (USRBIO) | 特性 | Fuse Client | Native Client (USRBIO) | |---------------------|--------------------------------------|---------------------------------------| | 上下文切换 | 多次进入退出内核 | 完全规避 | | 数据拷贝 | 至少两次 | 不发生 | | 并发控制 | 锁较多 | 几乎无锁 | | 性能表现(顺序写) | 较低 | 提升约20%-40% | 上述表格总结了两种模式的关键差异点。可以看出,在追求极致性能的应用场景中,采用 Native Client 方案无疑更具竞争力。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值