原文链接:消息队列二十年
消息队列基本概念
- 生产者:发送消息的一方,每条消息必须有一个主题(TOPIC)
- 主题: 消息的类别,类似于我们的数据库名一样,标识不同的业务来源
- 消费者: 接受消息的一方
- 分区: 每条消息都会存储在分区中,类似于负载均衡策略
- 分区可以让消息写入变得横向可拓展,也是 kafka 等消息队列高吞吐量的本质原因
消息队列发展史
- 阶段一:解耦合。代表产品:ActiveMq, rabbitMq
- 阶段二:吞吐量与一致性。代表产品:kafka, RocketMq
- kafka: 解决实时计算等大数量的问题
- rocketMq: 更进一步解决了可靠性、一致性、顺序消息、事务消息 能力
- 阶段三:平台化。代表产品 pulsar。此阶段云计算、k8s、容器化 等新兴技术兴起,开始规模性尝试基础能力平台化
对于不同的消息队列来说,特点和性能是存储结构的一种显现方式。
kafka
Kafka 底层存储的单位可以粗略认为是分区,分区的存储方式决定了不同的架构形式。如下图所示,1个生产者,1个消费者,2个分区,3个副本,3个服务节点。不同的分区,主节点和从节点都不太一样,因此可以认为 kafka 的服务节点没有主从之分。
消息队列的工作流程:
- 生产者、消费者 会先和 zookeeper 建连,并保持心跳
- 消息会被均匀投递到其中一个分区。信息写入主分区后,系统会有同步/异步的复制机制,避免单节点故障
- 消费者开始工作,拉取相应的信息,并返回ack。此时消费的 offset + 1
kafka 怎么支持的高吞吐量?
- 零拷贝技术:引入零拷贝,直接将数据从内核空间发送到网络,避免数据的来回复制
- 批量发送:生产者发送消息先暂存缓存区,等达到一定量级或达到指定时间再一次性发送
- 分区和多副本:topic 划分为多个分区,每个分区在不同的 broker。同时,kafka 也支持消息的多副本,保证了系统的可靠性
- 消费者维护偏移量:不需要消费者再和MQ进行消费进度的维护,减少了网络 IO
- 磁盘顺序写
优点:顺序写盘
Kafka 底层按照磁盘顺序写盘。设计上一个分区对应一个文件系统,本质上是一个基于磁盘的消息队列。磁盘速度慢的主要原因在于「磁头」的移动,如果按照顺序读写,再加上页缓存,可以非常高效。底层设计上 每个分区对应了一组连续的物理空间,新消息追加到磁盘文件末尾,消费者按顺序拉数据进行消费。
问题:Topic 数量不能太大
Kafka 快的主要原因是采用顺序写盘,但是随着业务的拓展,topic 的数量不断的增加,由于每个 topic 的分区都代表了一个文件系统,磁头需要不断在多个分区之间移动,kafka 的性能也会急剧下降。如下图所示,从左图到右图后,每次消息的写入都代表着磁头的跨区移动。
rocketMq
RocketMq 双主双从架构图如下图,主要的架构改变:
- 元数据管理:去除了zk,维护了轻量级的独立服务集群 name server
- 服务节点变为了多主多从
**RocketMq 在 kafka 基础上做了哪些优化?
- 顺序消息
- 延迟消息 和 定时消息
- 事务消息
- 消费者负载均衡:确保所有消费者公平获取消息
- 多重重试机制
为什么去除 zk,转而使用 namesrv ?
ZK 内部使用 zab 进行信息同步和容灾。信息较多时性能下降比较明显。且 zk 变为了不能挂的存在。namesrv 主要解决大数据场景下的消息发现的可用性和吞吐量,特点如下:
- 使用简单的 kv 结构保存信息
- 支持集群模式,每个namesrv 相互独立,不进行任何通信,水平可拓展
- 数据全部维护在内存,服务节点需要遍历所有的 namesrv 完成注册
怎么提升多Topic的写入性能 ?
kafka 设计中每个分区是一个单独的文件系统,不同分区存储在不同的节点,消息写入时只会写到对应 topic 的主节点之一,也因此可以做到良好的水平拓展
rocketMq 追求极致的消息写入,最好的方案就是所有消息写入都是顺序写入的。rocketMq 将所有 topic 的消息存储在一个文件中,所有消息发送时按照顺序写文件。不同 topic 对应着磁盘上非连续的区域
存储结构
不同 Topic 对应磁盘的非连续区域势必会带来读取的性能问题,解决方案是把所有的 topic 信息顺序写入 commitlog,消费过程中使用 ConsumerQueue、indexFile 索引文件实现数据的高效率读取。
- commitlog: 顺序写入日志文件,文件满了之后,继续写入下一个 log 文件。文件名代表了偏移量
- consumeQueue: 只存储 topic 维度的 commitlog 偏移量,因此文件较小。查询待消费消息时会按照偏移量根据二分方式查找 commitlog的实际消息内容
- indexFile: 另一种索引文件,可以通过 key 或时间区间来查询消息,底层本质上是一个 hash 索引,可以快速通过 key 找到对应 value
pulsar
为了平台化,pulsar 最大的调整就是分层&分片。kafka/rocketMq 中服务节点既是计算节点,同时也是存储节点。节点一旦有状态不太容易实现平台化,比如容器化、数据迁移、数据扩/缩容。
- 分层
- broker: 服务层,无状态。用于发布和消费消息
- bookie: 存储层,有状态。专注于存储
- 分片: 使用更细粒度的分片(Segment)代替粗粒度的分区(Partition)
broker 设计
Broker 中所有的数据都存储在 bookKeeper,所有的元信息都存储在 zookeeper。会带来以下方面的优势:
- 容器化更容易
- 扩容缩容 非常轻量
- 单节点故障转移
bookie 设计
数据会并发写入多个存储节点。下图为一个 4 存储节点,3 副本架构。其中 broker 需要写入 segment 数据时,会经过路由算法找到需要写入的 bookie。底层的副本存储模式为「条带化」的写入方式,是遵循一定规律的,因此可以通过路由算法快速找到 segment 对应的真实存储位置,便于数据迁移和恢复。需要注意,这里的 segment 和 topic 并不是绑定关系,且多个存储节点中的 segment 都是一致的。
bookie 内部读写分离如下图所示,数据的读取流程如下:
- 尝试从写缓存读取数据
- 写缓存 miss, 从读缓存读取
- 读缓存 miss, 根据映射关系找到消息对应的存储位置,从磁盘上读取数据
- 把磁盘读取的数据回写到读缓存
- 数据返回给 broker
怎么支持扩容?
答:新增一个存储节点,路由算法负责优先写入资源充足的节点。
该方案与 kafka 相比是解除了分区和 topic 的绑定性(一个 topic 写入主节点在一个分区上),可以实时调整资源的分配情况。
怎么支持容灾?
答:新增一个存储节点,依赖路由算法找到备份数据的存储地址,复制过来。(还是依赖中间的路由算法)