深度解析RocketMq源码-持久化组件(三) 刷盘策略

1.刷盘策略

rocketmq有两种刷盘策略,分别是同步刷盘和异步刷盘:

同步刷盘:同步刷盘就是,消息写入到内存中后,会立刻刷入到磁盘中,然后才会给客户端返回成功。对应配置为:FlushDiskType.SYNC_FLUSH

异步刷盘:异步刷盘是,消息写入内存后就直接给客户端返回成功,然后启动一个线程异步的将数据从内存中刷新到磁盘中,有消息丢失的风险。对应配置为:FlushDiskType.ASYNC_FLUSH。

2.刷盘源码分析

2.1 刷盘的步骤

2.1.1 构造刷盘组件

在构造commitLog的时候,会分别构建出commit和flush的服务。

 //如果是同步的时候,采用GroupCommitService进行flush
        if (FlushDiskType.SYNC_FLUSH == defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            this.flushCommitLogService = new GroupCommitService();
        } else {
            //如果是异步刷盘,采用FlushRealTimeService进行flush
            this.flushCommitLogService = new FlushRealTimeService();
        }
        //开启瞬时缓存池技术的话,采用CommitRealTimeService进行commit
        this.commitLogService = new CommitRealTimeService();

接下来我们主要来分析一下这几个服务的作用。

2.2.2 启动刷盘线程

 public void start() {
        //启动flush服务
        this.flushCommitLogService.start();

        flushDiskWatcher.setDaemon(true);
        flushDiskWatcher.start();

        //如果开启了瞬时缓存池技术 && 采用异步刷盘 && 为主节点,才需要开启commit服务
        if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            this.commitLogService.start();
        }
    }

public boolean isTransientStorePoolEnable() {
        return transientStorePoolEnable && FlushDiskType.ASYNC_FLUSH == getFlushDiskType()
            && BrokerRole.SLAVE != getBrokerRole();
    }

可以看出,如果要开启commit服务的话,必须要为开启了瞬时缓存技术,并且采用异步刷盘,并且为主节点才会生效。

2.2.3 发送刷盘请求到刷盘队列

如果采用了同步刷盘模式,采用的GroupCommitService直接进行flush;

如果采用异步刷盘模式,采用的是FlushRealTimeService进行flush,CommitRealTimeService进行commit。

/**
     * @param result 向mappedfile中追加消息的结果
     * @param messageExt 具体的消息内容已经协议头等
     * @return
     */
    public CompletableFuture<PutMessageStatus> submitFlushRequest(AppendMessageResult result, MessageExt messageExt) {
        // Synchronization flush
        //如果采用同步刷盘
        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            //采用的是GroupCommitService进行刷盘
            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
            //默认是需要等待消息刷盘成功的
            if (messageExt.isWaitStoreMsgOK()) {
                //构建刷盘请求,包括当前消息的在buffer中的写指针
                GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes(),
                        this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                flushDiskWatcher.add(request);
                //调用GroupCommitService的putRequest推送刷盘请求
                service.putRequest(request);
                //当前线程阻塞等待刷盘结果
                return request.future();
            } else {
                //如果不需要等待刷盘线程成功,便直接唤醒刷盘线程,并且返回成功(此时也可能有丢失数据的风险)
                service.wakeup();
                return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
            }
        }
        // Asynchronous flush
        else {
            //如果采用异步刷盘策略
            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                //如果未采用了瞬时缓存池技术,便唤醒flush服务线程
                flushCommitLogService.wakeup();
            } else  {
                //如果未采用了瞬时缓存池技术,便唤醒commit服务线程
                commitLogService.wakeup();
            }
            //返回成功
            return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
        }
    }

2.2 刷盘组件分析

2.2.1 刷盘组件的关系

FlushCommitLogService是一个抽象类,他其实就是一个线程池,不管是commit的组件还是flush的组件都需要继承该类。

注意:其中GroupCommitService和FlushRealTimeService都是提供flush服务的,而commitRealTimeService是提供cimmit服务的。

2.2.2 rocketmq对线程的封装-ServiceThread

1. 如何唤醒该线程

通过countLatch的countDown方法唤醒当前线程。

 public void wakeup() {
        if (hasNotified.compareAndSet(false, true)) {
            waitPoint.countDown(); // notify
        }
    }
