原创 rocketmq核心源码分析第十一篇一消息处理第五部分一broker端获取添加消息的mappedFile

获取映射文件原理图

  • 当前文件已经写满或者空间小于当前消息长度,则需要新建
  • commitlog[1G]需要通过allocateMappedFileService创建文件
  • 每次构建两个请求[创建两个commitlog文件]加入任务池
  • 追加消息线程通过任务池的request阻塞[线程闭锁工具]
  • allocateMappedFileService从任务池异步按顺序获取任务创建文件
  • 完成假值填充预热[磁盘载入内存]
  • 完成内存锁定[防止内存交换导致缺页]
  • 完成文件创建,线程闭锁工具CountDownLatch().countDown()唤醒追加消息线程

在这里插入图片描述

问题答疑
什么会创建两个文件? (nextFilePath和nextNextFilePath)因为默认以一个commitlog文件大小1G,创建两个后,再次获取文件无需完成内存映射等过程,提高添加消息性能
为什么需要写入假值零?创建的内存映射文件实际还在硬盘上,需要给内存映射文件填充假值,从而实现操作系统底层的缺页处理,完成mmap从而实现内存映射
为什么需要mlock操作系统底层存在内存置换,当其他进程所需内存不足时,可能存在swap交换当前内存到硬盘,mlock锁住当前内存,不允许操作系统置换,提高append消息性能

源码分析一创建新文件

commitlog.asyncPutMessage消息追加缓冲区作为创建文件入口

  • step-1: 缓冲区写满
  • step-2: 追加消息到commitlog的文件对应内存
  • step-3: 当前文件不足则新建文件后添加消息到缓冲区
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
    
    step-1: 缓冲区写满
    if (null == mappedFile || mappedFile.isFull()) {
        // 获取一个新的mappedFile
        mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
    }
    
    追加消息到commitlog的文件对应内存
    result = mappedFile.appendMessage(msg, this.appendMessageCallback);
    switch (result.getStatus()) {
        case PUT_OK:
            step-2: 消息正常写入缓冲区
            break;
        case END_OF_FILE:
            step-3: 当前文件不足则新建文件
            mappedFile = this.mappedFileQueue.getLastMappedFile(0);
            ...... 删除其他代码
            消息刷到内存
            result = mappedFile.appendMessage(msg, this.appendMessageCallback);
            break;
        ...... 删除其他代码
        default:
            beginTimeInLock = 0;
            return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result));
    }
}

源码分析一getLastMappedFile

  • step-1: 异步创建:构建两个文件名称
  • step-2: commitlog创建方式,commitlog大小1g,一次异步创建2个
  • step-3: 同步创建 consumequeue和index创建方式,直接创建一个mmapedFile
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
    ...... 删除其他代码
    if (createOffset != -1 && needCreate) {
        step-1: 异步创建:构建两个文件名称
        第一个文件创建完毕唤醒appendmessage线程
        第二个文件异步创建用于提高下次获取文件的效率
        String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
        String nextNextFilePath = this.storePath + File.separator
            + UtilAll.offset2FileName(createOffset + this.mappedFileSize);
        MappedFile mappedFile = null;

        if (this.allocateMappedFileService != null) { 
            commitlog创建方式,commitlog大小1g,一次异步创建2个
            mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
                nextNextFilePath, this.mappedFileSize);
        } else {
            consumequeue和index创建方式
            try {
                mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
            } catch (IOException e) {
                log.error("create mappedFile exception", e);
            }
        }
        ...... 删除其他代码
        return mappedFile;
    }  
}

源码分析一putRequestAndReturnMappedFile

  • step-1: 添加两个请求到任务处理池requestTable 处理线程阻塞等待唤醒
  • step-2: 添加两个请求添加到任务处理池requestQueue[commitlog文件名进行排序 文件名小的先创建 文件名按照offset进行命名]
  • step-3: 主线程等待nextFilePath创建完毕 nextNextFilePath无需等待
