OMG!RocketMQ的高性能网络通信机制竟然如此实现?

目录

  • RocketMQ的基本通信通信流程
  • RocketMQ的通信类结构
  • RocketMQ的多线程模型
    • 同步异步与阻塞非阻塞的区别
    • linux 网络 I/O 模型
    • Netty 的多线程模型
    • RocketMQ 的线程模型
  • 消息协议设计与编解码
  • 消息的通信方式和流程
    • Client 发送请求消息
    • Server 接收请求消息和处理逻辑
  • 总结

本文篇幅较长,建议先收藏再观看。

前言

上一篇文章《NameServer 启动流程和存储结构剖析》主要讲解了 RocketMQ 中 NameServer 的元数据存储结构,其实 NameServer 还有一个重要的就是 RemotingServer 通信模块,对于一个消息队列来说,通信模块是一个核心的组件,通信模块的性能很大程度上决定了消息传输的能力和整体性能。

本文会对 RocketMQ 网络通信框架进行深入的学习,让你可以彻底掌握 RocketMQ 的网络通信的底层原理,并且可以学习一下高性能网络通信模块在开源框架中是如何实现的。

RocketMQ 的基本通信流程

下图是 RocketMQ 消息队列的整体架构图

RocketMQ 消息队列的整体架构图

关于 RocketMQ 架构图中各角色和功能可以查看 《人人都懂的RocketMQ基本原理

这里重点介绍一下各个角色是如何进行通信的:

NameServer

NameServer 每隔 10s 会扫描所有的 Broker 连接,如果 NameServer 发现 Broker 上一次的心跳距离当前已经超过 120s,断开 NameServer 与 Broker 的连接,并将 Broker 相关的数据从 NameServer 中剔除掉。(NameServer 剔除 Broker 的流程详见 《NameServer 启动流程和存储结构剖析》的 Broker 下线流程)

Broker

Broker 启动后会跟所有的 NameServer 建立长连接,并且每隔 30s 定时向 NameServer 上报 Topic 路由信息。

Producer

Producer 与 NameServer 集群中的其中一个节点建立长连接,默认每隔 30s 从 NameServer 中获取所有 Topic 队列的最新情况。假如 Broker 不可用,Producer 最多 30s 能够感知。

Producer 与提供 topic 服务的 Broker 建立长连接,默认 30s 向所有关联的 Broker 发送心跳,Broker 每隔 10s 扫描所有存活的连接。如果 Broker 在 120s 内没有收到心跳数据,关闭与 Producer 建立的长连接。

Consumer

Consumer 与 NameServer 集群中的其中一个节点建立长连接,默认每隔 30s 从 NameServer 中获取所有 Topic 队列的最新情况。假如 Broker 不可用,Consumer 最多 30s 能够感知。

Consumer 每隔 30s 向所有关联的 Broker 发送心跳,Broker 每隔 10s 扫描所有存活的连接,若某个连接 120s 内没有发送心跳数据,则关闭连接;并向该 Consumer Group 的所有 Consumer 发出通知,Group 内的 Consumer 重新分配队列,然后继续消费。

可以看出整个 RocketMQ 各个角色之间都存在网络通信,因此如何设计一个良好的网络通信模块在 MQ 中是至关重要的,它将会决定 RocketMQ 集群整体的消息传输能力与最终的性能。

RocketMQ 在 rocketmq-remoting 模块下设计了网络通信的结构,所有用到网络通信的模块(nameserver、broker、客户端)都依赖该模块。并且为了实现客户端与服务器之间高效的数据请求与接收,RocketMQ 消息队列自定义了通信协议并在 Netty 的基础上扩展了通信模块

RocketMQ的通信类结构

我们看一下 Remoting 通信模块的结构类图:

RocketMQ通信类结构

  • RemotingService 是最上层的接口,定义了三个方法
void start();
void shutdown();
void registerRPCHook(RPCHook rpcHook);
复制代码
  • RemotingServer:定义了服务端的接口,继承了上层接口 RemotingService
  • RemotingClient:定义了客户端的接口,继承了上层 RemotingService

