如何手写一个消息队列【更新中】
最近在了解学习消息队列的底层实现原理,尝试自己手写一个简易版本消息队列 tiny-mq。
1. 整体框架
消息队列的整体架构如图所示,包含生产者、消费者、存储元数据的 MetaServer、实际存储 Topic/Partition 的 Broker
2. 核心模块划分
2.1 网络模块
Producer 生产一个消息之后,根据通讯协议进行编码,然后进行序列化,最后发送到网络中,经过网络传输之后到达 Broker, Broker 经过反序列化之后,根据通讯协议进行解码,Broker 讲过内部处理之后,Consumer 按照同样的流程从 Broker 获取消息进行消费。
从这个流程上来看,我们看到性能瓶颈在几个地方:
- 编解码、序列化/反序列化的速度
- 可以选择更加编解码更快,序列化/反序列化更快的算法进行优化
- 网络传输速度
- 这一个我们没办法在应用层面去进行优化
- 网络连接管理
- 随着客户端和服务端的网络连接数量增多,网络连接增多,我们应该如何进行高效管理连接、处理连接事件
- 并发请求处理
- 面对海量的请求,应该如何高效处理这些请求
2.1.1 消息协议
消息协议的设计应该要有以下几个特点:1. 消息大小尽可能小,消息队列的吞吐和消息的大小成反比 2. 可扩展,消息协议需要能做到向前兼容,方便功能扩展。
一个完整的消息会包含 header 和 body 两部分,其中
- header: 消息头
- Magic:魔数,用来快速标识是否为需要处理的“合法”数据
- Version:版本,方便后续兼容升级
- Type: 消息类型,标识消息为生产者请求、消费者请求、ACK 等消息类型
- MessageId: 消息唯一标识,实现全双工通信
- MsgLen: 消息长度,为 Body 的消息长度
- body: 消息体
- 承载按照特定序列化之后的产物,即实际的数据
所以,消息头固定长度为 12 Byte
,消息体的长度由 MsgLen
决定
2.1.3 序列化方式
todo
2.1.4 连接管理
面对海量的 TCP 连接,目前主流的处理方式都是借助 I/O 多路复用实现。
对于 TCP 连接,Broker 会为将分配一个 FD(文件句柄)
, 然后将其注册到 select/poll/epoll
上,后续 select/poll/epoll
会监听 FD
的各种事件(读/写/就绪等事件),然后配合 Reactor 事件驱动模型高效处理请求。
2.1.5 事件驱动模型
Reactor 模型,是一个异步事件驱动模型;
Reactor 负责接受所有事件,然后根据事件类型去做分发,所有的连接事件都由 accept 处理,读写事件则分发给到业务 handler 去处理。
2.2 数据存储
2.2.1 元数据存储
消息队列元数据包含:
- Broker
- Topic
- Producer
- Consumer
其中,Broker 和 Topic 的元数据存储在 MetaSever
中,Producer 和 Consumer 的元数据存储在 Broker
中
2.2.2 消息持久化
TODO 1.0 暂不考虑做数据持久化
2.2 生产者
发送消息的流程如下:
生产者对外提供统一的 API进行使用。对于使用方来说,只需要提供 Topic 信息即可。
2.2.1 负载均衡
Topic 在哪个 Broker 上,这个映射关系存储在 MetaServer 上,Producer 在发送消息之前需要从 MetaServer 获取映射关系(为了减少频繁请求,我们可以在本地缓存一份,然后定时刷新),然后根据路由规则选择好 Broker 经过网络模块,将消息发送出去。
消息发送选择哪一个 Partition 路由规则一般有以下方式:
- 轮询
- 可以实现将消息均匀地分布在 Partition 上,缺点是只能在 Topic 维度做到顺序消费,而无法在 Partition 维度做到顺序消费
- 哈希
- 可以根据消息设定的键值来选择 Partition 进行消息发送,能够做到在 Topic 和 Partition 维度都是顺序消费;缺点是可能会存在数据倾斜的情况。
2.2.2 ACK 机制
为保证 Broker 收到了发送的消息,Broker 在收到消息之后,会给 Producer 返回一个 ACK 消息,表示已经成功收到。
这里有几种场景可以拿出来讨论以下:
- Broker 成功收到消息,并且 Producer 成功收到 ACK 消息;消息成功发送。
- Broker 成功收到消息,ACK 消息有可能在网络传输中丟失了,导致 Producer 最终没有收到消息,以为消息发送失败了。
- Broker 没有收到消息,消息有可能在网络传输中丢失了,自然就不会有 ACK 消息。
场景 1 是正常场景,场景 2、3 出现了异常,我们应该如何来处理呢?
- 重试:如果在固定时间内没有收到 ACK 消息,可以将消息进行重新发送。
- 幂等:如果是场景 2 的情况下,对同一消息进行重试发送,有可能造成消费者多次收到同一个消息;所以需要做好幂等处理(我们放在消费者模块再展开讲)