public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) {
    默认可以处理两个映射文件创建请求
    int canSubmitRequests = 2;
    重新计算最多可以提交几个文件创建请求,(一般两个)
    if (this.messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
        if (this.messageStore.getMessageStoreConfig().isFastFailIfNoBufferInStorePool()
            && BrokerRole.SLAVE != this.messageStore.getMessageStoreConfig().getBrokerRole()) { //if broker is slave, don't fast fail even no buffer in pool
            canSubmitRequests = this.messageStore.getTransientStorePool().availableBufferNums() - this.requestQueue.size();
        }
    }
    添加到任务处理池requestTable 处理线程阻塞等待唤醒
    AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize);
    boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null;
    if (nextPutOK) {
        if (canSubmitRequests <= 0) {
            log.warn("[NOTIFYME]TransientStorePool is not enough, so create mapped file error, " +
                "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().availableBufferNums());
            this.requestTable.remove(nextFilePath);
            return null;
        }
        添加到任务处理池requestQueue[commitlog文件名进行排序 文件名小的先创建 文件名按照offset进行命名]
        boolean offerOK = this.requestQueue.offer(nextReq);
        if (!offerOK) {
            log.warn("never expected here, add a request to preallocate queue failed");
        }
        canSubmitRequests--;
    }

    处理nextNextReq到requestTable和requestQueue
    AllocateRequest nextNextReq = new AllocateRequest(nextNextFilePath, fileSize);
    boolean nextNextPutOK = this.requestTable.putIfAbsent(nextNextFilePath, nextNextReq) == null;
    if (nextNextPutOK) {
        if (canSubmitRequests <= 0) {
            log.warn("[NOTIFYME]TransientStorePool is not enough, so skip preallocate mapped file, " +
                "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().availableBufferNums());
            this.requestTable.remove(nextNextFilePath);
        } else {
            boolean offerOK = this.requestQueue.offer(nextNextReq);
            if (!offerOK) {
                log.warn("never expected here, add a request to preallocate queue failed");
            }
        }
    }

    if (hasException) {
        log.warn(this.getServiceName() + " service has exception. so return null");
        return null;
    }
    主线程等待nextFilePath创建完毕
    AllocateRequest result = this.requestTable.get(nextFilePath);
    try {
        if (result != null) {
            通过CountDownLatch完成线程间信息通信
            boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);
            if (!waitOK) {
                log.warn("create mmap timeout " + result.getFilePath() + " " + result.getFileSize());
                return null;
            } else {
                this.requestTable.remove(nextFilePath);
                return result.getMappedFile();
            }
        } else {
            log.error("find preallocate mmap failed, this never happen");
        }
    } catch (InterruptedException e) {
        log.warn(this.getServiceName() + " service has exception. ", e);
    }

    return null;
}

源码分析一AllocateMappedFileService.run

  • DefaultMessageStore构造函数会启动AllocateMappedFileService线程
  • 自旋获取请求,不存在阻塞,否则创建文件
public void run() {
    log.info(this.getServiceName() + " service started");
    while (!this.isStopped() && this.mmapOperation()) {
    }
    log.info(this.getServiceName() + " service end");
}   