RemotingServer 与 RemotingClient 定义的方法是类似的,主要包含了同步、异步、oneway 方式的通信和注册处理器 processor,其余的就是针对服务端和客户端特定的接口方法,比如服务端根据 requestCode 获取处理器的 getProcessorPair() 方法,客户端获取NameServer地址列表 getNameServerAddressList() 方法。

  • NettyRemotingAbstract:Netty 通信抽象类,定义并封装了服务端与客户端公共方法。这个也是 RocketMQ 网络通信的核心类。

  • NettyRemotingServer服务端的实现类,实现了RemotingServer 接口,继承 NettyRemotingAbstract 抽象类。

  • NettyRemotingClient客户端的实现类,实现类 RemotingClient 接口,继承 NettyRemotingAbstract 抽象类。

RocketMQ 基于 Netty 框架的基础上,对通信的 Server 和 Client 进行抽象和封装的处理,使得结构更为简洁和易扩展。

RocketMQ 基于 Netty 进行扩展,那我们来学习一下 RocketMQ 的多线程模型是如何实现的。

RocketMQ 多线程模型

学习 RocketMQ 多线程模型,需要学习下面的基本概念:

  • 同步异步与阻塞非阻塞的区别
  • linux 网络 I/O 模型
  • Netty 的多线程模型

同步异步与阻塞非阻塞的区别

同步和异步

关注的是结果消息的通信机制

同步:同步的意思就是调用方需要主动等待结果的返回。

异步:异步的意思就是不需要主动等待结果的返回,而是通过其它手段,比如:状态通知,回调函数等。

阻塞与非阻塞

主要关注的是等待结果返回的调用方的状态

阻塞:进程调用接口后,如果接口没有准备好数据,那么这个进程会被挂起,什么都不能做,直到有数据返回的时候才会唤醒。内核将 CPU 时间片切换给其它需要的进程,那当前的进程在这种情况下就得不到 CPU 时间做该做的事情了。

非阻塞:进程调用接口,如果接口没有准备好数据,进程也能处理后续的操作,不会被挂起,CPU 时间片不会切换给其他进程,应用程序可以得到足够的 CPU 时间继续完成其它事情。通过不断的轮询检查数据是否已经处理完成了。由于轮询效率太低了(每次都需要进行系统调用),就有了后面讲到的 I/O 多路复用模型。

下图是网络编程中,socket 下面 read 与 write 方法阻塞模式与非阻塞模式的区别:

阻塞与非阻塞的区别

两者的组合

同步和异步在于是否需要你去确认是否有结果。 阻塞与非阻塞在于你这期间是一直等待结果还是不等待做其它的事情。

同步阻塞

同步阻塞是编程中常见的模型,打个比方你去喜茶买杯奶茶,这个时候需要现做奶茶,你需要在店里一直等待,期间不能做任何事情,像木头人一样死等,等到店员做完后交给你。(不能玩手机感觉要死了)

同步非阻塞

同步非阻塞在编程模式中可以抽象为一种轮询模式,这个时候你不需要死等了,你可以玩手机或者先出去逛一逛,然后回来问店员奶茶做好了么。

异步阻塞

异步阻塞在编程模式中用的比较少,有点像你写了个线程池,submit 之后马上 future.get()。这就像你买奶茶,店员再做,然后告诉你好了会叫号的,然后呢,你就瞪大眼睛一直看着号码牌。

异步非阻塞

异步非阻塞,你不需要瞪大眼睛一直盯着号码牌了,快玩手机吧,到了你的号码就会通知你的。

Linux 网络 IO 模型

CPU 处理数据的速度远大于 IO 准备数据的速度 。所以理论上 任何编程语言 都会遇到这种 CPU 处理速度和 IO 速度不匹配的问题,在网络编程中如何进行网络 IO 优化,怎么高效地利用 CPU 进行网络数据处理就变得非常重要。

