RocketMQ网络通信基础概念

58 阅读8分钟

 一、概述

        对于消息队列来说,网络模块是其重要的组成之一,网络模块的性能很大程度上决定了消息的传输性能和整体性能。

        我们都知道在Broker启动向NameServer发起注册的时候,还有我们生产者向Broker发送消息的时候他们之间的通信是怎么实现的呢?

二、通信协议介绍

       数据要在网络中传输首先要考虑的就是使用哪种通信协议,像我们耳熟能详的Http协议,TCP/UDP协议等等,假设让我们来选择消息中间件的通信协议,我们应该选取那种协议作为消息中间件的通信协议呢?

通信协议的选择

       通信协议的选择要根据我们具体的需求来,我们都知道消息队列的核心特性是高吞吐、高可靠、低延迟,所以我们在协议的选择上至少要满足:

  • 可靠性要高,不能丢失数据
  • 性能要高,通信延迟要低

       回想我们之前学到知识,是否满足我们需求的这种协议呢,目前业界这种通信协议我们可以分为两种可以分为:公有协议和私有协议这两种协议。

(1)公有协议:所谓的公有协议就向我们耳熟能详的Http协议,具备通用性。

(2)私有协议:根据自身的功能和需求去设计的协议,一般不具备通用性,比如RocketMQ,kafka的协议都是私有协议。

       通过自定义协议,RocketMQ 可以更精确地控制消息的序列化、压缩、加密等操作,从而优化消息传输的性能。相较于使用通用协议(如 HTTP、REST 等),私有协议能够提供更低的延迟和更高的吞吐量。

编辑

 协议类型图

         从功能需求出发,为了保证性能和可靠性,几乎所有主流消息队列在核心生产、消费链路的协 议选择上,都是基于可靠性高、长连接的 TCP 协议。

设计私有协议

设计出满足消息队列的私有协议一般包括一下核心几个点:

(1)高效的消息编码与解码

  • 消息格式的简洁性:私有协议应该尽量设计成简单、紧凑的格式,以减少网络传输的开销。避免冗余字段和复杂的结构,采用高效的序列化方式(例如使用 Protobuf、Avro 或自定义的二进制格式)
  • 支持高效的编码/解码:在消息的编解码过程中,效率是非常关键的,尤其是高并发环境下。选择合适的编解码技术和数据格式,保证消息的快速处理。

(2)协议的扩展性

  • 版本管理与兼容性:随着系统的迭代,协议的版本可能需要不断更新。因此,设计时应考虑到协议的向后兼容性和向前兼容性。通常通过版本号控制或协议字段扩展机制来实现。
  • 支持多种类型的消息:协议需要设计成能够支持多种消息类型(例如普通消息、延迟消息、事务消息等),并为每种类型的消息定义相应的处理逻辑。

(3)可靠性保障机制

  • 消息确认机制:为了保证消息不丢失,私有协议需要实现消息的确认机制(如 ACK/NACK)。这能确保消息在传输过程中一旦被消费或者丢失后能够重新传递。
  • 消息持久化支持:协议设计中需要明确支持消息的持久化机制,例如设计特定的标识符或元数据来指示消息是否已成功存储在消息队列中。
  • 重试与去重机制:应对因网络或其他原因导致的消息丢失或重复发送,私有协议需要支持自动重试和去重机制,确保消息的高可靠性。
编解码

       编解码也有另外一个我们熟悉的名称序列化和反序列化,数据在发送是时候编码,收到数据的时候进行解码。

       数据在网络中传输时是以二进制的方式传输的,所以在客户端发送数据的时候就要将原始数据的格式编码为二进制数据,以便在TCP协议中传输,这就是我们说的序列化。同样的我们服务端接收到二进制数据将约定好的规范解析成为原始数据,这就是反序列化。在序列化和反序列化中,最重要的就是 TCP 的粘包和拆包。

1. 粘包(Sticky Packet)

  • 定义:粘包是指多个应用层数据包被 TCP 协议层合并成一个 TCP 数据包发送。也就是说,发送方连续发出的多个小数据包,在接收方接收到数据时,可能会被粘在一起,无法区分出每个原始数据包的边界。
  • 原因:TCP 是一种面向字节流的协议,它并不关心数据的边界,数据以流的形式进行传输。TCP 在发送和接收数据时,可能会将多个小数据包合并成一个大的 TCP 包进行传输,而接收方并不知晓原始的分包结构。这是因为 TCP 层对应用层数据流进行自动的流量控制和合并操作。

2. 拆包(Packet Splitting)

  • 定义:拆包是指一个大的应用层数据包在传输过程中被拆分成多个小的 TCP 数据包发送,接收方在接收到数据时可能需要将多个包重新组合才能得到完整的应用层数据包。
  • 原因:TCP 传输的数据包大小受最大传输单元(MTU)限制,通常 MTU 是 1500 字节。应用层的数据如果超过了 MTU 的大小,TCP 会将其拆分成多个小的数据包进行传输。接收方接收到的这些数据包可能需要重新组合才能恢复出完整的应用层数据包。

我们从上面可以看到实现从0到1的编解码器比较复杂,早期的消息队列的协议设计几乎都是自定义实现的编解码,随着业界主流的编解码框架和协议的成熟,一些消息队列开始使用成熟的编解码框架(RocketMQ 5.X),如Protobuf框架。

在RocketMQ中引入了基于gRPC实现的Protobuf编解码框架,而5.0之前的版本定义了RemotingCommand作为通信协议,就需要设计他的一个整体结构,而gRPC只需要关注协议体。其内部已经完成了整体结构和协议体的设计,使用现成的编解码框架比自定义编解码更加方便和高效。

三、RocketMQ 网络模型

       RocketMQ 采用 Netty 组件作为底层通信库,遵循 Reactor 多线程模型,Netty 底层采用的是主从 Reactor 多线程模型。

Reactor 主从多线程模型

Reactor 主从多线程模型是对传统 Reactor 模型 的一种扩展,旨在通过多线程方式提高并发处理能力,尤其是在负载较高的场景下。它通过将 事件监听事件处理 分离,利用多线程来并行处理事件,避免了单线程模型可能出现的性能瓶颈。

​编辑

处理流程:

  • Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理事件;
  • 当Accept处理连接事件后,MainReactor将连接分配给SubReactor;
  • subReactor将连接加入到连接队列进行监听,并创建handler进行各种事件处理;
  • 当有新事件发生时,subReactor将调用对应的handler处理;
  • handler通过read读取数据,分发给后面的worker线程进行处理;
  • handler收到响应结果后,通过send发送给client;

在了解了Reactor 主从多线程模型后,我们再来看看RocketMQ 中 NettyRemotingServer 的具体实现形式。

NettyRemotingServer实现

​编辑

具体流程:

  • 一个 Reactor 主线程负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并 注册到 Selector 上。RocketMQ 的源码中会自动根据 OS 的类型选择 NIO 和 Epoll,也可 以通过参数配置,监听真正的网络数据。
  • 接收到网络数据后,会把数据传递给 Reactor 线程池处理。
  • 真正执行业务逻辑之前,会进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作 在 Worker 线程池处理(defaultEventExecutorGroup)。
  • 处理业务操作,放在业务 Processor 线程池中执行。

四 、小结

RocketMQ网络模块的性能问题,核心是通过 Reactor 模型、IO 多路复用技术解决的。Reactor 模式在 Java 网络编 程中用得非常广泛,比如 Netty 就实现了 Reactor 多线程模型。