源码分析一mmapOperation

  • step-1: 优先级阻塞队列,获取创建文件请求[队列无元素阻塞]
  • step-2: 是否允许堆外内存,要求异步刷盘 并且启动堆外内存才会走该机制
  • step-2.1: 堆外内存创建方式
  • step-2.2: 没有堆外内存,则mmap内存映射
  • step-3: 内存预热以及mlock
  private boolean mmapOperation() {
    boolean isSuccess = false;
    AllocateRequest req = null;
    try {
        step-1: 优先级阻塞队列,创建请求[队列无元素阻塞]
        req = this.requestQueue.take();
        AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath());
        ...... 删除其他代码
        if (req.getMappedFile() == null) {
            long beginTime = System.currentTimeMillis();

            MappedFile mappedFile;
            step-2: 是否允许堆外内存,要求异步刷盘 并且启动堆外内存才会走该机制
            if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                step-2.1: 堆外内存创建方式
                mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
                mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                ...... 删除其他代码
            } else {
                step-2.2: 没有堆外内存,则mmap内存映射
                mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
            }
            ...... 删除其他代码
            if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
                .getMappedFileSizeCommitLog()
                &&
                this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
                step-3: 内存预热以及mlock
                写入假值0 进行预热 写入假值进而操作系统发现os page 缺页 从而读取物理磁盘数据到内存
                mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
                    this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
            }
            ......删除其他代码
        }
    ......删除其他代码
    }finally {
        唤醒添加消息线程
        if (req != null && isSuccess)
            req.getCountDownLatch().countDown();
    }
    return true;
}

源码分析一warmMappedFile

对1G的commitlog进行预热 写入假值,适当让出cpu 然后通过mlock防止swap

ps: 笔者至今也没有查到为什么填充假值0,而不是1或者其他很官方的说明,读者有知道的可以留言

public void warmMappedFile(FlushDiskType type, int pages) {
    long beginTime = System.currentTimeMillis();
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    int flush = 0;
    long time = System.currentTimeMillis();1G的commitlog进行预热 写入假值,适当让出cpu 然后通过mlock防止swap
    for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
         仅分配内存并调用 mlock 并不会为调用进程锁定这些内存,因为对应的分页可能是写时复制(copy-on-write)的5。因此,你应该在每个页面中写入一个假的值
         也就是说仅仅分配,但内存映射尚未执行

        /**
         * Copy-on-write
         * 为什么是0不是1 或者其他值
         * renxl: 我个人的理解 不管是0,1或者其他值,都可以达到内存映射 物理内存分配 防止缺页
         * 但是0相对来说不会改变其原有的值,合理性更好
         * 建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存
         *
         *
         * 使用mmap()内存分配时,只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,进而进入内核空间分配物理内存、更新进程缓存表,最后返回用户空间,回复进程运行
         */
        byteBuffer.put(i, (byte) 0);
        // force flush when flush disk type is sync
        if (type == FlushDiskType.SYNC_FLUSH) {
            if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                flush = i;
                mappedByteBuffer.force();
            }
        }

        主动放弃cpu 
        if (j % 1000 == 0) {
            log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
            time = System.currentTimeMillis();
            try {
                Thread.sleep(0);
            } catch (InterruptedException e) {
                log.error("Interrupted", e);
            }
        }
    }

    刷盘
    if (type == FlushDiskType.SYNC_FLUSH) {
        log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",
            this.getFileName(), System.currentTimeMillis() - beginTime);
        mappedByteBuffer.force();
    }
    通过mlock避免内存被操作系统swap
    this.mlock();
}

mlock

  • 调用c语言的mlock函数完成内存锁定
  • 防止内存不足或其他情况导致内存被置换出,从而导致下次内存缺页,操作系统需要从磁盘进行IO
public void mlock() {
    final long beginTime = System.currentTimeMillis();
    final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
    Pointer pointer = new Pointer(address);
    {
        // 位置+ 长度
        int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));
        log.info("mlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
    }

    {
        int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED);
        log.info("madvise {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
    }
}

