分布式系统中一个重要的前提假设是所有的网络传输都是不可靠的,在网络传输不可靠的情况下,保证消息的可靠传输,除了进行重试投递别无他法。常用的绝大多数消息队列RocketMQ、RabbitMQ等在消息传输上都只能保证至少传输成功一次,也即(At least once),而不能保证只传输成功一次(Exactly once)。由于分布式系统网络的不可靠,可能就会出现消息丢失的现象,那么RocketMQ是如何最大限度的保证消息不丢失的呢?那就需要从消息的产生到最终消费的整个过程来分析,消息完整链路可以划分为以下三个阶段:
生产阶段:消息在 Producer 发送端创建出来,经过网络传输发送到 Broker 存储端。
存储阶段:消息在 Broker 端存储,如果是主备或者多副本,消息会在这个阶段被复制到其他的节点或者副本上。
消费阶段:Consumer 消费端从 Broker存储端拉取消息,经过网络传输发送到 Consumer 消费端上,并通过重试来最大限度的保证消息的消费。
一 发送端消息可靠性发送端Producer发送消息Broker端的核心逻辑如下图所示:
消息队列存储的最小单位是消息Message。
同一个Topic下的消息映射成多个逻辑队列。
不同Topic的消息按照到达broker的先后顺序以Append的方式添加至CommitLog,顺序写,随机读。
Broker端CommitLog采用顺序写,可以大大提高写入效率,同时采用不同的刷盘模式提供不同的数据可靠性保证,此外采用了ConsumeQueue中间结构来存储偏移量信息,实现消息的分发。由于ConsumeQueue结构固定且大小有限,在实际情况中,大部分的ConsumeQueue 能够被全部读入内存,可以达到内存读取的速度。此外为了保证CommitLog和ConsumeQueue的一致性, CommitLog里存储了Consume Queues 、Message Key、Tag等所有信息,即使ConsumeQueue丢失,也可以通过 commitLog完全恢复出来,这样只要保证commitLog数据的可靠性,就可以保证Consume Queue的可靠性。RocketMQ存储端采用本地磁盘进行CommitLog消息数据的存储,不可避免的就会带来存储可靠性的挑战,如何保证消息不丢失,RocketMQ消息服务一直在不断提高数据的可靠性。1 存储可靠性挑战RocketMQ存储端也即Broker端在存储消息的时候会面临以下的存储可靠性挑战:
Broker正常关闭
Broker异常Crash
OS Crash
机器掉电,但是能立即恢复供电情况
机器无法开机(可能是cpu、主板、内存等关键设备损坏)
磁盘设备损坏
1正常关闭,Broker 可以正常启动并恢复所有数据。2、3、4同步刷盘可以保证数据不丢失,异步刷盘可能导致少量数据丢失。5、6属于单点故障,且无法恢复。解决单点故障可以采用增加Slave节点,主从异步复制仍然可能有极少量数据丢失,同步复制可以完全避免单点问题。这里一般来说就需要在性能和可靠性之间做出取舍,对于RocketMQ来说,Broker的可靠性主要由两个方面保障:
单机的刷盘机制
主从之间的数据复制
针对broker端单机存储可靠性,主要依赖单机的刷盘策略,主从之间的副本复制可以参考下一章节的主从模式。2 同步刷盘消息写入内存的 PageCache后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。这种方式可以保证数据绝对安全,但是吞吐量不大。3 异步刷盘(默认)消息写入到内存的 PageCache中,就立刻给客户端返回写操作成功,当 PageCache中的消息积累到一定的量时,触发一次写操作,或者定时等策略将 PageCache中的消息写入到磁盘中。这种方式吞吐量大,性能高,但是 PageCache中的数据可能丢失,不能保证数据绝对的安全。实际应用中要结合业务场景,合理设置刷盘方式,尤其是同步刷盘的方式,由于频繁的触发磁盘写动作,会明显降低性能。4 过期文件删除由于RocketMQ操作CommitLog、ConsumeQueue文件是基于文件内存映射机制,并且在启动的时候会将所有的文件加载,为了避免内存与磁盘的浪费、能够让磁盘能够循环利用、避免因为磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平,最终保证新写入消息的可靠存储。三 消费端消息可靠性RockerMQ默认提供了至少消费一次的消费语义来保证消息的可靠消费。通常消费消息的确认机制一般分为两种思路:
先提交后消费
先消费,消费成功后再提交
思路1可以解决重复消费的问题但是会丢失消息,因此RocketMQ默认实现的是思路2,由各自consumer业务方保证幂等来解决重复消费问题。消费端Consumer消费消息核心逻辑如下图所示: