数据存储这一块,RocketMQ使用的是文件编程模型。为了提高文件的写入性能通常会引入内存映射机制,数据先写入页缓存然后再择机将页缓存数据刷盘到磁盘,写入涉及性能与数据可靠性是必须要考虑的。针对刷盘策略一般会有同步刷盘与异步刷盘,RocketMQ也是如此,默认使用异步刷盘。
先来简单看下RocketMQ刷盘操作的代码块:

            try {
                    //We only append data to fileChannel or mappedByteBuffer, never both.
                    if (writeBuffer != null || this.fileChannel.position() != 0) {
                        this.fileChannel.force(false);
                    } else {
                        // 注释4.8.1:同步落盘
                        this.mappedByteBuffer.force();
                    }
                } catch (Throwable e) {
                    log.error("Error occurred when force data to disk.", e);
                }

可以看到刷盘其实就是调用了MappedByteBuffer的force方法。

同步刷盘

同步刷盘指的 Broker 端收到消息发送者的消息后,先写入内存,然后将内容持久化到磁盘后才向客户端返回消息发送成功。
刷盘要分两条线进行分析:

  1. 第一条线是broker在启动的时候会启动一个刷盘线程,调用路径为:BrokerController#start()->DefaultMessageStore#start()->CommitLog#start()->GroupCommitService#start()->MappedFileQueue#flush();
  2. 第二条线是broker在接收到消息后加载或更新MappedFile然后存入MappedFileQueue,调用路径为:SendMessageProcessor#processRequest()->DefaultMessageStore#putMessage()->CommitLog#putMessage()->CommitLog#handleDiskFlush()->GroupCommitRequest#waitForFlush().

第一条线的刷盘线程会在一个while循环里每间隔10ms执行一次刷盘操作,刷盘成功后会唤醒第二条线里中等待响应的线程,在第二条线里组装好MappedFileQueue(CopyOnWriteArrayList类型)之后便会调用countDownLatch的await方法等待刷盘线程的执行。

    //broker接收到消息组装好MappedFileQueue后等待刷盘线程执行
    public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
        // Synchronization flush
        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
            if (messageExt.isWaitStoreMsgOK()) {
                GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
                service.putRequest(request);
                //等待刷盘线程执行
                boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                if (!flushOK) {
                    log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
                        + " client address: " + messageExt.getBornHostString());
                    putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
                }
            } else {
                service.wakeup();
            }
        }
        // Asynchronous flush
        // 注释4.8.2:异步刷盘
        else {
            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                flushCommitLogService.wakeup();
            } else {
                commitLogService.wakeup();
            }
        }
    }
                   for (GroupCommitRequest req : this.requestsRead) {
                        // There may be a message in the next file, so a maximum of
                        // two times the flush
                        boolean flushOK = false;
                        for (int i = 0; i < 2 && !flushOK; i++) {
                            //当前已刷盘指针大于该条消息对应的物理偏移量说明已刷完
                            flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();

                            if (!flushOK) {
                                //刷盘操作
                                CommitLog.this.mappedFileQueue.flush(0);
                            }
                        }
                        //唤醒等待刷盘的线程
                        req.wakeupCustomer(flushOK);
                    }                

关于同步刷盘需要提一下就是每次刷盘并非只刷写一条消息,而是一组消息。
image.png

异步刷盘

同步刷盘的优点是能保证消息不丢失,即向客户断返回成功就代表这条消息已被持久化到磁盘,即消息非常可靠,但是以牺牲写入性能为前提条件的,但由于 RocketMQ 的消息是先写入 PageCache,故消息丢失的可能性较小,如果能容 忍一定几率的消息丢失,但能提高性能,可以考虑使用异步刷盘。

异步刷盘指的是 Broker 将消息存储到 PageCache 后就立即返回成功,然后开启一个异步线程定时执行 FileChannel 的 forece 方法将内存中的数据定时刷写到磁盘,默认间隔为 500ms。在 RocketMQ 的异步刷盘实现类为 FlushRealTimeService。看到这个默认间隔为 500ms,大家是不是会猜测 FlushRealTimeService 是使用了定时任务?
其实不然。这里引入了带超时时间的 CountDown await 方法,这样做的好处时如果没有新的消息写入,会休眠 500ms,但收到了新的消息后,可以被唤醒,做到消息及时被刷盘,而不是一定要等 500 ms。
刷盘线程的等待在CommitRealTimeService#run方法里,唤醒刷盘线程刷盘是在CommitLog#handleDiskFlush的异步分支里。

文件的恢复

这里只是简单提一下。
文件恢复分了正常退出后文件恢复与异常退出的文件恢复。

  • 正常退出后的恢复:以ConsumerQueue为依据,获取里面最后一条消息消费的物理偏移量。如果这个偏移量大于CommitLog文件里的偏移量,则会删除ConsumerQueue里多余的数据;如果小于CommitLog文件里的偏移量,则将多出来的物理偏移量对应的消息进行重发,保证两个文件一致。
  • 异常后的恢复:broker会记录commitlog、index、consumequeue 等文件的最后一次刷盘时间戳,之后还会记录一个checkpoint时间戳(checkpoint文件也会刷盘生成文件)。以checkpoint里的时间戳为基准对比commitlog里的刷盘时间戳进行相应操作。

RocketMQ 启动时候会创建一个名为 abort 的文件,然后在正常退出时会删除该文件,故判断 RocketMQ 进程是否是异常退出只需要查看 abort 文件是否存在,如果存在表示异常退出。

文件恢复入口:DefaultMessageStore#recover,详情可以参考:从 RocketMQ 学基于文件的编程模式(二)

另外,这里涉及到的缓存页与MappedByteBuffer,零拷贝有关,可以参考之前的一篇文章:Java里的零拷贝

相关的文章:RocketMQ源码-MappedFile介绍,其中涉及到了TransientStorePool暂存池,MappedFile预分配,写入与刷盘

参考的文章:从 RocketMQ 学基于文件的编程模式(二)
RocketMQ源码分析之消息存储


步履不停
38 声望13 粉丝

好走的都是下坡路