网络模块

消息队列的需求:满足高吞吐、高可靠、低延时,并且还要支持多语言访问的基础软件,所以网络模块最需要解决性能、稳定性、开发成本这三个问题

对于单个请求而言,请求流程是:客户端(生产者/消费者)构建请求后,向服务器发送请求包,服务器接收到这个包后交给业务线程处理,业务线程处理完成后将结果返回给客户端。

上述请求过程存在三个消耗性能的地方:编解码速度、网络延迟、服务端或客户端网络模块的处理速度

  • 编解码的速度:序列化和反序列化带来的性能开销

  • 网络延迟:网络延迟取决于网络链路的性能,前面也讲过我们的核心链路不会使用HTTP,因为太耗时,而且不能满足需求,与网络模块无关,在软件层面无法优化

  • 网络模块:发送或接收请求包后,包能否被及时处理,比如逻辑线程处理完成后,网络模块能否即使回包,这一点数据性能优化

对于并发请求来说,在解决单个请求维度问题的基础上,还要处理高并发、高qps、高流量等场景带来的性能问题,主要包含三个方面:

  • 高效的连接管理:当客户端和服务端之间有大量TCP连接时,如何高效处理、管理连接

  • 快速处理高并发请求:当客户端和服务端之间的QPS很高时,如何快速处理(接收、 返回)请求

  • 大流量场景:当客户端和服务端之间的流量很高时,如何快速吞吐(读写)数据

大流量场景在某种意义上是高并发处理的一种子场景因为大流量分为单个请求包大并发小或单个请求包小并发大这两个场景。

  • 第一种场景:瓶颈在于数据拷贝、垃圾回收、CPU占用等,主要依赖语言层面的编码技巧解决,一般问题不大,不需要着重解决

  • 第二种场景:这个场景是需要着重解决的,怎么解决呢?下面来说


高性能网络模块

从技术上看,高性能网络模块的设计可以分为:

  • 如何高效管理大量的TCP连接

  • 如何快速处理高并发请求

  • 如何提高稳定性和降低开发成本


如何高效管理大量的TCP连接

对于高效管理大量的TCP连接,消息队列中主要有单条TCP连接的复用和多路复用两者技术思路


单条TCP连接的复用

对于单条TCP连接的复用,是在一条真实的TCP连接中创建信道channel(虚拟连接),通过编程的手段,将信道当作一条TCP连接使用,避免创建大量的TCP连接,但这在协议设计和编码实现时会有额外的开发工作量,现在一般使用的是I/O多路复用技术,而RabbitMQ由于语言特性和历史原因,使用的是单条TCP连接的复用


I/O多路复用技术

对于I/O多路复用技术,主要的消息队列KafKa、RocketMQ、Pulsar的网络模块都是基于I/O多路复用的思路开发的,将多个I/O阻塞复用到同一个selector的阻塞上,可以让系统在单线程中同时处理多个客户端请求,就是仅靠单线程就可以同时处理多个客户端请求,减小了系统开销,因为仅靠单线程就可以,所以不需要去额外创建进程或线程,降低线程维护的工作量、节约资源

目前支持I/O多路复用的系统调用有select、poll、epoll等,JavaNIO库底层也是基于epoll实现的


即使使用了上述的两种方式来解决大量TCP连接的问题,单机能够处理的连接数依旧是有限的:

  • 操作系统的FD上限,如果连接数超过了FD的数量,那么后续的连接都会创建失败,无法建立连接

  • 系统资源的限制,主要是CPU和内存的限制,频繁创建链接、销毁链接(I/O多路复用)会消耗大量的物理资源,导致系统负载过高

所以一般情况下,每个消息队列的配置都会调高连接数的限制和系统FD的上限。这样就解决了连接管理的问题

image.png


如何快速处理高并发的请求