首先我们要知道一个基本概念,Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件),对一个 File 的读写操作会调用内核提供的系统命令,返回一个 File Descriptor(FD 文件描述符)。而对一个 Socket 的读写也会有响应的描述符,称为 Socket File Descriptor(Socket 文件描述符),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区等一些属性)。

根据 UNIX 网络编程对 IO 模型的分类,UNIX 提供了 5 种 IO 模型。

5 种 I/O 模型主要经历了下面两个阶段:

  1. 等待数据准备好,需要从网卡将数据拷贝到内核缓冲区中
  2. 将准备好的数据,从内核缓冲区中拷贝到用户缓冲区中

IO模型两阶段

下面介绍 IO 模型的时候所说的第一阶段和第二阶段就对应着上面的这两个阶段。

阻塞 IO 模型

read 和 write 操作都可以理解为是一个 recvfrom 的调用。

阻塞I/O模型

阻塞 IO 模型(Blocking IO)是最常用的 IO 模型,IO 模型两个阶段没有准备好的情况下,所有文件操作都是阻塞的。以套接字为例:在进程空间中调用 recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或发生错误时才返回。在此期间一直会等待,进程从调用 recvfrom 开始到它返回的整段时间都是被阻塞的。即 recvfrom 的调用会被阻塞。

优点:程序简单,阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。

缺点:每个连接需要独立的进程/线程单独处理,高并发的情况下,会创建大量的连接,导致内存、线程切换开销都很大,所以不适合高并发的场景使用。

非阻塞 IO 模型

非阻塞I/O模型

非阻塞 IO 模型(Non-blocking IO),recvfrom 从应用层到内核的时候,如果第一阶段没有准备好,缓冲区没有数据的话,就直接返回一个 EWOULDBLOCK 错误,一般对非阻塞 IO 模型进行轮询检查这个状态,看内核是不是有数据到来。即反复调用 recvfrom 等待成功指示(轮询)。

优点:不会阻塞在内核等待数据的过程,也就是不会等待第一阶段准备的过程,不用阻塞等待,有较好的实时性。

缺点:不断轮询访问内核,每次访问都是一次系统调用,每次系统调用都会涉及到用户态与内核态的切换,一直占用着 CPU,系统资源利用率降低。

我们需要减少频繁的系统调用,需要将轮询的操作交给操作系统来进行实现,我们只需要进行一次系统调用就可以了。

IO 多路复用模型

IO多路复用模型

多路复用:「多路」代表着多个网络连接,「复用」指的是复用同一个线程。

IO 复用模型(IO Multiplexing),Linux 提供 select/poll,进程通过将一个或者多个 fd 传递给 select 或 poll 系统调用,阻塞在 select 操作上,阻塞在 select 操作上,这样 select/poll 可以帮我们侦测到多个 fd 是否处于就绪状态。select/poll 会顺序扫描 fd 是否就绪,select 与 poll 区别在于 fd 数量上限制,select 限制 fd 数量为 1024,poll 对数量不限制。select/poll 系统调用有下面几个问题:

  1. select/poll 调用需要传入 fd 数组,需要拷贝一份到内核中,高并发场景下会带来很大的性能消耗。减少 fd 数组的复制可以减少不必要的开销。
  2. select/poll 在内核层面仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步的过程。如果改为异步事件通知,可以有效提高内核的工作效率。
  3. select /poll 仅仅返回刻度描述文件符的个数,具体哪个可读还需要用户自己遍历。如果给用户返回就绪的文件描述符,就不需要用户进行遍历了。

epoll 针对以上三点进行了改进:

  1. 内核会保存一份文件描述符集合,不需要用户每次都重新传入,只需要告诉内核修改的部分即可。

  2. 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 I/O 事件唤醒

  3. 内核仅会将有 IO 事件的文件描述符返回给用户,用户也不需要遍历整个文件描述符集合。

    下图是 epoll 的流程,源自:闪客的《 你管这破玩意叫 IO 多路复用?》

    epoll的流程

信号驱动 IO 模型

