RocketMQ源码解析-消息是如何写入Broker服务器(Broker端篇)

RocketMQ源码解析-消息是如何写入Broker服务器(客户端篇)

上一篇文章主要是讲到了一条普通的消息要发送的话,在客户端也就是Producer经历了哪些流程,处理了哪些数据。在经过客户端的处理后,这一条消息被编码为了字节序列,沿着网络向着Broker服务端进发了。本篇主要是以源码的方式讲解,在这条消息到达Broker服务器后,又会经历哪些流程或者操作,才能顺利的写入到本地文件commitlog中保存起来,真正的完成消息的写入。

先从最底层的网络处理说起,RocketMQ的网络通信框架采用是Netty,并且为了让Netty更加贴合项目的使用,对Netty进行了很多的封装,使用起来也更加的简洁方便。

Broker的初始化以及启动流程这篇文件中提到过,NettyRemotingServer这个类是对服务端网络服务的抽象,当调用NettyRemotingServer的start方法时,就是启动了服务端的网络服务,以下这段代码就是基于Netty实现的服务端网络处理程序。 其余的配置项暂时先不关注,我们只需要关注NettyDecoder,本篇文章的起始点就从这里说起。

image.png

NettyDecoder类是RocketMQ的解码器,实现了Netty的LengthFieldBasedFrameDecoder,代表了它是一个基于长度标识的解码器,正好对应到客户端在编码时指定了请求字节的总体长度。待解码器解码到了一个完成的请求报文时,会将对应的报文数据传入到RemotingCommand的decode,开始将这个完整的数据报文按照自定义协议进行拆分。

image.png

RemotingCommand的decode方法就是按照自定义的协议对byteBuffer进行拆解,对报文的解码对应到对报文的编码即可,最终得到一个RemotingCommand对象,这个对象就封装了本次的请求,后续基于此对象进行业务处理。

解码完成之后,基于Netty框架的处理流程,继续进行Inbound事件的传播。

image.png

在继续进行Inbound事件的传播,会到达NettyServerHandler,NettyServerHandler这个类是RocketMQ业务逻辑处理的入口类,这个类将请求RemotingCommand带入到了核心业务逻辑处理层次。

扫描二维码关注公众号,回复: 14486840 查看本文章

image.png

image.png

根据我们的请求类型为REQUEST_COMMAND,会进入到针对请求处理的方法processRequestCommand, 这个方法主要做了如下两件事情:

1.根据请求的code值获取对应的处理器以及处理器对应的线程池,此处应该是SendMessageProcessor处理器

2.将RemotingCommand、本次请求对应Netty的channel、以及Runnable对象进行封装,将RequestTask放入处理器对应的线程池。

image.png

分析一下Runnable做的事情。

定义了一个回调方法,这个回调方法用于将响应的数据写回到客户端去。

根据定义的请求处理器属于不同的类型调用不同的请求处理方法。SendMessageProcessor的asyncProcessRequest和processRequest在内部采用的了CompletableFuture进行实现,这两个方法的区别是SendMessageProcessor对应的线程池的线程等待CompletableFuture的结果,然后回调callback方法写回客户端。asyncProcessRequest不在依赖SendMessageProcessor对应线程池的线程回写结果,而是采用新的线程回写结果,是完全的异步化。

image.png

继续跟进asyncProcessRequest,这个方法首先从RemotingCommand中提取到SendMessageRequestHeader,然后根据消息类型是批量还是单条走不同的逻辑分支,此处将会执行asyncSendMessage,单条消息处理逻辑。

image.png

继续跟进asyncSendMessage方法,这个方法除了提供流程处理的前置校验外,就是围绕SendMessageRequestHeader来为MessageExtBrokerInner的属性赋值。一些列的属性赋值完成后,消息写入的流程就进入到了RocketMQ的存储层面,进入了DefaultMessageStore的处理方法asyncPutMessage。这里可以看到RocketMQ也是采用分层处理的设计,每一层负责处理的内容都是不同的,这样设计显得整个逻辑上比较清晰,后续定位排查问题也是很方便的。

image.png

跟进DefaultMessageStore的处理方法asyncPutMessage。因为准备要写入消息了,所以先校验Broker的状态,后续的checkMessage以及CheckLMqMessage方法我个人感觉应该放入到上一层的方法去做,放在此处做有些不合适。

做完校验之后,则会调用commitlog的asyncPutMessage方法。commitlog这个类是commitlog文件在软件层面的抽象,利用这个类来实现commitlog文件的管理,这个类的有一个属性MappedFileQueue,MappedFileQueue是由一组MappedFile构成的,一个MappedFile在此处代表了一个commitlog文件,而MappedFileQueue就代表了一组MappedFile,也就是这个RocketMQ进程所管理的所有commitlog文件。

image.png

commitlog的asyncPutMessage方法篇幅较长,总体来说,主要做了以下几件事:

1.继续对MessageExtBrokerInner设置相关的属性信息。

2.将消息追加写入commitlog文件,涉及到加锁进行顺序写操作。

3.写入的消息进行刷盘以及执行主从同步操作(主从模式的部署方式)。

关注一下 putMessageThreadLocal.getEncoder().encode(msg),这个处理是将Message的相关信息写入到ByteBuf(Netty封装),是一个预写入的操作,后续写入文件也是ByteBuffer(JDK的封装) 形式调用相关API。

image.png

进入到MappedFile的appendMessagesInner方法,获取消息将要写入的目标字节Buffer,并设置好写入的开始点position,消息写完之后,需要更新wrotePosition,代表消息的写指针,这个写指针是相对的,相对于此MappedFile的写指针。每一次MappedFile都拥有wrotePosition、committedPosition、flushedPosition,也就是写到堆外内存的指针、提交到操作系统pagecache的指针、刷到磁盘的指针。当不需要使用transientStorePool时,只使用wrotePosition、flushedPosition。

image.png

doAppend方法继续处理写入流程,这个方法中可以看到在追加写入commitlog文件时需要至少保留8个字节的空间,代表文件写完了,需要新起一个文件继续写,用END_OF_FILE状态表示文件已经写完。

剩余的写入操作就是填补之前预写入操作没有写入的数据,包括队列的偏移量、文件的物理偏移量等信息。

byteBuffer.put(preEncodeBuffer),将存储数据的ByteBuffer写入到MappedFile对应的ByteBuffer,也可以表述为写入当前commitlog表示的ByteBuffer。

这个方法执行完毕就表示从客户端发来的消息已经写入到Broker服务器中了,剩余的就是将这条消息以自定义的刷盘方式刷新到磁盘上以及执行主从同步的流程。

image.png

这个是commitlog的asyncPutMessage方法最后的执行步骤,一个方法submitFlushRequest是执行刷盘的策略,一个方法是submitReplicaRequest是执行主从同步的操作。

这两个方法基于CompletableFuture来实现,当前线程不必等待这两个操作执行完即可返回,后续的结果会进行异步的通知回调,优化了RocketMQ的消息写入流程。

image.png

关于消息是如何写入到Broker服务器(Broker端篇)已经梳理完成,从这一些列的流程中我们可以看出RocketMQ在提示写入性能方面做了很多的工作,也为我们优化自己的程序提供了很多可以借鉴的地方。

在写入完commitlog文件之后,RocketMQ是如何构建ConsumeQueue以及index文件的,后续会梳理一下,非常感谢。

猜你喜欢

转载自juejin.im/post/7132086199778279461