总结

  • commitlog的创建方式分为直接内存映射或者内存映射+堆外缓冲的方式[参见刷盘章节]
  • commitlog的创建会期望一次创建两个
  • 通过假值0填充完成内存映射
  • 通过mlock锁定内存
  • 所有的工作都是为了appendMessage时能够高性能写入缓冲区
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的。以下是一个简单的 RocketMQ 消息队列的代码示例,它用于处理数据: ``` import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class Consumer { public static void main(String[] args) throws InterruptedException, MQClientException { // 创建消费者 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group"); // 设置 Nameserver 地址 consumer.setNamesrvAddr("localhost:9876"); // 设置从哪里开始消费 consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 设置消费者消费的主题和标签(* 表示消费所有消息) consumer.subscribe("TopicTest", "*"); // 注册消息监听器 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { System.out.println("收到消息:" + new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // 启动消费者 consumer.start(); System.out.println("消费者已启动"); } } ``` 这段代码中,我们首先创建了一个消费者,然后设置了 Nameserver 地址、消费的起始位置和消费的主题和标签。接着,我们注册了一个消息监听 ### 回答2: RocketMQ 是一个开源的消息队列系统,它具备高吞吐量、低延迟等优势,适用于大规模分布式数据处理。 下面是一个简单的RocketMQ消息队列处理数据的代码示例: 1. 首先,需要引入RocketMQ的依赖包,可以通过Maven进行管理: ```xml <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>最新版本号</version> </dependency> ``` 2. 创建一个Producer(消息生产者)对象,并设置相关配置: ```java import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.common.message.Message; public class RocketMQProducer { public static void main(String[] args) throws Exception { // 创建消息生产者并指定组名 DefaultMQProducer producer = new DefaultMQProducer("group_name"); // 设置NameServer地址 producer.setNamesrvAddr("127.0.0.1:9876"); // 启动生产者 producer.start(); // 创建消息对象,并指定Topic、Tag、消息内容 Message message = new Message("topic_name", "tag_name", "Hello, RocketMQ!".getBytes()); // 发送消息 producer.send(message); // 关闭生产者 producer.shutdown(); } } ``` 3. 创建一个Consumer(消息消费者)对象,并设置相关配置和消息处理逻辑: ```java import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class RocketMQConsumer { public static void main(String[] args) throws Exception { // 创建消息消费者并指定组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group_name"); // 设置NameServer地址 consumer.setNamesrvAddr("127.0.0.1:9876"); // 订阅Topic和Tag consumer.subscribe("topic_name", "tag_name"); // 注册消息监听器 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) { for (MessageExt message : list) { // 处理消息逻辑 System.out.println(new String(message.getBody())); } // 消息处理成功,返回消费状态 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // 启动消费者 consumer.start(); } } ``` 以上就是一个简单的RocketMQ消息队列处理数据的代码示例,通过消息生产者发送消息消息消费者接收并处理消息。具体使用时,需要根据自己的实际需求进行配置和逻辑处理。 ### 回答3: 下面是一个使用RocketMQ消息队列处理数据的Java代码示例: ```java import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class RocketMQConsumer { public static void main(String[] args) throws Exception { // 创建一个消费者实例,并指定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup"); // 设置NameServer地址,多个地址用分号隔开 consumer.setNamesrvAddr("localhost:9876"); // 订阅一个或多个Topic,以及Tag来过滤需要消费的消息 consumer.subscribe("TopicTest", "*"); // 注册消息监听器,处理收到的消息 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) { for (MessageExt message : list) { // 具体的消息处理逻辑 System.out.println("Received message: " + new String(message.getBody())); } // 消费成功,返回CONSUME_SUCCESS,让Broker知道该消息已被消费 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // 启动消费者实例 consumer.start(); System.out.println("Consumer started."); } } ``` 上述示例中,首先创建了一个消费者实例,并指定了消费者组名。然后通过`setNamesrvAddr`方法设置了RocketMQ的NameServer地址。接着使用`subscribe`方法订阅需要消费的Topic和Tag。 在`registerMessageListener`方法中,通过实现`MessageListenerConcurrently`接口来编写消息处理逻辑。在`consumeMessage`方法中,处理RocketMQ接收到的消息。通过`getBody`方法获取消息内容,然后可以根据实际需求进行相应的业务处理。在处理完成后,需要返回`CONSUME_SUCCESS`,告诉Broker消息已被消费。 最后,调用`start`方法启动消费者实例,然后就可以开始监听和处理消息了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值