对于单个请求来说,最快的处理方式就是客户端直接发出请求,服务端接收到请求后直接交给业务线程处理,当业务处理成功后直接返回给客户端。但是有两个问题:

  • 客户端向服务端发送请求数据包,服务端怎么样第一时间拿到这个数据包然后把这个数据包交给业务线程进行处理呢?

  • 当业务线程接收到数据包,处理完之后如何立即将返回数据返回给客户端呢?

最直观的思路就是阻塞等待模型,通过不断轮询等待请求,拿到数据包后交由业务线程处理,处理完之后直接把处理结果返回给客户端,但阻塞等待模型是串行处理机制,每个请求要等待上一个请求完成,就是接收到返回数据之后,处理完成了,才能轮到等待的请求进行处理,所以单个请求通常会采用异步事件驱动模型,通过epoll和异步编程来处理

但是我是在高并发场景下,高并发场景下,有很多连接、请求需要处理,核心思路就是并行、多线程处理,于是就需要Reactor模型。

Reactor模型是一种处理并发服务请求的事件模式,当主流程收到请求后,通过多路分离处理的方式,把请求发给相应的请求处理器进行处理,Reactor模型包含Reactor、Acceptor、Handler三个角色

  • Reactor:负责监听和分配事件。收到事件后分派给对应的Handler进行处理,事件包括连接建立就绪、读就绪、写就绪等操作

  • Acceptor:负责处理客户端新连接。Reactor接收到客户端的连接事件后,会转发给AcceptorAcceptor接收到客户端的连接,然后创建对应的Handler,并向Reactor注册Handler

  • Handler:请求处理器,负责业务逻辑的处理,即业务处理线程

Reactor有三种实现模型


1.单Reactor单线程模型(单Reactor单线程)

面试回答Reactor单线程模型通过一个线程同时处理I/O事件和业务逻辑,利用事件驱动机制实现高并发,比如Redis6.0之前的版本就采用这种设计。他的有点是结构简单、没有锁竞争、没有上下文切换,适合连接数量多但业务处理块的场景,例如缓存、轻量化代理等

但是单Reactor单线程模型的缺点也很明显:

  • 无法利用多核CPU,性能受限于单线程

  • 业务逻辑必须非阻塞,否则会导致整个系统瘫痪

  • 容错性差,一旦主线程崩溃,服务就不可用了

所以单Reactor单线程模型适合I/O密集型但计算简单的场景(如Redis),不适合计算密集型或高可靠要求的系统。

在实际生产中,更复杂的场景会采用多线程Redis或主从Reactor模型来提升吞吐量和稳定性


Reactor单线程模型中ReactorHandler都是单线程串行处理,所以处理逻辑都是单线程实现的,没有上下文切换,没有线程竞争,没有进程通信等问题,但因为是单线程处理,所以无法充分利用CPU资源,无法使用多核CPU,受限于CPU,并且业务逻辑Handler的处理是同步的,容易造成阻塞而出现性能瓶颈,同时Reactor出现异常而不能处理请求,,那么整个系统的通信模块就不可用了,无法建立连接,无法接收客户端发送过来的数据包了,因此单Reactor单线程模型不适合计算密集型场景,只适用于业务处理非常快,不会出现阻塞的场景

image.png


2.单Reactor多线程模型(单Reactor多线程)

Reactor多线程模型在保持单线程事件监听Reactor的基础上,将业务逻辑交给线程池异步处理,在线程池中就可以达到复用,显著提高了吞吐量。例如:Memcached就采用了类似的设计,它的核心优势有三点:

  • 解耦I/O与计算Reactor线程专注事件分开,避免业务阻塞I/O处理

  • 利用多核资源:线程池并行处理任务,适合计算密集型场景

  • 高吞吐量:通过线程池消化任务,降低响应延迟

代价是引入多线程复杂性:

  • 需要通过锁或CAS保护共享数据(如连连接状态)

  • Reactor单点分发可能成为瓶颈(如C10K问题)

  • 线程切换带来额外开销

因此,这种模型适合I/O与计算混合型负载(如中间件),但需要权衡线程安全与性能。