信号驱动 IO 模型

首先我们允许套接口进行信号驱动 I/O, 并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用I/O操作函数处理数据。

**优点:**线程并没有在等待数据时被阻塞,可以提高资源的利用率。

**缺点:**信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。

但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。

异步 IO 模型

异步 IO 模型

异步 IO(Asynchronous IO),告知内核启动某个文件,并让内核整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。

这种模型与信号模型的主要区别是:信号驱动 IO 由内核通知我们何时可以开始一个 IO 操作:异步 IO 模型由内核通知我们 IO 操作何时已完成。

优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。

缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。

而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 IO 复用模型模式为主。

五种 IO 模型的总结

五种IO模型总结

这五种 I/O 模型中,前四种属于同步 I/O,因为其中真正的 I/O 操作将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配。

阻塞式I/O:简单、高并发创建连接多,造成性能瓶颈。

非阻塞式I/O:不用阻塞等待数据,快速响应,但轮询是在线程中进行的,会占用 CPU 时间,浪费系统资源。

I/O 多路复用:多个网络连接可以复用同一个线程,而且多路复用的轮询是在内核中进行,效率比线程更高。并且 epoll 比 select/poll 性能更好,直接返回发生 I/O 事件的描述符。

Netty 的多线程模型

通过上面讲解,我们了解了五种 I/O 模型,Netty 是基于 NIO 实现的高性能、异步事件驱动的网络通信框架。

基于 NIO 可以实现 I/O 多路复用模型,通过一个线程对多个连接事件进行监听。Netty 基于此实现了 Reactor 多线程模型。

Reactor 模式:是指通过一个或者多个输入同时传递给服务器的服务请求的事件驱动处理模式。

通过 I/O 多路复用统一监听事件,收到事件后分发给线程进行处理。

单 Reactor 单线程

单 Reactor 单线程

如图是单 Reactor 单线程对请求的处理流程:

  1. Reactor 基于 select 实现 I/O 多路复用,应用程序通过一个阻塞对象监听多路连接请求。
  2. 如果是建立连接请求事件,通过 Acceptor 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理。
  3. 如果不是建立事件,则 Reactor 会分发调用连接对应的 Handler 来处理请求。
  4. Handler 会完成:decode -> read -> 业务处理 -> send -> encode 完整的业务流程

这个模型是单线程处理,不会涉及上下文切换、资源竞争等问题。但是单线程处理没有办法发挥多核 CPU 的性能,并且 Handler 如果处理某个响应很慢,就会导致其它的请求处于长时间等待状态。并且如果线程意外掉线或者进入死循环,会导致整个通信模块不可用,造成节点故障。

单 Reactor 多线程

单 Reactor 多线程

Handler 不进行业务的处理,只负责响应,业务交给 Worker 线程池进行处理。这样可以充分利用多核 CPU 的能力,但是 Reactor 承担所有事件监听和响应,单线程运行,高并发场景下就很容易出现系统瓶颈。

所以需要建立连接时间与读写事件进行分开,就有了多 Reactor 的场景。

多 Reactor 多线程

多 Reactor 多线程

Reactor 主线程 MainReactor 进行建立连接,然后将创建好的连接分配到 Reactor 子线程 SubReactor 进行事件的监听。然后 SubReactor 监听到读写事件,就进行请求的处理,这块和上面的单 Reactor 多线程是一样的了。

基于该模型,我们可以通过增加 Reactor 的实例个数来充分利用 CPU 资源,并且将各个处理进行了解耦,提高了整体的并发效率。

Netty 就是基于主从多线程模型来支持高并发网络请求的处理。

RocketMQ 的线程模型

RocketMQ 在 Netty 原生的多线程 Reactor 模型上做了一系列的扩展和优化,如下图所示:

RocketMQ 多线程设计

