RocketMQ源码笔记(学习更新中)

RocketMQ源码笔记

本文是学习 “RoecketMQ技术内幕” 时的学习笔记,中间会贴上部分dubug源码的调试结果。

2 NameServer

2.1 NameServer 架构设计

Broker消息服务器在启动时,向所有NameServer注册,Producer在发送消息之前从NameServer获取broker服务器地址列表,然后根据负载均衡算法从列表中选择一台消息服务器进行消息发送。NameServer和每台broker服务器保持长连接,每隔30s检测broker是否存活,如果监测到宕机则从路由注册表中将其移除。但是路由变动不会立刻通生产者,为了降低NameServer实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用。

NameServer彼此之间互不通信,NameServer服务器之间在某一时候的数据并不会完全相同。

2.2 NameServer启动流程

2.2.1:装载配置文件

        final NamesrvConfig namesrvConfig = new NamesrvConfig();
        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        nettyServerConfig.setListenPort(9876);
        if (commandLine.hasOption('c')) {
            String file = commandLine.getOptionValue('c');
            if (file != null) {
                InputStream in = new BufferedInputStream(new FileInputStream(file));
                properties = new Properties();
                properties.load(in);
                MixAll.properties2Object(properties, namesrvConfig);
                MixAll.properties2Object(properties, nettyServerConfig);

                namesrvConfig.setConfigStorePath(file);

                System.out.printf("load config properties file OK, %s%n", file);
                in.close();
            }
        }

启动的时候,创建 NamesrvConfig 和 NettyServerConfig ,如果命令行有 -c 参数(加载配置文件),则读入参数,并把Netty 服务器在9876端口启动。

NamesrvConfig:源码

// rocketmq主路径
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
// 存储KV配置属性的持久化路径
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
// 默认config文件路径,不生效
private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";

NettyServerConfig:配置

RocketmqNamesrvConsole - listenPort=9876
// serverWorkerThreads: Netty业务线程池线程个数
RocketmqNamesrvConsole - serverWorkerThreads=8
// Netty public任务线程池线程个数, Netty 网络设计,根据业务类型会创建不同的线程池,比如处理消息发送、消息消费、心跳检测等。如果该业务类型(RequestCode)未注册线程池, 则由 public线程池执行。
RocketmqNamesrvConsole - serverCallbackExecutorThreads=0
// IO线程池线程个数,主要是NameServer、Broker端解析请求、返回相应的线程个数,这类线程主要是处理网络请求的,解析请求包,然后转发到各个业务线程池完成具体的业务操作,然后将结果再返回调用方 。
RocketmqNamesrvConsole - serverSelectorThreads=3
// serverOnewaySemaphoreValue: send oneway 消息请求井发度(Broker端参数) 。
RocketmqNamesrvConsole - serverOnewaySemaphoreValue=256
// serverAsyncSemaphoreValue: 异步消息发送最大并发度( Broker 端参数) 。
RocketmqNamesrvConsole - serverAsyncSemaphoreValue=64
// serverChannelMaxldleTimeSeconds:网络连接最大空闲时间,默认120s。如果连接空闲时间超过该参数设置的值,连接将被关闭 。
RocketmqNamesrvConsole - serverChannelMaxIdleTimeSeconds=120
// serverSocketSndBufSize:网络 socket发送缓存区大小, 默认 64k。
RocketmqNamesrvConsole - serverSocketSndBufSize=65535
// serverSocketRcvBufSize:网络 socket接收缓存区大小 ,默认 64k。
RocketmqNamesrvConsole - serverSocketRcvBufSize=65535
// serverPooledByteBufAllocatorEnable: ByteBuffer是否开启缓存,建议开启 。
RocketmqNamesrvConsole - serverPooledByteBufAllocatorEnable=true
// useEpollNativeSelector: 是否启用EpollIO模型,Linux环境建议开启。
RocketmqNamesrvConsole - useEpollNativeSelector=false

2.2.2 : 启动和初始化NamesrcController

    public boolean initialize() {
        this.kvConfigManager.load();
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
        this.remotingExecutor =
            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
        this.registerProcessor();
      // 下面两个定时任务都称为心跳检测
      // 每隔十秒扫描不活跃的 broker 然后移除
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);
			// nameServer 每隔10分钟打印一次 KV 配置
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                NamesrvController.this.kvConfigManager.printAllPeriodically();
            }
        }, 1, 10, TimeUnit.MINUTES);
        return true;
    }

