• RocketMQ详解(三)核心设计原理(待完善)


    引子

    本节参照github设计文档+源码,分析RocketMQ的核心设计原理。从几个方面去分析:消息存储、通信机制、负载均衡、事务消息。

    一、broker消息存储

    本节揭露broker上消息存储的真实目录结构+存储方式。

    1.1 目录、文件结构

     有3个比较重要的文件:

    • commitLog消息日志:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,
    • consumequeue逻辑消费队列:存储了commitLog的起始物理offset,目的是提高消息消费的性能。
    • indexFile索引文件:提供了一种可以通过key或时间区间来查询消息的方法。

    1.2 consume Queue目录结构

    consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;

    1.3.消息存储整体架构图

    如上图(全网唯一最佳图片,优化自官方文档),RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。

    核心步骤:

    1.首先,生产者根据topic发送消息,消息存储在commitLog中,1G一个文件。当文件满了,写入下一个文件;

    2.其次,ReputMessageService重写消息服务执行2个分发操作

      1)创建ConsumerQueue逻辑消费队列:参数:commitLogOffset 物理偏移量、msgSize 消息长度、tagsCode tag哈希。可以看成是基于topic的commitlog索引文件

      2)创建IndexFile索引文件:以创建时的时间戳命名。参数:phyOffset物理偏移量(=commitLogOffset)、keys.(注意上图中每个索引数据包含4部分:key hash、commitLogOffset、timestamp diff、preIndexOffset,其中最后一个参数,官网写成了NextIndex offset,分析源码后发现是错的,slot 的值=index个数,查询时从最后一个index开始倒序查询,next index offset由此而来。例如一个槽有3个节点,3->2->1这样倒序遍历节点。在节点3中记录的就是3),

    3.最后,消费者根据topic、tag拉取消息消费;根据key查询消息。

    1.4 消息查询

     RocketMQ支持按照下面两种维度进行消息查询:

    1.按照Message Id查询消息

    RocketMQ中的MessageId的长度总共有16字节,其中包含了消息存储主机地址(IP地址和端口),消息Commit Log offset。“按照MessageId查询消息”在RocketMQ中具体做法是:

    • 1)Client端从MessageId中解析出Broker的地址(IP地址和端口)和Commit Log的偏移地址后封装成一个RPC请求后通过Remoting通信层发送(业务请求码:VIEW_MESSAGE_BY_ID)。
    • 2)Broker端走的是QueryMessageProcessor,读取消息的过程用其中的 commitLog offset 和 size 去 commitLog 中找到真正的记录并解析成一个完整的消息返回。

    2.按照Message Key查询消息

     主要是基于RocketMQ的IndexFile索引文件来实现的。结构如下图:

    indexFile的具体结构,主要包含三部分:索引头Header、槽位表SlotTable(500W个槽位)、索引链表index list(2000W个索引数据),如下图:

    如上图所示,主要通过Broker端的QueryMessageProcessor业务处理器来查询,读取消息的过程

    • 1.找槽位=40byte  +hash(topic + "#" + key)  %500W*4byte   ,槽位值slotValue=最新插入index的位置
    • 2.遍历单向链表:从slotValue找到最新index在整个索引文件中位置=40byte  +500w*4byte  + slotValue*20byte,然后根据单个索引文件的pre index值找到前一个索引,一直遍历下去。index数据中key hash和时间区间都满足,则匹配。添加到 List<Long>  phyOffsets(commitLog的偏移量list)中。最终根据其中的commitLog offset从CommitLog文件中读取消息的实体内容。

    二、通信机制

    2.1 通信架构图

     

    RocketMQ消息队列集群主要包括NameServer、Broker(Master/Slave)、Producer、Consumer4个角色,基本通讯流程如下:

    (1) Broker启动后需要完成一次将自己注册至NameServer的操作;随后每隔30s时间定时向NameServer上报Topic路由信息。

    (2) 消息生产者Producer作为客户端发送消息时候,需要根据消息的Topic从本地缓存的TopicPublishInfoTable获取路由信息。如果没有则更新路由信息会从NameServer上重新拉取,同时Producer会默认每隔30s向NameServer拉取一次路由信息。

    (3) 消息生产者Producer根据2)中获取的路由信息选择一个队列(MessageQueue)进行消息发送;Broker作为消息的接收者接收消息并落盘存储。

    (4) 消息消费者Consumer根据2)中获取的路由信息,并再完成客户端的负载均衡后,选择其中的某一个或者某几个消息队列来拉取消息并进行消费。

    从上面1)~3)中可以看出在消息生产者, Broker和NameServer之间都会发生通信(这里只说了MQ的部分通信),因此如何设计一个良好的网络通信模块在MQ中至关重要,它将决定RocketMQ集群整体的消息传输能力与最终的性能。

    为了实现客户端与服务器之间高效的数据请求与接收,RocketMQ-remoting包自定义了通信协议并在Netty的基础之上扩展了通信模块。

    2.2 Remoting通信类图

     

     如上图所示,

    RemotingService远程通信服务顶级接口:定义了启动、关闭、钩子注册3个方法。

    RemotingClient远程通信客户端服务接口:继承RemotingService,拓展了获取/更新namesever地址、同步、异求、单向3种请求、注册请求处理器、添加回调执行器等方法。

    RemotingServer远程通信服务端接口:继承RemotingService,拓展了注册请求处理器、获取<请求处理器,执行器>、同步、异求、单向3种请求等方法。

    NettyRemotingAbstract netty远程服务抽象类:封装了获取通道事件监听器、添加netty event至执行器、处理消息接收、处理请求命令、处理返回命令、获取RPC钩子、获取回调执行器、同步、异步、单向三种请求实现方法。

    NettyRemotingClient netty远程通信客户端类:继承自NettyRemotingAbstract 实现了RemotingClient接口,复写同步、异求、单向3种请求,拓展了关闭通道方法。

    NettyRemotingServer netty远程通信服务端类:继承自NettyRemotingAbstract 实现了RemotingServer接口,复写同步、异求、单向3种请求。

    2.3 通信流程

    有三种请求方式同步、异步、单向。其中异步最复杂,我们就分析异步通信流程,如下图:

    如上图所示分四大块:

    1)NettyRemotingClient 【远程通信客户端】发送请求消息

    2)NettyServerHandler 【服务端处理器】接收消息处理器

    3)NettyRemotingServer【远程通信服务端】执行命令,发送返回消息

    3)NettyClientHandler【客户端处理器】接收消息处理器

    2.4 源码剖析

    我们以【生产者发送消息给broker->broker接收->消费者从broker拉取消息消费;】这个过程来剖析完整通信流程,如下图:

    2.4.1.发送消息-远程通信客户端

    发送消息分同步、异步、单向三种请求类型,这里我们以异步请求为例,分析如下图:

    如上图所示,发送消息主流程如下:

    1.DefaultMQProducer.send(): 发送消息入口

    2.NettyRemotingClient.invokeAsync() :Netty远程通信客户端,执行异步请求

    3.Netty 通信模块 channel.writeAndFlush(): 往通道中写数据

    2.4.2 消费消息-远程通信客户端

    消费消息流程需要从消费者启动开始看,consumer启动时执行了MQClientInstance.start()启动客户端实例。如下图所示:

    总结:

    不管生产者还是消费者,通信模型一致,都是调用NettyRemotingClient远程通信客户端(RemotingClient接口)实现通信。

    2.4.3 远程通信服务端(接收消息并处理)

     前面第二节讲broker启动时,会执行NettyRemotingServer.start()启动远程通信服务端,内部使用ServerBootstrap类构建初始化netty channel,并自定义。

    ===========参数解释============

    ChannelOption通道参数类中定义的参数,其中SO开头代表socket参数,TCP代表TCP参数,IP代表IP参数。

      1、Integer ChannelOption.SO_BACKLOG 待处理队列长度

        对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,

        服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小

      2、Boolean ChannelOption.SO_REUSEADDR 允许覆重复使用本地地址和端口,对应于套接字(socket)选项中的SO_REUSEADDR。

        比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,

        比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。

      3、Boolean ChannelOption.SO_KEEPALIVE 是否开启周期存活嗅探

         对应于套接字(socket)选项中的SO_KEEPALIVE,设置为true,TCP会实现监控连接是否有效,当连接处于空闲状态的时候,超过了2个小时,本地的TCP实现会发送一个数据包给远程的 socket,如果远程没有发回响应,TCP会持续尝试11分钟,一直到         响应为止,如果在12分钟的时候还没响应,TCP尝试关闭socket连接。

      4、Integer ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF 发送接收缓存区大小,默认65535

        对应于套接字选项中的SO_SNDBUF,SO_RCVBUF。这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。

      5、Integer ChannelOption.SO_LINGER 关闭时先发送数据完全

        对应于套接字选项中的SO_LINGER,可以阻塞close()的调用时间,直到数据完全发送。

      6、Boolean ChannelOption.TCP_NODELAY 禁止延迟发送

        参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网    络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

    =======================

    上图中的最后一个参数就是NettyServerHandler继承SimpleChannelInboundHandler简单通道入站处理器,复写channelRead0方法,如下图:

    1 @ChannelHandler.Sharable
    2     class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> {
    3 
    4         @Override
    5         protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
    6             processMessageReceived(ctx, msg);
    7         }
    8     }

    最终调用processMessageReceived()处理消息接收方法。

     

     如上图所示,最终根据请求方式(同步/异步)调用processRequest()方法.处理请求。该方法实际是NettyRequestProcessor接口定义的,实现类有:SendMessageProcessor发送消息、PullMessageProcessor拉取消息,等等。

    四、负载均衡

     

    五、事务消息(半消息事务)

    =======参考==========

    https://github.com/apache/rocketmq/blob/master/docs/cn/design.md

    RocketMQ之消息查询IndexFile(四)

    ------------------个人能力有限,大家多交流,一起壮哉我大JAVA!------------------

    如果你觉得本文对你有点帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!

  • 相关阅读:
    MySQL InnoDB事务隔离级别脏读、可重复读、幻读
    数据结构与算法-Python/C
    Go语言 转至Nick老师博客
    短信验证功能、邮箱验证功能
    psutil模块
    简单邮件传输协议SMTP
    CSS3主要的几个样式笔记
    cURL的几个经典实例
    Socket进程通信机制
    对PDO的认识
  • 原文地址:https://www.cnblogs.com/dennyzhangdd/p/15035116.html
Copyright © 2020-2023  润新知