2.如果使当前线程阻塞
 protected void waitForRunning(long interval) {
        if (hasNotified.compareAndSet(true, false)) {
            this.onWaitEnd();
            return;
        }

        //entry to wait
        waitPoint.reset();

        try {
            //通过waitPoint的await方法阻塞当前线程
            waitPoint.await(interval, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            log.error("Interrupted", e);
        } finally {
            hasNotified.set(false);
            this.onWaitEnd();
        }
    }

2.2.3 commit服务-CommitRealTimeService

CommitRealTimeService的作用主要是异步进行堆外内存的刷盘,他会启动一个线程每隔200ms进行扫描,如果没有需要刷盘的数据,便等待200ms。如果有,便每次刷入4页数据(16kb)。并且如果距离上次成功刷入数据超过了200ms(commitCommitLogThoroughInterval配置),也会将所有未commit的数据刷入到磁盘中。其实,简单而言,CommitRealTimeService其实就是启动一个线程,每200ms就commit16kb的数据。

        @Override
        public void run() {
            CommitLog.log.info(this.getServiceName() + " service started");
            //如果commit线程启动
            while (!this.isStopped()) {
                //interval为当前线程循环休眠等待的时间,默认为200ms
                int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
                //最少一次性commit的页数,默认为4
                int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();
                //真正的距离上一次成功commit数据的事件间隔,默认为200ms
                int commitDataThoroughInterval =
                    CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();

                long begin = System.currentTimeMillis();
                //如果当前时间距离上次commit的事件超过了commitDataThoroughInterval所规定的时间
                if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
                    this.lastCommitTimestamp = begin;
                    //设置commit刷新的页数为0,其实就是将缓存中所有的数据写入到直接内存中
                    commitDataLeastPages = 0;
                }

                try {
                    //调用commitfile的commit方法commit数据
                    boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
                    long end = System.currentTimeMillis();
                    if (!result) {
                        //如果有数据写入成功,更新最后的commit的2024年6月17日21:21:30
                        this.lastCommitTimestamp = end; // result = false means some data committed.
                        //now wake up flush thread.
                        //唤醒当前线程
                        flushCommitLogService.wakeup();
                    }

                    if (end - begin > 500) {
                        log.info("Commit data to file costs {} ms", end - begin);
                    }
                    //如果没有数据写入,便阻塞线程,默认为200ms
                    this.waitForRunning(interval);
                } catch (Throwable e) {
                    CommitLog.log.error(this.getServiceName() + " service has exception. ", e);
                }
            }

            boolean result = false;
            //重试10次,将当前线程数据全部写入
            for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
                result = CommitLog.this.mappedFileQueue.commit(0);
                CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
            }
            CommitLog.log.info(this.getServiceName() + " service end");
        }
    }

2.2.4 异步flush服务-FlushRealTimeService

FlushRealTimeService是异步刷磁盘的实现方式,里面有两种模式分别是定时刷新模式和实时刷新模式。

实时刷新模式:当有数据写入的时候,便会刷新数据,并且每次最多刷新4页(16kb)的数据。

定时刷新模式:启动一个线程,每过200ms,便会刷新16kb的数据到内存中,每过10s钟,会就检测未刷盘的文件,并且全部刷入到磁盘中。

可以看出,其实刷盘也有实时刷盘模式和定时刷盘模式,而异步刷盘和实时刷盘的本质区别还是在调用线程是否等待刷盘结果上的,同步模式会阻塞线程并且等待刷盘结果,而异步模式则不会等待刷盘结果,便直接返回成功。

class FlushRealTimeService extends FlushCommitLogService {
        private long lastFlushTimestamp = 0;
        private long printTimes = 0;

        public void run() {
            CommitLog.log.info(this.getServiceName() + " service started");
            //flush服务启动的话
            while (!this.isStopped()) {
                //是否开启定时flush
                boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
                //距离上次成功flush时间,默认为500ms
                int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
                //每次flush的页数最少为4页
                int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
                //flush的时间间隔,默认为10s
                int flushPhysicQueueThoroughInterval =
                    CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();

                boolean printFlushProgress = false;

                // Print flush progress
                long currentTimeMillis = System.currentTimeMillis();
                //如果距离上次刷盘时间超过10s,便会将磁盘中所有数据刷入到内存中
                if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
                    this.lastFlushTimestamp = currentTimeMillis;
                    flushPhysicQueueLeastPages = 0;
                    printFlushProgress = (printTimes++ % 10) == 0;
                }

                try {
                    //是否开启定时刷盘配置
                    if (flushCommitLogTimed) {
                        //如果开启定时刷盘,便是每隔500ms,会刷新4个内存页到磁盘中
                        Thread.sleep(interval);
                    } else {
                        //如果没开启定时刷盘,便会等待500ms的时间,注意,如果中间有数据写入到缓存中,便会唤醒该线程,开始新的循环检测
                        this.waitForRunning(interval);
                    }

                    if (printFlushProgress) {
                        this.printFlushProgress();
                    }

                    long begin = System.currentTimeMillis();
                    //调用mappedfile的flush组件刷新磁盘文件
                    CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
                    //更新存储时间戳
                    long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
                    if (storeTimestamp > 0) {
                        CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
                    }
                    long past = System.currentTimeMillis() - begin;
                    if (past > 500) {
                        log.info("Flush data to disk costs {} ms", past);
                    }
                } catch (Throwable e) {
                    CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
                    this.printFlushProgress();
                }
            }