其实是四个线程池,分别进行不同的处理,线程池分别对应数字:1 + N + M1 + M2

  • 一个 Reactor 主线程(eventLoopGroupBoss,即为上面的1)负责监听 TCP 网络连接请求,建立好连接,创建SocketChannel,并注册到 selector 上。
  • RocketMQ的源码中会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置, 然后监听真正的网络数据。(NIO 底层基于 poll 实现,Epoll 底层是基于 epoll 实现)
  • 拿到网络数据后,再丢给 Worker 线程池(eventLoopGroupSelector,即为上面的“N”,源码中默认设置为 3 )。
  • 在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给defaultEventExecutorGroup(即为上面的“M1”,源码中默认设置为8)去做。
  • 而处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去processorTable 这个本地缓存变量中找到对应的 processor,然后封装成 task 任务后,提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例,即为上面的 “M2”)。
  • 从入口到业务逻辑的几个步骤中线程池一直在增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽。
线程数 线程名 线程具体说明
1 NettyBoss_%d Reactor 主线程
N NettyServerEPOLLSelector_%d_%d Reactor 线程池
M1 NettyServerCodecThread_%d Worker线程池
M2 RemotingExecutorThread_%d 业务processor处理线程池

消息协议设计与编解码

客户端与服务端之间发送消息,需要对发送的消息进行一个协议约定,按照约定好的协议规则进行编码和解码操作。RocketMQ 自定义了 RocketMQ 的消息协议,并且为了高效地在网络中传输消息和对收到消息读取,对消息进行编解码。

RemotingCommand 这个类在消息传输的过程中对所有数据内容进行封装,包含下面的一些数据内容:

Header字段 类型 Request说明 Response说明
code int 请求操作码,应答方根据不同的请求码进行不同的业务处理 应答响应码。0表示成功,非0则表示各种错误
language LanguageCode 请求方实现的语言 应答方实现的语言
version int 请求方程序的版本 应答方程序的版本
opaque int 相当于requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 应答不做修改直接返回
flag int 区分是普通RPC还是onewayRPC的标志 区分是普通RPC还是onewayRPC的标志
remark String 传输自定义文本信息 传输自定义文本信息
extFields HashMap<String, String> 请求自定义扩展信息 响应自定义扩展信息

自定义的 RocketMQ 传输协议格式如下:

 RocketMQ 传输协议格式

可见传输内容主要可以分为以下4部分:

(1) 消息长度:总长度,四个字节存储,占用一个int类型;

(2) 序列化类型&消息头长度:同样占用一个int类型,第一个字节表示序列化类型,后面三个字节表示消息头长度;

(3) 消息头数据:经过序列化后的消息头数据;

(4) 消息主体数据:消息主体的二进制字节数据内容;

RemotingCommand 中的编码和解码的源码:

// RemotingCommand 根据传输协议进行编码
public ByteBuffer encode() {
        // 1> header length size
        int length = 4;

        // 2> header data length,把消息头进行了编码,拿到了header字节数组
        byte[] headerData = this.headerEncode();
        length += headerData.length;

        // 3> body data length
        if (this.body != null) {
            length += body.length;
        }

        ByteBuffer result = ByteBuffer.allocate(4 + length);

        // 1、设置消息长度
        result.putInt(length);

        // 2、设置序列化类型+消息头长度
        result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC));

        // 3、经过序列化后的消息头数据
        result.put(headerData);

        // 4、消息主题数据
        if (this.body != null) {
            result.put(this.body);
        }

        result.flip();

        return result;
}
复制代码
// RemotingCommand 根据传输协议进行解码
    public static RemotingCommand decode(final byte[] array) throws RemotingCommandException {
        ByteBuffer byteBuffer = ByteBuffer.wrap(array); 
        return decode(byteBuffer);
    }

    public static RemotingCommand decode(final ByteBuffer byteBuffer) throws RemotingCommandException {
        // 解码的过程就是编码过程的逆向过程
        int length = byteBuffer.limit(); // 获取总长度
        int oriHeaderLen = byteBuffer.getInt(); // 获取消息长度
        int headerLength = getHeaderLength(oriHeaderLen);// 获取消息头长度

        // 搞一个头长度的字节数组,一次性把headers都读出来放到字节数组里去
        byte[] headerData = new byte[headerLength];
        // 获取消息头数据
        byteBuffer.get(headerData);

        // 对header根据协议类型进行解码
        RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen));

				// 获取消息体长度
        int bodyLength = length - 4 - headerLength;
        byte[] bodyData = null;
        // 获取消息体内容
        if (bodyLength > 0) {
            bodyData = new byte[bodyLength];
            byteBuffer.get(bodyData);
        }
        cmd.body = bodyData;

        return cmd;
    }