更高并发的场景通常会升级为主从Reactor模型(如Netty),进一步分离I/O与业务线程

技术亮点:

  • 性能瓶颈数据:单Reactor10万级QPS时,CPU可能会因频繁事件分发成为性能瓶颈的问题,此时主从Reactor的吞吐量可提升2~3

  • 锁优化方案:可以通过无锁队列(如Disruptor)减少线程竞争,或改用ThreadLocal避免共享状态

  • 对比案例:类似Nginx的多Worker模式,但Nginx通过进行隔离而非线程池,避免锁开销但牺牲内存效率

回答结构技巧:

  • 场景化:关联实际系统(Memcached/Netty)增强说服力

  • 数字量化:用QPS、线程数等具象化性能表现

  • 辩证表达:先肯定优势,再说明trade-off,体现架构思维

此回答控制在45s内,适合中高级面试


Reactor多线程模型将业务逻辑处理Handler变成了多线程,读取到I/O事件后,业务逻辑在一批线程中进行处理,Handler收到响应后通过send把响应结果返回给客户端,降低Reactor的性能开销,提高了整个系统的吞吐量,同时Handler使用多线程模式,充分利用CPU资源,提高了整个系统的吞吐量,但这也带来了多线程竞争资源的开销,涉及到共享数据的互斥与保护机制,实现起来比较复杂,另外,单个Reactor承担了所有事件的监听、分发、响应,在高并发场景下存在性能瓶颈

image.png


3.主从Reactor多线程模型(多Reactor多线程)

当前主流的消息队列的网络模型都是基于主从Reactor多线程模型开发的,如KafKa、RocketMQ、Pulsar,这可以让Reactor的主线程和子线程分工明确,主线程只负责接收到新连接,子线程负责后续的业务逻辑处理,并且主线程和子线程的交互很简单,只需要子线程接收主线程的请求后,只负责业务逻辑处理即可,无需关心主线程,当子线程处理完数据后直接将结果返回给客户端,因此主从Recator多线程模型适用于高并发场景,Netty的网络通信框架也使用了主从Reactor多线程模型,单如果基于NIO从零开始开发,那么开发的复杂度和成本是很高的,而且Acceptor是单线程,如果挂了就无法处理客户端的新连接,因此有些组件也会为了保证高可用,将Acceptor变为多线程的形态,目前共有云商业化版本的KafKa就使用的是这种模型

image.png

image.png


4.基于成熟网络框架提高稳定性并降低开发成本

稳定性指的是代码稳定性,因为网络模块的编程非常复杂,要考虑的细节和边界很多,需要长时间进行打磨,所以一旦开发完成并稳定后,代码几乎不需要修改,因为需求是比较固定的。在Java中网络编程的核心是NIO库,它的底层是基于LinuxI/O多路复用模型epoll实现的,如果我们基于Java NIO库开发一个Server,那么就要处理网络的闪断、客户端的重复接入、连接管理、安全认证等等细节(心跳保持、半包读写、异常处理),工作量是很大的,所以在消息队列的网络编程模型中,为了提高稳定性、选出现成的、成熟的NIO框架是一个更好的方法

Netty就是一个基于Java NIO封装的成熟框架,当前业界主流消息队列RocketMQ、Pulsar也是基于Netty进行开发的网络模块,KafKa因为历史原因是基于Java NIO实现的

image.png


a.KafKa网络模型

KafKa的网络层没有使用Netty作为底层的通信库,而是直接采用Java NIO实现网络通信,不过也是参照Reactor多线程模型,采用多线程、多Selector设计

Processor线程和Handler线程之间通过RequestChannel传递数据,RequestChannel中包含一个RequestQueue队列和多个ResponseQueue队列,每个Processor线程对应一个ReposneQueue,具体就是:

  • 一个Acceptor接收到客户端的连接请求后,创建Socket连接并分配给Processor处理

  • Processor线程把读取到的请求存入RequestQueue中,由Handler线程从RequestQueue队列中取出请求进行处理

  • Handler线程处理请求产生的响应会存放到Processor对应的ResponseQueue

  • Processor线程从对应的ResponseQueue中取出响应结果并返回 给客户端

