本篇文章主要讨论RocketMQ中网络通信的具体实现。对于一个消息中间件来说,它的作用一般会有
阅读提示:文章篇幅较长,源码解析有点多,如果对细节不感兴趣可以跳过源码部分,或者可以按照我上一篇博客搭建源码阅读环境。地址:RocketMQ源码调试环境搭建
还是看图说话比较好,在没有MQ解耦的系统中,一次简单的RPC系统调用如下所示:
在加入了MQ后,系统调用会如下所示:
原先的1次RPC会变成目前的“2”次RPC。这里的2之所以打引号是因为从系统边界来看是两次RPC,但是对于MQ本身来说,会附加上很多MQ内部需要的网络调用。
如果再具体一点,就会如下所示:
上图中的broker就是MQ中的服务端,负责消息的转储和分发。一般来说MQ分为有broker和无broker两种设计,比如kafka,actimemq,rabbitmq以及RocketMQ就是有broker设计,而类似zeroMQ和AKKA就是无broker设计,我个人觉得后者更偏向于一种带有消息范式的编程模型(没有深入研究,有偏差的地方欢迎指正讨论)。
数据的传输在MQ中都是以这种RPC数据流的方式进行传输,所以一个良好的网络通信设计在MQ中非常重要。
那么,对于一个中间件的RPC网络通信来说,到底需要哪些设计要素才能满足它的需求?
我认为下面几点是必须要满足的。
老规矩,看图说话,先上一张UML图先(RocketMQ和大多数中间件一样,使用了著名的Netty作为网络通讯框架):
RemotingService:以RemotingService为最上层接口,提供了
三个接口:
void start();
void shutdown();
void registerRPCHook(RPCHook rpcHook);
NettyRemotingAbstract:netty处理的抽象类,封装了netty处理的公共方法,比如下面这一段对消息的总体处理:
RemotingClient/RemotingSever:这两个接口继承了最上层接口,同时提供了client和server所必需的方法,下面这个就是RemotingClient的方法:
public RemotingCommand invokeSync(final String addr, final RemotingCommand request,
final long timeoutMillis) throws InterruptedException, RemotingConnectException,
RemotingSendRequestException, RemotingTimeoutException;
public void invokeAsync(final String addr, final RemotingCommand request, final long timeoutMillis,
final InvokeCallback invokeCallback) throws InterruptedException, RemotingConnectException,
RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException;
public void invokeOneway(final String addr, final RemotingCommand request, final long timeoutMillis)
throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException,
RemotingTimeoutException, RemotingSendRequestException;
public void registerProcessor(final int requestCode, final NettyRequestProcessor processor,
final ExecutorService executor);
public boolean isChannelWriteable(final String addr);
BrokerOuterAPI:是broker和别的模块通信的类,封装了NettyRemotingClient。
MQClientImpl:是客户端和broker与nameserver通信的类,也封装了NettyRemotingClient。
UML图下方紫色区域都是对编码解码和事件处理的一些类,没有全部罗列出来,其中RemotingCommand非常重要,是所有传输数据的封装,下面会详细讲解。
作为网络通信模块,协议设计和编码解码是最基础和重要的,在介绍具体的网络协议设计前,我觉得应该把RemotingCommand的具体内容详细说明一下,这个类是传输过程中对所有数据的封装,不但包含了所有的数据结构,还包含了编码解码操作。
RemotingCommand:
private static final int RPC_TYPE = 0; // 0, REQUEST_COMMAND rpc类型的标注,一种是普通的RPC请求
private static final int RPC_ONEWAY = 1; // 0, 这种ONEWAY 是指单向RPC,比如心跳包
private static final Map, Field[]> clazzFieldsCache =
new HashMap, Field[]>();//**CommandCustomHader**是所有headerData都要实现的接口,后面的Field[]就是解析header所对应的成员属性,所以这个map就是解析时候的字段缓存,下面两个map也是分别对应类名缓存和注解缓存。
private static final Map canonicalNameCache = new HashMap();
// 1, RESPONSE_COMMAND
private static final Map notNullAnnotationCache = new HashMap();
private static AtomicInteger requestId = new AtomicInteger(0);//这里的requestId是RPC请求的序号,每次请求的时候都会increment一下,同时后面会讲到的responseTable会用这个requestId作为key。
private int code;//这里的code是用来区分request类型的
private LanguageCode language = LanguageCode.JAVA;//区分语言种类
private int version = 0;//RPC版本号
private int opaque = requestId.getAndIncrement();//这里的opaque就是requestId
private int flag = 0;//区分是普通RPC还是onewayRPC得标志
private String remark;//标注信息
private HashMap extFields;//存放本次RPC通信中所有的extFeilds,extFeilds其实就可以理解成本次通信的包头数据
private transient CommandCustomHeader customHeader; //包头数据,注意transient标记,不会被序列化
private transient byte[] body; //body数据,注意transient标记,不会被序列化
重要的成员变量都在上面了,给大家展现一下一次心跳注册的报文:
[
code=103,//这里的103对应的code就是broker向nameserver注册自己的消息
language=JAVA,
version=137,
opaque=58,//这个就是requestId
flag(B)=0,
remark=null,
extFields={
brokerId=0,
clusterName=DefaultCluster,
brokerAddr=125.81.59.113: 10911,
haServerAddr=125.81.59.113: 10912,
brokerName=LAPTOP-SMF2CKDN
},
serializeTypeCurrentRPC=JSON
]
上面这个截图是RocketMQ代码里自带的注释,传输内容主要分为4部分内容:
OK,来具体看一下具体怎么实现的,先看encode编码:
headerEncode()首先将extField放入到这个对象的feildMap中(上面有写过),然后将这个RemotingCommand序列化成byte[]字节数组,序列化使用的是阿里的fastJson:
其中的markProtocolType方法是将RPC类型和headerData长度编码,放到一个byte[4]数组中,实现的比较巧妙。
(当然也有可能是我接触位运算很少,可能C语言里面这种设计很常见)
OK,说完了编码,再看看解码:
解码就是编码的一个逆向流程:
现在编码解码就先到此为止,下面来看RPC的事件处理
NettyRemotingAbstract这个抽象类包含了很多公共数据处理,也包含了很多重要的数据结构,先介绍一下NettyRemotingAbstract的成员属性。
NetttyRemotingAbstract:
//单向RPC信号量,控制线程个数
protected final Semaphore semaphoreOneway;
//异步RPC信号量,控制线程个数
protected final Semaphore semaphoreAsync;
//responseTable,存放异步请求的ResponseFuture
protected final ConcurrentHashMap responseTable =
new ConcurrentHashMap(256);
//processorTable,存放注册的processor,key是request Code,对应的是
protected final HashMap> processorTable =
new HashMap>(64);
//netty事件处理内部类
protected final NettyEventExecuter nettyEventExecuter = new NettyEventExecuter();
//默认的事件处理器,处理一些公共的消息
protected Pair defaultRequestProcessor;
public NettyRemotingAbstract(final int permitsOneway, final int permitsAsync) {
this.semaphoreOneway = new Semaphore(permitsOneway, true);
this.semaphoreAsync = new Semaphore(permitsAsync, true);
}
//添加ChannelEvent接口
public abstract ChannelEventListener getChannelEventListener();
//添加NettyEvent事件
public void putNettyEvent(final NettyEvent event) {
this.nettyEventExecuter.putNettyEvent(event);
}
先讲一下业务层的事件处理:
首先上述的processorTable负责存储各个requestCode对应的processor,RemotingServer和RemotingClient中都有
void registerProcessor(final int requestCode, final NettyRequestProcessor processor,
final ExecutorService executor);
这个接口,在注册的时候会把processor和对应的request Code 装载进processorTable中,在处理事件时就会去processorTable中去取对应的processor:
然后再讲一下对几种通信方式的处理:
1、通过调用invekeSyncImpl来发出请求,此时会创建一个responseFuture,并put到responseTable中,key是opaque,
2、请求到达server端后会进行对应处理,然后回写response
3、根据opaque取出对应的responseFuture并把response放进去。
异步消息:
在同步消息的基础上会检测remotingCommand是否有回调函数,如果有会执行回调函数。
Tips:
小的设计技巧:
1、通过countDownLatch来控制等待网络通信时间
2、通过两个AtomicBoolean的CAS方法来控制RPC只执行了一次
这里的编码解码分别使用了
Netty里面的MessageToByteEncoder和LengthFieldBasedFrameDecoder进行编码解码,这一对工具是比较常用的编解码方式,具体实现和原理我这里就不细说了。
Netty事件处理最重要的两个类就是
前者继承自ChannelDuplexHandler,可以监控connect,disconnect,close等事件,每个事件过来存入NettyEventExecuter的队列里面。
NettyEventExecuter是一个线程,不停地从队列里面取出事件进行相应处理。
class NettyEventExecuter extends ServiceThread {
private final LinkedBlockingQueue eventQueue = new LinkedBlockingQueue();
private final int maxSize = 10000;
public void putNettyEvent(final NettyEvent event) {
System.out.println("put event: "+event.getType());
if (this.eventQueue.size() <= maxSize) {
this.eventQueue.add(event);
} else {
plog.warn("event queue size[{}] enough, so drop this event {}", this.eventQueue.size(), event.toString());
}
}
@Override
public void run() {
plog.info(this.getServiceName() + " service started");
final ChannelEventListener listener = NettyRemotingAbstract.this.getChannelEventListener();
while (!this.isStoped()) {
try {
NettyEvent event = this.eventQueue.poll(3000, TimeUnit.MILLISECONDS);
if (event != null && listener != null) {
switch (event.getType()) {
case IDLE:
listener.onChannelIdle(event.getRemoteAddr(), event.getChannel());
break;
case CLOSE:
listener.onChannelClose(event.getRemoteAddr(), event.getChannel());
break;
case CONNECT:
listener.onChannelConnect(event.getRemoteAddr(), event.getChannel());
break;
case EXCEPTION:
listener.onChannelException(event.getRemoteAddr(), event.getChannel());
break;
default:
break;
}
}
} catch (Exception e) {
plog.warn(this.getServiceName() + " service has exception. ", e);
}
}
plog.info(this.getServiceName() + " service end");
}
详细阅读RocketMQ的过程中收获了很多关于网络通信设计的知识,其中对于netty的使用和消息的设计让我收益很多,当然对于MQ来说,网络设计并没有真正的RPC框架那么复杂,不需要考虑很多第三方调用问题和并发量问题,因为瓶颈一般不会卡在网络这一层,不管怎么说还是学习到了很多。由于本人水平有限,如果读者有什么问题或者文章哪里有错误的地方,欢迎大家指正和探讨,我的邮箱 [email protected]。