复制代码

消息的通信方式和流程

RocketMQ 消息通信主要有三种方式:同步(sync),异步(async),单向(oneway)

我们就以同步通信模式为例,来分析一下客户端发送流程和服务端接收消息并处理逻辑的流程。

Client 发送请求消息

客户端(生产者 Producer)发送消息的时候,会直接调用 DefaultMQProducerImpl 类中的 send(Message) 进行消息的发送,默认是同步通信模式的。这个方法最底层会调用 NettyRemotingClient#invokeSync 方法,获取到服务器与 Broker 之间的 Channel,调用 NettyRemotingAbstract#invokeSyncImpl 方法传入 Channel 和 RemotingCommand 将消息发送给服务端(Broker)。

NettyRemotingAbstract#invokeSyncImpl 发送消息的源码如下(已附上相关注释):

// 因为需要先分析出来请求,然后再进行响应的分析,分析请求肯定要根据调用 responseTable.put 的那块作为切入点进行分析。
public RemotingCommand invokeSyncImpl(
        final Channel channel, // 网络连接
        final RemotingCommand request, // 需要发送的请求
        final long timeoutMillis // 同步发送网络请求的超时时间
)
        throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
    // requestId,每个请求的唯一ID
    final int opaque = request.getOpaque();

    try {
        // 将 Channel 和 requestID 封装成一个 ResponseFuture
        final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
        // 将 ResponseFuture 放入到 responseTable 中,并以 requestId 作为 key。
        this.responseTable.put(opaque, responseFuture);
        final SocketAddress addr = channel.remoteAddress();
        // 使用 Netty 的 Channel 发送请求数据到服务端
        channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture f) throws Exception {
                // 请求成功,设置 ResponseFuture 的 sendRequestOK = true
                if (f.isSuccess()) {
                    responseFuture.setSendRequestOK(true);
                    return;
                } else {
                    responseFuture.setSendRequestOK(false);
                }

                responseTable.remove(opaque);
                // 请求失败,将异常进行保存
                responseFuture.setCause(f.cause());
                responseFuture.putResponse(null);
                log.warn("send a request command to channel <" + addr + "> failed.");
            }
        });
        
        // 基于 CountDownLatch 实现同步,等待 timeoutMillis 毫秒。
        RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
        // 判断请求异常的情况
        if (null == responseCommand) {
            if (responseFuture.isSendRequestOK()) {
                throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
                        responseFuture.getCause());
            } else {
                throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
            }
        }
        return responseCommand;
    } finally {
        this.responseTable.remove(opaque);
    }
}

复制代码
  • opaque:请求唯一标识码,客户端会给每个请求生成一个唯一标识码。
  • ResponseFuture:获取发送消息结果的封装对象,内部基于 CountDownLatch 来实现消息同步通信模式。创建对象会默认创建一个 countDownLatch = new CountDownLatch(1),调用 Channel 进行消息发送后然后调用 waitResponse(timeoutMillis)(内部是countDownLatch.await(timeout))阻塞等待结果,请求结果到达后会通过 countDownLatch.countDown() 来释放计数器。
  • responseTable:保存 opaqueResponseFuture 的映射关系,对于每个请求发送前,我们将关系保存起来,当响应到达后,可以根据唯一标识来获取 ResponseFuture 并设置响应结果 responseCommand

Server 接收消息和处理逻辑