            // Normal shutdown, to ensure that all the flush before exit
            boolean result = false;
            //如果刷新失败,强制将所有数据刷新到磁盘中
            for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
                result = CommitLog.this.mappedFileQueue.flush(0);
                CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
            }

            this.printFlushProgress();

            CommitLog.log.info(this.getServiceName() + " service end");
        }

2.2.5 同步刷盘服务-GroupCommitService

1.组成

可以看出GroupCommitService有两个队列来接收请求,一个用来请求,这个时候可以将另一个队列的请求写入到磁盘中。并且在固定的时间间隔,将两个队列进行交互,其实就是利用读写分离的思想,减少锁竞争。

 class GroupCommitService extends FlushCommitLogService {
        //接收请求的写队列
        private volatile LinkedList<GroupCommitRequest> requestsWrite = new LinkedList<GroupCommitRequest>();
        //接收请求的读队列
        private volatile LinkedList<GroupCommitRequest> requestsRead = new LinkedList<GroupCommitRequest>();
}
2.将请求加入到队列中
 //通过同步机制将请求加入到读队列中
        public synchronized void putRequest(final GroupCommitRequest request) {
            lock.lock();
            try {
                this.requestsWrite.add(request);
            } finally {
                lock.unlock();
            }
            //核心,在将请求加入到读队列中,会立刻唤醒当前线程,进行下一轮的逻辑
            this.wakeup();
        }
3.消费请求,并且进行flush

其实就是每过10ms检测一次是否有需要flush的数据,如过此时有数据写入,也可以立刻进行flush。

public void run() {
            CommitLog.log.info(this.getServiceName() + " service started");

            while (!this.isStopped()) {
                try {
                    //每过10ms检测做一次flush操作,如果这个时候有请求到来,便立刻进行flush
                    this.waitForRunning(10);
                    this.doCommit();
                } catch (Exception e) {
                    CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
                }
            }
}
  private void doCommit() {
            if (!this.requestsRead.isEmpty()) {
                //遍历读请求
                for (GroupCommitRequest req : this.requestsRead) {
                    // There may be a message in the next file, so a maximum of
                    // two times the flush
                    //刷新磁盘
                    boolean flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
                    for (int i = 0; i < 2 && !flushOK; i++) {
                        CommitLog.this.mappedFileQueue.flush(0);
                        flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
                    }
                    //写入成功后,唤醒调用线程并通知写入成功
                    req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT);
                }

                long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
                if (storeTimestamp > 0) {
                    CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
                }

                this.requestsRead = new LinkedList<>();
            } else {
                // Because of individual messages is set to not sync flush, it
                // will come to this process
                CommitLog.this.mappedFileQueue.flush(0);
            }
        }

2.2.6 GroupCommitService和FlushRealTimeService的区别

1.GroupCommitService和FlushRealTimeService的实时刷盘模式都可以在buffer被写入数据后立刻刷新到磁盘中,但是GroupCommitService,有一个批量的操作,可以一次性将多条消息刷入磁盘中。

2.FlushRealTimeService的默认模式其实是定时刷盘模式,它其实是每过500ms,将16kb的消息刷入到磁盘中,并且每过10秒会将缓存中所有消息刷入到磁盘中。

3.异步刷盘和同步刷盘的对比

3.1 同步刷盘

同步刷盘瞬时缓存池技术是失效的

同步刷盘的步骤如下:

其实就是在写入消息过后,并且阻塞当前线程并,立刻唤醒刷盘线程,然后执行commit方法,将数据刷入到磁盘中,然后唤醒调用线程。

3.2 异步刷盘

异步刷盘在写入数据过后,立刻给调用线程返回成功,并且看是否开启了定时刷盘模式(根据参数flushCommitLogTimed进行控制),如果开启,便每过500ms刷新16kb的数据,如果未开启,写入消息过后立刻进行刷新。

4.总结

同步刷盘主要是在发送刷盘请求过后,会同步等待刷盘结果,这是它不会丢数据的根本原因。异步刷盘有两种模式定时模式和实时模式。实时模式就是数据写入后立刻刷盘,但是调用线程并不会阻塞等待刷盘结果;定时模式就是会启动一个线程每过500ms写入数据,这样做可以增大一次刷盘的条数,减少磁盘IO。

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值