2.2.3 :注册JVM钩子函数并启动服务,便于监听Broker,消息生产者的网络请求

        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));
        controller.start();

如果代码中使用了线程池,一种优雅的停机方式就是注册一个JVM钩子函数,在JVM进程关闭之前,先将线程池关闭,即时释放资源。

2.3:NaveServer路由注册,故障剔除

2.3.1 :路由元信息

NameServer路由实现类:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager

// topicQueueTable: Topic消息队列路由信息,消息发送时根据路由表进行负载均衡 。
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
// brokerAddrTable : Broker 基础信息,包含 brokerName所属集群名称 、                                                                                   主备Broker地址。
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
// clusterAddrTable: Broker 集群信息,存储集群中所有 Broker 名称 。
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
// brokerLiveTable: Broker 状态信息 。 NameServer 每次 收到心跳包时会 替换该信 息 。
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
// filterServerTable : Broker上的 FilterServer列表,用于类模式消息过滤
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
class BrokerLiveInfo {
    private long lastUpdateTimestamp; // 上次broker心跳时间,超过120s没跟新则移除broker路由信息
    private DataVersion dataVersion;
    private Channel channel;
    private String haServerAddr;

2.3.2 :路由注册

RMQ路由注册是通过Broker与NameServer的心跳功能实现的。Broker启动时向集群中所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,NameServer收到Broker心跳包时会更新 brokerLiveTable 缓存中 BrokerLiveInfo 的 lastUpdateTimestamp,然后NameServer每隔10s扫描 brokerLiveTable,如果连续120s没有收到心跳包,NameServer将移除该 Broker 的路由信息同时关闭 Socket 连接。

  1. Broker发送心跳包

    BrokerController #start

