目录
- 思维导图
- 主要内容
- 架构
- 文件系统
- 一次完整的通信流程
- 高可靠
- 高可用
- 高性能
- 相关面试题
1. 思维导图
2. 主要内容
2.1 架构组成
- 使用发布-订阅模型
- NameServer: 主要负责对于Topic-Broker关联信息的管理
- Producer: 消息生产者
- Broker: 消息中转角色,负责存储消息,转发消息
- Consumer: 消息消费者,后台系统负责异步消费
- Pull 模式: 定时主动从消息服务器拉取信息,然后消费
- Push 模式:
- Consumer 后台线程,会去Broker拉取待处理消息(PullRequest)
- 如果Broker有待处理消息的话,那么就间隔一定时间再次从Broker】拉取消息的逻辑
- 如果Broker没有待处理消息的话,就hold住请求(ConcurrentHashMap, key=topic+queueid, val=PullRequest数组),等有新的消息到的时候才返回(长轮询机制)
- 消费者获取到消息之后,回调MessageListener的实现去消费消息,然后重新执行1的逻辑
- Push模式选择基于Pull模式封装实现的原因:
- 如果要实现Broker主动推送的Push,需要Broker对Consumer做负载均衡,而Broker是分布式的,实现难度较大
2.2 文件系统
CommitLog : 混合型的存储结构,即为Broker单个实例下多个Topic的消息实体内容都存储于一个文件中。缺点在于,会存在较多的随机读操作,因此读的效率偏低。同时消费消息需要依赖ConsumeQueue,构建该逻辑消费队列需要一定开销。 ConsumeQueue: 1. commitlog的索引,提升commitlog的查询性能, 2. 索引文件定长设计,路径HOME/store/consumequeue/{topic}/{queueId}/{fileName},单文件存储30w条,类似数组随机访问 3. offsettable.offset 记录已消费的索引偏移量 Indexfile:提供了一种可以通过key或时间区间来查询消息的方法, hashmap结 Broker端的后台服务线程—ReputMessageService不停地异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。
2.3 一次完整的通信流程
- 发送消息
- Broker启动时,向NameServer注册信息
- 客户端调用producer发送消息时,会先检查本地是否有topic的路由信息,没有则从NameServer获取
- 路由信息,包括topic包含的队列列表和broker列表
- Producer端根据查询策略,选出其中一个队列,用于后续存储消息
- Producer向Broker发送rpc请求,将消息保存到broker端
- 消息刷盘 6. Broker端收到消息后,将消息原始信息保存在CommitLog文件对应的MappedFile中,然后异步刷新到磁盘 7. ReputMessageServie线程异步的将CommitLog中MappedFile中的消息保存到ConsumerQueue和IndexFile中(ConsumerQueue和IndexFile只是原始文件的索引信息)
- 消费消息 8. Consumer实例启动上线,向Broker发送心跳,通知Broker订阅关系,重新负载均衡( 分配MessageQueue) 9. Consumer向Broker请求拉取消息(group, topic, queueId, offset) 10. Broker根据offset*20,映射consumequeue到mmap文件,找到commitlog的偏移量 11. Broker根据commitlog的偏移量,映射commitlog到mmap文件,找到消息并返回 12. Broker更新偏移量
2.4 高可靠设计
- 发送消息
- 同步发送,关注SendResult#sendStatus=SEND_OK
- 异步发送,实现SendCallback接口关注onSuccess函数回调
- 消息落盘
- 同步刷盘(SYNC_FLUSH): 消息写入成功后(到 page cache 后),会立刻触发操作系统的 fsync 操作,把消息刷到磁盘中,这时候表现上就是消息发送需要等到真正刷到磁盘才会返回。
- 异步落盘(ASYNC_FLUSH): 写完 page cache 就结束, 大概率会持久化成功,但如果这时 broker 发生重启,有机会造成消息丢失。何时才刷盘?一来是由操作系统决定,二来 RocketMQ 也支持在异步刷盘的情况下,隔一段时间就强行触发一次刷盘。
- 主从复制
- 同步双写(SYNC_MASTER): 消息写入 master 之后,写入的线程会等待 slave 的数据同步。由于 slave 同步的过程中会上报自己的同步的最大进度(消息文件里面的物理offset),当发现至少有一个 slave 的进度达到了和 master 一致,这条写入的线程才会返回。这意味着消息至少在两个 broker 上存在了。当然了,这里的存在也只能保证写入到了 page cache。
- 异步复制(ASYNC_MASTER): 异步复制是指消息写入 master 之后,就算成功,slave 自己会不断地同步 master 数据。注意,slave 同步数据不是以消息为维度的,而是以 commitLog 文件同步的方式去顺序同步的,也就是说如果 slave 落后很多的话,slave 没有同步完成前面的消息是不会同步这个最新的消息的。
- 一般而言,会建议采取 SYNC_MASTER+ASYNC_FLUSH 的方式,在消息的可靠性和性能间有一个较好的平衡。如果要保证不丢消息,是不是至少需要SYNC_FLUSH+ASYNC_MASTER?
- 消费消息
- Broker没有收到ack回调或者收到的是ConsumeConcurrentlyStatus.RECONSUME_LATER,一段时间后会重新投递
2.5 高可用设计
- Name server轻量化,无状态设计,支持水平扩展
- 服务启动后从Name server获取Broker信息到本地缓存,Name server短时间不可用影响不大
- Broker高可用:主从复制+集群
- 消费高可用:Broker不可用,consumer自动从master切换到slave
- 发送高可用:message queue可以创建在多个broker组上,broker组a的master不可用,组b的master依然可用
2.6 高性能设计
- 顺序写,不区分topic统一写一个文件
- Broker端的后台服务线程—ReputMessageService不停地异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。
- pagecache:ConsumeQueue 顺序读取情况下,系统pagecache命中率很高
- ConsumeQueue/Indexfile定长存储,mmap操作内存直接映射磁盘文件,减少一次内核态<->用户态的内存复制
2.7 严格顺序消息
- 消息投递
- 生产者实现MessageQueueSelector接口,使用Hash取模法来保证同一个业务主键投递到同一个Broker上
- 消息投递时必须使用同步方法
- Broker挂了/扩容引起服务器数量变化,会导致消息短暂乱序的情况
- 消息存储
- broker端的分布式锁: 保证同一个consumerGroup下同一个messageQueue只会被分配给一个consumerClient
- messageQueue的本地synchronized锁: 保证同一时刻对于同一个队列只有一个线程去消费它
- ProccessQueue的本地consumeLock: 防止在消费消息的过程中,该消息队列因为发生负载均衡而被分配给其他客户端,进而导致的两个客户端重复消费消息的行为
- 消息消费
- 消费者注册MessageListenerOrderly类型的回调接口实现顺序消费
- 顺序消费的问题
- 使用了很多的锁,降低了吞吐量
- 前一个消息消费阻塞时后面消息都会被阻塞。如果遇到消费失败的消息,会自动对当前消息进行重试(每次间隔时间为1秒),无法自动跳过
3. 相关面试题
- 为什么要使用消息队列呢? 解耦 异步 削峰
- RocketMQ的消息模型是什么?
- RoctetMQ基本架构了解吗?
- 如何保证消息的可用性
- 如何保证消息可靠性/不丢失呢?
- 如何处理消息重复的问题呢
- 延迟消息原理
- 主从复制原理
- RocketMQ 如何保证一条消息at least once?
- 生产者拿到成功的回执才认为发送成功
- 消费者确认消费成功了才回调成功,需要注意幂等性判断和异常处理
- 亿级消息堆积能力支持?
- 1条消息4M算,1亿消息commitlog需要近40G硬盘容量
- consumequeue每条20字节,单文件30w条,空间5.7M, 总容量1.8G
- indexfile每条40字节,总容量3.7G
- 日志总容量45G左右,正常服务器的硬盘负荷内,并且因为混合存储,各个topic的读取性能比较稳定