走进 RocketMQ(四)高性能网络通信
前言
Halo,我是白裤。
上一次我们学习了 RocketMQ 消息的存储与消费的原理机制,今天我们将学习 RocketMQ 的网络通信原理机制。
众所周知,RocketMQ 的性能是非常高的,单机 TPS 可以达到 10W 级别,而且这家伙还参与过双十一,顶住了双十一的压力,相信参与过双十一的家伙性能应该不会差到哪儿去,我们今天来看下 RocketMQ 的网络通信模块的设计与实现,大部分中间件的运行都避免不了网络通信这个模块,在 RocketMQ 中,有多处功能都需要网络通信模块的支撑,如 Broker 向 NameServer 上报 Topic 路由信息、Producer 向 NameServer 拉取 Topic 路由信息、Producer 对消息的发送、Broker 对消息的接收等等,所以一个好的网络通信设计也影响着中间件的性能表现。
在了解 RocketMQ 的网络通信前,我们先从 Linux 的 I/O 模型开始到 Java 的 NIO 再到 Netty 的 Reactor 模型了解整个 I/O 模型的一个起始和演变,这样有利于帮助我们理解 RocketMQ 的网络通信设计。
Linux I/O 模型
我们都知道,在 Linux 操作系统中,内存空间分为用户空间和内核空间,用户进程在获取数据或发送数据的时候都得经过内核空间的拷贝,那为什么要分为用户空间和内存空间呢?其实最主要的原因是安全问题,你想想,如果用户进程可以随意操作系统内核,那不乱套了,因为内核是拥有访问底层硬件设备的权限的,所以这就导致用户进程得有自己的内存空间,不能直接访问内核空间。那么用户进程在获取数据或发送数据的时候它是怎样的一个过程呢?我们以读数据为例来看看,一般用户进程读取数据会经过以下两个阶段:
阶段一:等待内核的数据准备
阶段二:数据从内核空间拷贝至用户空间
其实 I/O 就是对 socket 即流的操作,当发生一次数据读取的时候就相当于发生一次 I/O 操作,这个过程有多种实现方式,具体分为五大模型:
同步阻塞 I/O
顾名思义,这个 I/O 过程是阻塞的,即用户进程从发起数据获取请求系统调用到内核准备并且拷贝好数据再到用户进程拿到数据的整个过程都是阻塞的,是需要等待的。
同步非阻塞 I/O
同步非阻塞 I/O 指的是用户进程在请求数据时,用户进程会一直向内核再次请求询问数据,如果内核数据未准备好,用户进程会继续请求询问,这个阶段是非阻塞的,当内核数据准备完成之后,会将数据拷贝至用户空间,最后用户进程获取到数据,这个阶段是阻塞的,所以同步非阻塞 I/O 并不是整个过程都是非阻塞的,只是在内核数据还未准备好之前是非阻塞,而在内核数据准备完成后拷贝至用户空间时这个阶段是阻塞的。
多路复用 I/O
I/O 多路复用是用户进程向系统请求数据,不再需要自己去轮询结果,而是调用系统的函数(select、poll、epoll)去处理,由这些系统函数去帮忙询问数据的准备结果,这些系统函数可以同时处理多个 I/O 操作,如果数据准备好了,会通知对应的用户进程,然后对应的用户进程才进行发起读取数据的系统调用,数据才拷贝至用户空间,用户进程才拿到数据进行自己的处理。
信号驱动 I/O
信号驱动 I/O 是用户进程向内核发送信号申明需要获取哪些数据,内核会立即返回反馈,然后内核就会准备数据,准备好了数据就会主动通知用户进程,这时候用户进程才去读取数据,等待数据从内核空间拷贝至用户空间,则获取到数据进行处理。
异步 I/O
异步 I/O 是用户发起异步读取数据的系统调用,请求会被立即返回,然后内核开始准备数据并且拷贝至对应的用户空间,拷贝完成后,用户进程拿到数据进行处理。
上面我们简单阐述了系统 I/O 的五种基本模型,有同步也有异步,有阻塞也有非阻塞,对于每种模型都有其非常显著的特点。
同步和异步最主要的区别:请求的结果是否随着请求的结束而返回。
阻塞和非阻塞主要的区别:调用线程是否被挂起。
Java NIO
在了解了五种基本 I/O 模型后,我们再来了解下 Java 的 NIO,Java 的 NIO 是一种同步非阻塞的 I/O 模型,是 I/O 多路复用的基础。
Java NIO 是一种基于 Channel 和 Buffer 的 I/O 方式,相对于传统的阻塞 I/O 即 BIO 来说,BIO 是以流的方式进行数据的处理,而 NIO 是以 Buffer 的方式对数据进行处理,效率上是优于 BIO 的,下面咱们来看看 NIO 是怎么进行数据的处理操作的。
NIO 是基于事件模型来处理 I/O 操作请求的,以便达到处理海量连接的效率,在 NIO 中有几个核心组件,包括 Selector、Channel、Buffer,它们之间有什么关系呢?我们先来看看下面这张图:
组件含义:
Selector
选择器,通过监听注册在 Selector 上的 Channel 发生的事件类型来处理对应的事件,其中事件包括 OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE。
Channel
通道,注册在 Selector 上,一个 Channel 对应一个 Buffer,与 Buffer 是双向的,可以从 Buffer 读数据也可以向 Buffer 写数据。
Buffer
缓冲区,数据的载体容器,也相当于一块分配的内存块。
工作流程:
服务端会创建一个 SocketServerChannel 实例,去监听某个端口,然后获取一个 Selector 对象,将 SocketServerChannel 与需要关心的事件注册到 Selector 对象上面(这里多个 SocketServerChannel 是可以注册到一个 Selector 上面的),然后服务端会等待客户端的连接且一直轮询结果,如果轮询的结果中获取的事件集合大于 0,则证明有事件发生,然后按照事件的类型进行对应的处理,如果是 ACCEPT 事件,还应该为 channel 分配一个 Buffer,作为用于读写数据的内存块。
Netty 线程模型
在网络通信中,RocketMQ 之所以能有如此高的性能,是因为 RocketMQ 的网络通信是基于 Netty 来扩展设计的,众所周知,Netty 在网络通信中享有盛名,很多中间件的网络通信都选择基于 Netty 来实现,所以在了解 RocketMQ 的线程模型前,我们先简单了解下 Netty 的 Reactor 线程模型。
Netty 的 Reactor 模式分为多种模式,分别有如下几种:
单线程模式
这种模式是只用一个线程来处理所有的 I/O 事件和业务逻辑。
多线程模式
I/O 事件由一个线程去处理,业务的逻辑丢给工作线程池去处理。
主从模式
一个主 Reactor 线程只处理 I/O 的 Accept 事件,多个子 Reactor 线程处理 I/O 的 Read、Write 事件,业务逻辑就由工作线程池去处理。
默认模式
一个主 Reactor 线程处理 Accept 事件,多个子 Reactor 线程处理 Read、Write 事件,工作线程处理业务逻辑,并将子 Reactor 线程和工作线程池化放置于 Reactor 线程池中执行。
RocketMQ 网络通信设计
好的,前面了解了 I/O 的基础模型,然后我们又简单了解了下 Java NIO 以及 Netty 的 Reactor 模型,心里大概对于 I/O 的几种工作模式和演变流程都有个数了,接下来我们正在步入 RocketMQ 的网络通信设计大堂,来认识和了解 RocketMQ 的通信是怎么设计的。
协议设计与编解码
协议设计
当客户端与服务端在通信时,它们之间肯定是以某种约定好的协议进行交互的,不然你传给我东西,而我却不知道收到的东西是一个怎样的一个结构,那么这东西我就算拿到了也不知道怎么解析并且获取自己想要的,所以呢,RocketMQ 就按照自己约定好的协议进行通信,下面我们来看看这个约定好的数据的一个结构与设计:
(1) 消息长度:总长度,四个字节存储,占用一个 int 类型;
(2) 序列化类型 &消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度;
(3) 消息头数据:经过序列化后的消息头数据;
(4) 消息主体数据:消息主体的二进制字节数据内容;
其中消息头数据是序列化的 JSON 数据,内容包括了以下字段:
编解码
编码
编码其实就是将传输数据需要的内存大小计算好,然后根据自定义的协议,将上面所讲的消息内容都写入到 ByteBuffer 中。
解码
解码就是跟编码反过来,从 ByteBuffer 中读取并解析出消息内容。
通信方式和流程
接下来,我们来看看 RocketMQ 中的网络通信方式与流程。
我们讲通信,那首先肯定是涉及到客户端和服务端吧?我们先来看看 RocketMQ 中用于远程通信的模块里一些核心类是怎样的一个关系。
RemotingService:一个顶层接口,定义了 start、shutdown、registerRPCHook 三个方法。
RemotingServer:RemotingService 的子接口,有着自己的方法。
RemotingClient:RemotingService 的子接口,有着自己的方法。
NettyRemotingServer:RemotingServer 的具体实现类。
NettyRemotingClient:RemotingClient 的具体实现类。
NettyRemotingAbstract:重复代码的抽象封装。
我们可以看到,在 RocketMQ 的远程通信模块中,定义了一个顶层接口,包含了启动、关闭、注册钩子(一般重要的核心流程方法要提供钩子方法,以便于扩展)三个方法,然后服务端和客户端分别有自己的接口,分别继承于顶层接口,除了有顶层接口的三个方法之外,服务端和客户端分别定义了自己的抽象接口,其他的实现类以及内部类都是基于 Netty 去实现各自的功能。
那么我们来看下通信的请求过程是怎样的,我们看下图:
首先请求会被 NettyRemotingServer 的内部类 NettyServerHandler 进行处理,最后会执行 processRequestCommand 方法,在 processRequestCommand 方法中,会根据请求的编码 code 去本地的一张哈希映射表中查询对应的 Pair 对象,Pair 对象包含了对应的处理器和对应的线程池,然后将对应的处理器封装成一个新的线程任务提交至线程池中处理,期间处理器会执行 processReqeust 方法,最后将执行的处理结果返回值通过 Netty 的 ChannelHandlerContext 对象写回。
Reactor 多线程模型
上面我们了解了通信方式和流程,下面我们来看看 RocketMQ 的多线程模型,RocketMQ 的通信选择了 Netty 作为底层通信库,那么自然也就遵循了 Reactor 多线程模型,那么 RocketMQ 是怎么设计的呢?
其实跟 Netty 的 Reactor 多线程模型差不多,首先是 Reactor 主线程负责监听网络连接请求,然后建立连接,建立好连接后丢给 Reactor 线程池,它负责将建立好连接的 socket 注册到 selector(这里有两种方式,NIO 和 Epoll,根据系统适配,也可自主手动配置),开始监听网络数据,如果有网络数据则丢给 Worker 线程池,然后将 SSL 验证、编解码、空闲检查、网络连接管理这些工作交给 Netty 的 ChannelPipeline 以责任链的形式进行处理,最后业务逻辑将交给业务线程池去处理执行对应的 processor 即处理器,处理完成后进行结果的回写。
实际上我们可以看出 RocketMQ 的通信多线程模型也是按照分治以及事件驱动的设计思想去实现的,将任务进行拆解,分而治之,然后各种事件的处理按照事件类型进行分开处理,在平时的系统设计中,设计思想很重要,一个好的设计可以让系统将性能达到一个很高的标准,平时我们要多从一些好的软件系统中去学习一些优秀的设计,当我们遇到同样的问题的时候便可以利用学过的设计来处理对应的问题。
小结
好了,RocketMQ 的网络通信机制我们就学习到这,我们来做个小结吧。
首先我们从 Linux 的 I/O 基础模型开始,到 Java NIO,再到 Netty 的线程模型,初步了解了 I/O 的一个模型的演变,最后我们对 RocketMQ 的通信设计进行了学习,学习了 RocketMQ 的通信自定义协议以及它的一个解编码,然后学习了 RocketMQ 通信的一个方式和流程,最后我们学习了基于 Netty 的多线程模型的 RocketMQ 的一个 Reactor 多线程模型,让我们对 RocketMQ 的设计加深了理解,以上就是我们今天学习的内容,后续细节上的东西我们将在后续的源码解析阶段进行详细的跟踪与学习。
好了,今天我们就学习到这里,下一次我们将学习 RocketMQ 的高性能文件读写,一起了解 RocketMQ 的文件读写机制。
版权声明: 本文为 InfoQ 作者【白裤】的原创文章。
原文链接:【http://xie.infoq.cn/article/27b7f8a73018d9a0fc9d50a83】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论