            this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
                @Override
                public void run() {
                    try {
                        BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                    } catch (Throwable e) {
                        log.error("registerBrokerAll Exception", e);
                    }
                }
            }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
    
    

    BrokerOuterAPI #registerBrokerAll

            if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
    
                for (final String namesrvAddr : nameServerAddressList) {
                    brokerOuterExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                                if (result != null) {
                                    registerBrokerResultList.add(result);
                                }
    
                                log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                            } catch (Exception e) {
                                log.warn("registerBroker Exception, {}", namesrvAddr, e);
                            } finally {
                                countDownLatch.countDown();
                            }
                        }
                    });
                }
    
                try {
                    countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                }
            }
    
    

    遍历NameServer列表,Broker消息服务器依次向NameServer发送心跳包

    BrokerOuterAPI #registerBroker (网络发送代码)

        private RegisterBrokerResult registerBroker(
            final String namesrvAddr,
            final boolean oneway,
            final int timeoutMills,
            final RegisterBrokerRequestHeader requestHeader,
            final byte[] body
        ) throws Exception {
            RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
            request.setBody(body);
    
            if (oneway) {
                try {
                    this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
                } catch (RemotingTooMuchRequestException e) { // Ignore }
                return null;
            }
    
            RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
    

    心跳包具体逻辑,首先封装请求包头(Header)

  2. NameServer处理心跳包

    RouteInfoManager #registerBroker clusterAddrTable

                    this.lock.writeLock().lockInterruptibly();
    
                    Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
                    if (null == brokerNames) {
                        brokerNames = new HashSet<String>();
                        this.clusterAddrTable.put(clusterName, brokerNames);
                    }
                    brokerNames.add(brokerName);
    

    路由 注册需要加写锁 ,防止并发修改 RoutelnfoManager 中的路由表 。首先判断 Broker 所属集群是否存在,如果不存在,则创建,然后将 broker 名加入到集群Broker 集合中。

    P40

4 RocketMQ消息存储

4.1 存储概要设计

RocketMQ主要存储的文件包括Commitlog文件,ConsumeQueue文件,IndexFile文件。RMQ把所有主题的消息存储在同一个文件中,确保消息发送时顺序写文件。为了提高效率引入了ConsumeQueue消息队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。IndexFile索引文件,其主要设计理念就是为了加速消息的检索性能,根据消息的属性快速从Commitlog文件中检索消息。

4.2 初识消息存储

消息存储实现类:org.apache.rocketmq.store.DefaultMessageStore,它是存储模块里面最重要的一个类,包含了很多对存储文件的操作API。

// 消息存储配置属性    
private final MessageStoreConfig messageStoreConfig;
// CommitLog 文件的存储实现类
private final CommitLog commitLog;
// 消息队列存储缓存表,按消息主题分组
private final ConcurrentMap<String/* topic */, ConcurrentMap<Integer/* queueId */, ConsumeQueue>> consumeQueueTable;
// ConsumeQueue刷盘线程
private final FlushConsumeQueueService flushConsumeQueueService;
// 清除CommitLog文件服务
private final CleanCommitLogService cleanCommitLogService;
// 清除ConsumeQueue文件服务
private final CleanConsumeQueueService cleanConsumeQueueService;
// 索引文件实现类
private final IndexService indexService;
// MappedFile分配服务
private final AllocateMappedFileService allocateMappedFileService;
// CommitLog消息分发,根据CommitLog文件构建ConsumeQueue,IndexFile文件
private final ReputMessageService reputMessageService;
// 存储HA机制
private final HAService haService;
// 消息堆内存缓存
private final TransientStorePool transientStorePool;
// 消息拉取长轮询模式消息达到监听器
private final MessageArrivingListener messageArrivingListener;
// Broker属性配置类
private final BrokerConfig brokerConfig;
// 文件刷盘监测点
private StoreCheckpoint storeCheckpoint;
// CommitLog文件转发请求
private final LinkedList<CommitLogDispatcher> dispatcherList;

4.3 消息发送存储流程

  • Step1:如果Broker停止工作或者为Slave,则拒绝消息写入。如果消息长度超过256个字符,消息属性长度超过65536个字符将拒绝改消息的写入。

  • Step2:如果消息的延迟级别大于0,将消息的原主题名称与原消息队列ID存入消息属性中,用延迟消息主题SCHEDULE_TOPIC、消息队列ID更新原先消息的主题与队列,这是并发消息消费重试关键的一步。

  • Step3:获取当前可以写入的Commitlog文件。每一个文件默认1G,一个文件写满后再创建另外一个,以该文件中第一个偏移量为文件名,偏移量小于20位用0补齐。第一个文件初始偏移量为0,第二个文件的偏移量是1073741824,代表该文件中的第一条消息的物理偏移量为1073741824,这样就能快速定位到消息。

  • Step4:在写入CommitLog之前,先申请putMessageLock,也就是将消息存储到CommitLog文件中是串行的。

  • Step5:设置消息的存储时间,如果mappedFile为空,表明 /commitlog 目录下不存在任何文件,说明本次消息是第一次消息发送,用偏移量0创建第一个 commit 文件。

    messageExtBatch.setStoreTimestamp(beginLockTimestamp);
    
                if (null == mappedFile || mappedFile.isFull()) {
                    mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
                }
                if (null == mappedFile) {
                    log.error("Create mapped file1 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString());
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
                }
    
    • Step6:将消息追加到 MappedFile 中。首先先获取 MappedFile 当前写指针,如果currentPos大于或等于文件大小则表明文件已写满,抛出UNKNOWN_ERROR。如果currentPos小于文件大小,通过 slice() 方法创建一个与MappedFile的贡献内存区,并设置position为当前指针。

              int currentPos = this.wrotePosition.get();
      
              if (currentPos < this.fileSize) {
                  ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
                  byteBuffer.position(currentPos);
                  AppendMessageResult result;
                  if (messageExt instanceof MessageExtBrokerInner) {
                      result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
                  } else if (messageExt instanceof MessageExtBatch) {
                      result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
                  } else {
                      return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
                  }
                  this.wrotePosition.addAndGet(result.getWroteBytes());
                  this.storeTimestamp = result.getStoreTimestamp();
                  return result;
              }
      
    • Step7:创建全局唯一消息ID,消息ID有16字节,消息ID前4字节是IP,中间4字节是端口号,最后8字节是消息偏移量。

      long wroteOffset = fileFromOffset + byteBuffer.position();
      
      this.resetByteBuffer(storeHostHolder, storeHostLength);
      
      String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(storeHostHolder), wroteOffset);
      

      但是为了消息ID可读性,返回给应用程序的msgId为字符类型,可以通过UtilAll.byte2string方法将msgId字节数组转换成字符串,通过UtilAll.string2bytes方法将msgId字符串还原成16个字节的字节数组,从而根据提取消息偏移量,可以快速通过msgId找到消息内容。

    • Step8:获取该消息在消息队列的偏移量,CommitLog中保存了当前所有消息队列的当前待写入偏移量。

                  // Record ConsumeQueue information
                  keyBuilder.setLength(0);
                  keyBuilder.append(msgInner.getTopic());
                  keyBuilder.append('-');
                  keyBuilder.append(msgInner.getQueueId());
                  String key = keyBuilder.toString();
                  Long queueOffset = CommitLog.this.topicQueueTable.get(key);
                  if (null == queueOffset) {
                      queueOffset = 0L;
                      CommitLog.this.topicQueueTable.put(key, queueOffset);
                  }
      
    • Step9:根据消息体的长度、主题的长度、属性的长度结合消息存储格式计算消息的总长度。

          protected static int calMsgLength(int sysFlag, int bodyLength, int topicLength, int propertiesLength) {
              final int msgLen = 4 //TOTALSIZE:该消息条目总长度,4字节
                  + 4 //MAGICCODE:魔数,4字节。固定值0xdaa320a7
                  + 4 //BODYCRC:消息体crc校验码,4字节
                  + 4 //QUEUEID:消息消息队列ID,4字节
                  + 4 //FLAG:消息FLAG,RMQ不做处理,供应用程序使用,默认4字节
                  + 8 //QUEUEOFFSET:消息在消息消费队列的偏移量,8字节
                  + 8 //PHYSICALOFFSET:消息在CommitLog文件中偏移量,8字节
                  + 4 //SYSFLAG:消息系统Flag,例如是否压缩,是否是事务消息等,4字节
                  + 8 //BORNTIMESTAMP:消息生产者调用消息发送API的时间戳,8字节
                  + bornhostLength //BORNHOST:发送者IP,端口,8字节
                  + 8 //STORETIMESTAMP:消息存储时间戳,8字节
                  + storehostAddressLength //STOREHOSTADDRESS:BrokerIP和端口,8字节
                  + 4 //RECONSUMETIMES:消息重试次数,4字节
                  + 8 //Prepared Transaction Offset:事务消息物理偏移量,8字节
                  + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY:消息体长度,4字节
                  + 1 + topicLength //TOPIC:主题存储长度,1字节
                  + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength:消息属性,长度为PropertiesLength中存储的值
                  + 0;
              return msgLen;
          }
      

      上述表明CommitLog条目是不定长的,每一个条目的长度存储在前4个字节中。

    • Step10:如果消息长度 + END_FILE_MIN_BLANK_LENGTH 大于 CommitLog文件的空闲空间,则返回AppendMessageStatus.END_OF_FILE,Broker会创建一个新的CommitLog文件来存储该消息。从这里可以看出,每个CommitLog文件最少会空闲8字节,高4字节存储当前文件剩余空间,低四字节存储魔数:CommitLog.BLANK_MAGIC_CODE。

      final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
      // Write messages to the queue buffer
      byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
      
      AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId, msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
      
    • Step11:将消息内容存储到ByteBuffer中,然后创建AppendMessageResult。把消息存储在MappedFile对应的内存映射Buffer中,并没有刷写到磁盘。

          // Return code
          private AppendMessageStatus status;
          // Where to start writing
          private long wroteOffset;
          // Write Bytes
          private int wroteBytes;
          // Message ID
          private String msgId;
          // Message storage timestamp
          private long storeTimestamp;
          // Consume queue's offset(step by one)
          private long logicsOffset;
          private long pagecacheRT = 0;
          private int msgNum = 1; //批量发送消息条数
      
      
      /**
       * When write a message to the commit log, returns code
       */
      public enum AppendMessageStatus {
          PUT_OK,
          END_OF_FILE, //超过文件大小
          MESSAGE_SIZE_EXCEEDED, //消息长度超过最大允许长度
          PROPERTIES_SIZE_EXCEEDED, //消息属性超过最大允许长度
          UNKNOWN_ERROR,
      }
      
    • Step12:更新消息队列逻辑偏移量

    • Step13:处理完消息追加逻辑后将释放putMessageLock锁

    • Step14:DefaultAppendMessageCallback#doAppend只是将消息追加在内存中,需要根据同步刷盘还是异步刷盘方式,将内存中的数据持久化道磁盘,然后执行HA主从同步复制。

      handleDiskFlush(result, putMessageResult, msg);
      handleHA(result, putMessageResult, msg);
      
      return putMessageResult;
      

4.4 存储文件组织与内存映射

RMQ通过使用内存映射文件来提高IO访问性能,无论是CommitLog、ConsumeQueue还是IndexFile,单个文件都被设计成固定长度,如果一个文件写满后再创建一个新文件,文件名就是第一条消息的偏移量。

RMQ使用MappedFile、MappedFileQueue来封装存储文件,一个MappedFileQueue可以包含多个MappedFile。

4.4.1 MappedFileQueue映射文件队列

你可能感兴趣的:(RocketMQ,RocketMQ)