image.png


b.RocketMQ网络模型

RocketMQ采用Netty组件作为底层通信库,而Netty底层是主从Reactor的多线程模型,具体是:

  • 一个Reactor主线程复杂监听TCP网络连接请求,连接建立后创建SocketChannel,并注册到Selector上,同时根据OS的类型自动选择NIOEpoll,也可以通过参数配置,监听真正的网络数据,(NIO底层基于poll实现,Epoll底层基于epoll实现)

  • 接收到网络数据后,Reactor主线程会把数据传递给Reactor连接池处理

  • 在真正进行业务逻辑处理前,还需要进行SSL验证、编解码、空闲检查、网络连接管理等工作,这些工作会在Worker线程池处理

  • 之后处理业务的操作会交由业务线程池执行,根据RomotingCommand的业务请求码codeprocessorTable这个本地缓存变量中找对应的Processor,封装成task任务后,提交给对应的业务processor线程池来执行

image.png


5.NIO编程和RPC框架

NIO编程属于TCP层网络编程,我们还需要进行协议设计、编解码、链路的建立/关闭等工作,才能完成完整的网络模块开发。要想不关心底层细节,我们可以通过RPC远程调用实现,而RPC调用的是一个远端对象,调用者和被调用者处于不同节点,因此要实现以下四个功能:

  • 网络传输协议:远端调用底层需要经过网络传输,因此需要选择网络哦通信协议,比如TCP

  • 应用通信协议:网络传输设计应用层的通信协议,比如HTTP2或自定义协议

  • 服务发现:调用的远端对象,需要定位到调用的服务器地址以及调用的具体方法

  • 序列化和反序列化:网络传输的是二进制数据,因此RPC框架需要自带序列化和反序列化的能力

RPC框架完成了通信协议和网络模块设计这两部分,以gRPC为例:

  • gRPC内核很好地实现了服务发现、连接管理、编解码器等公共部分,我们可以把开发精力集中在消息队列本身,不需要在网络模块消耗太多精力

  • gRPC几乎支持所有主流编程语言,开发各个消息队列的SDK可以节省很多开发成本

  • 很多云原生系统,比如Service Mesh都集成了gRPC协议,基于HTTP2gRPC的消息队列很容易被云原生系统中的其他组件访问,组件间的集成成本很低

但是当前主流的消息队列不支持gRPC框架,这是因为如果支持就要做很大的架构改动,而且gRPC底层默认是七层的HTTP2协议,在性能上比直接基于TCP协议实现的方式差一些。但是HTTP2本身在性能上做了一些优化,从实际表现来看,性能损耗在大部分场景下是可以接收的

如果是一个新设计的消息队列或消息队列的新架构,通过成熟的RPC框架来实现网络模块是一个不错的方案,比如RocketMQ5.0中的Proxy就使用了gRPC框架实现了网络模块


6.如何从网络模块设计消息队列
  1. 明确消息队列需要满足什么场景,比如消息、流、IOT 等。

  2. 理解目标场景的业务形态,比如 IOT 就需要管理大量连接,消息就需要尽量保证低延时的话就需要考虑吞吐问题等等。

  3. 根据业务特点分析出技术架构的瓶颈和难点

  4. 考虑技术语言的选型问题,用哪种语言合适,比如 Java、Go、Rust、C++ 等。这点应该 结合技术需要和团队本身的技术栈来思考选择哪种语言。

  5. 理解这个语言当前网络编程的相关库,网络库、网络库框架,并且调研该语言主流网络编程 **技巧**。

  6. 基于理解的网络模块编程思想,结合网络库去实现网络模块。

  7. 在最后需要设计压测场景,利用自研或开源的压测工具,最后完成性能和稳定性验证。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