Server 端接收消息的核心方法是 NettyRemotingAbstract#processRequestCommand,源码如下(省略一些异常处理代码):

public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
    // 获取请求处理组件
    final Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
    // 如果没有获取默认的请求处理组件
    final Pair<NettyRequestProcessor, ExecutorService> pair = null == matched ? this.defaultRequestProcessor : matched;
    // 获取请求的id
    final int opaque = cmd.getOpaque();

    if (pair != null) {
        Runnable run = new Runnable() {
            @Override
            public void run() {
                try {
                    String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
                    doBeforeRpcHooks(remoteAddr, cmd);
                    // 设置请求回调函数
                    final RemotingResponseCallback callback = new RemotingResponseCallback() {
                        @Override
                        public void callback(RemotingCommand response) {
                            doAfterRpcHooks(remoteAddr, cmd, response);
                            if (!cmd.isOnewayRPC()) {
                                if (response != null) {
                                    response.setOpaque(opaque);
                                    response.markResponseType();
                                    try {
                                        ctx.writeAndFlush(response);
                                    } catch (Throwable e) {
                                        log.error("process request over, but response failed", e);
                                        log.error(cmd.toString());
                                        log.error(response.toString());
                                    }
                                } else {
                                }
                            }
                        }
                    };
                    // 异步处理
                    if (pair.getObject1() instanceof AsyncNettyRequestProcessor) {
                        AsyncNettyRequestProcessor processor = (AsyncNettyRequestProcessor)pair.getObject1();
                        processor.asyncProcessRequest(ctx, cmd, callback);
                    } else {
                        // oneway 和 同步处理
                        NettyRequestProcessor processor = pair.getObject1();
                        RemotingCommand response = processor.processRequest(ctx, cmd);
                        callback.callback(response);
                    }
                } catch (Throwable e) {
                    log.error("process request exception", e);
                    log.error(cmd.toString());

                    if (!cmd.isOnewayRPC()) {
                        final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR,
                                RemotingHelper.exceptionSimpleDesc(e));
                        response.setOpaque(opaque);
                        ctx.writeAndFlush(response);
                    }
                }
            }
        };

        // 如果拒绝请求,就返回系统繁忙。
        if (pair.getObject1().rejectRequest()) {
            final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                    "[REJECTREQUEST]system busy, start flow control for a while");
            response.setOpaque(opaque);
            ctx.writeAndFlush(response);
            return;
        }

        try {
            // 将线程和channel,requestCommand 创建一个 RequestTask
            final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
            // 交给执行器对应的线程池处理,对应的就是我们在 RocketMQ 多线程模型的 M2。
            pair.getObject2().submit(requestTask);
        } catch (RejectedExecutionException e) {
            // 线程池拒绝处理异常
        }
    } else {
        // 没有处理器异常
    }
}
复制代码
  1. 根据 code 获取处理器和线程池 Pair<NettyRequestProcessor, ExecutorService> pair
  2. 将请求逻辑封装成一个 Runnable,内部根据 NettyRequestProcessor 进行请求的处理,并创建一个 RemotingCommand response,将响应结果通过 Netty 调用 writeAndFlush(response) 进行返回。
  3. 创建 RequestTask 封装线程、Channel、requestCommand,交给对应的线程池进行处理,也就是 M2 线程池。

总结

本篇文章分别从多线程模型、消息协议设计与编解码、消息通信方式这几个方面并结合源码深入的带着大家了解了 RocketMQ 的整个通信模块的设计与交互流程。并且在多线程模型中带着大家了解阻塞非阻塞和同步异步、五种网络I/O、Reactor 开发模式这几个基础概念。

关于 RocketMQ 网络通信这块就告一段落,后续小李会按照一个消息的发送流程,分别从 Producer 发送消息、Broker 存储消息、Consumer 消费消息来继续带着大家深入剖析 RocketMQ 的底层原理。

我是小李,欢迎大家留言探讨,如果看完文章觉得有收获的话,记得点击关注~

猜你喜欢

转载自juejin.im/post/7103437918366089230