百日筑基第十九天-一头扎进消息队列2
消息队列的通讯协议
目前业界的通信协议可以分为公有协议和私有协议两种。公有协议指公开的受到认可的具有规 范的协议,比如 JMS、HTTP、STOMP 等。私有协议是指根据自身的功能和需求设计的协 议,一般不具备通用性,比如 Kafka、RocketMQ、Puslar 的协议都是私有协议。
大多数消息队列为了自身的功能支持、迭代速度、灵活性考虑,在核心通信协议的选择上 不会选择公有协议,都会选择自定义私有协议。
从技术上来看,私有协议设计一般需要包含三个步骤。
- 网络通信协议选型,指计算机七层网络模型中的协议选择。比如传输层的 TCP/UDP、应用 层的 HTTP/WebSocket 等。 从功能需求出发,为了保证性能和可靠性,几乎所有主流消息队列在核心生产、消费链路的协 议选择上,都是基于可靠性高、长连接的 TCP 协议。
- 应用通信协议设计,指如何约定客户端和服务端之间的通信规则。比如如何识别请求内容、 如何确定请求字段信息等。 从应用通信协议构成的角度,协议一般会包含**协议头(数据源信息)和协议体(业务数据)**两部分。
- 编解码(序列化 / 反序列化)实现,用于将二进制的消息内容解析为程序可识别的数据格式(或反过来用于传输)。
消息队列的网络模块
网络模块:对消息队列来说,网络模块是核心组件之一,网络模块的性能很大程度上决定了消息传输的能力和整体性能。
消息队列是需要满足高吞吐、高可靠、低延时,并支持多语言访问的基础软件,网络模块最需要解决的是性能、稳定性、开发成本三个问题。
从技术上来看,高性能网络模块的设计可以分为如何高效管理大量的 TCP 连接、如何快速处理高并发的请求、如何提高稳定性和降低开发成本等三个方面。
- 高效处理大量 TCP 连接:在消息队列中主要有**单条 TCP 连接的复用(RabbitMQ)和IO多路复用(Kakfa、RocketMQ、Pulsar )**两种技术思路。
- 快速处理高并发的请求:Reactor 模型是一种处理并发服务请求的事件设计模式,当主流程收到请求后,通过多路分离处理的方式,把请求分发给相应的请求处理器处理。
- 提高稳定性和降低开发成本:在消息队列的网络编程模型中,为了提高稳定性或者降低成本,选择现成的、成熟的 NIO 框架是一个更好的方案(kafka如是)。
消息队列的存储模块
存储模块作为消息队列高吞吐、低延时、高可靠特性的基础保证,可以说是最核心的模块。
消息队列中的数据一般分为元数据和消息数据。元数据是指 Topic、Group、User、ACL、Config 等集群维度的资源数据信息,消息数据指客户端写入的用户的业务数据。
-
元数据信息的存储:元数据信息的特点是数据量比较小,不会经常读写,但是需要保证数据的强一致和高可靠,不允许出现数据的丢失。基于第三方组件来实现元数据的存储是目前业界的主流选择。比如 Kafka ZooKeeper 版本、 Pulsar、RocketMQ 用的就是这个思路,其中 Kakfa 和 Pulsar 的元数据存储在 ZooKeeper 中,RocketMQ 存储在 NameServer 中。另一种思路,集群内部实现元数据的存储是指在集群内部完成元数据的存储和分发。也就是在 集群内部实现类似第三方组件一样的元数据服务,比如基于 Raft 协议实现内部的元数据存储 模块或依赖一些内置的数据库。目前 Kafka 去 ZooKeeper 的版本、RabbitMQ 的 Mnesia、 Kafka 的 C++ 版本 RedPanda 用的就是这个思路。
-
数据消息的存储:第一个思路,每个分区对应一个文件的形式去存储数据(kafka如是)。因为消息队列在大部分情况下的读写是有序的,所以这种机制在读写性能上的表现是最高的。第二种思路,每个节点上所有分区的数据都存储在同一个文件中,这种方案需要为每个分区维护一个对应的索引文件,索引文件里会记录每条消息在 File 里面的位置信息,以便快速定位到具体的消息内容。(目前 RocketMQ、RabbitMQ 和 Pulsar 的底层存储 BookKeeper 用的就是这个方案)
数据分段的规则一般是根据大小来进行的,比如默认 1G 一个文件,同时会支持配置项调整分段数据的大小。
从技术上来看,当数据段到达了规定的大小后,就会新创建一个新文件来保存数据。
如果进行了分段,消息数据可能分布在不同的文件中。所以我们在读取数据的时候,就需要先 定位消息数据在哪个文件中。为了满足这个需求,技术上一般有根据偏移量定位或根据索引定位两种思路。
- 根据偏移量(Offset)来定位消息在哪个分段文件中,是指通过记录每个数据段文件的起始偏 移量、中止偏移量、消息的偏移量信息,来快速定位消息在哪个文件。
- 如果用索引定位,会直接存储消息对应的文件信息,而不是通过偏移量来定位到具体文件。(RabbitMQ 和 RocketMQ 如是)
消息数据存储格式一般包含消息写入文件的格式和消息内容的格式两个方面。
- 消息写入文件的格式指消息是以什么格式写入到文件中的,比如 JSON 字符串或二进制。从 性能和空间冗余的角度来看,消息队列中的数据基本都是以二进制的格式写入到文件的。
- 消息内容的格式是指写入到文件中的数据都包含哪些信息。对于一个成熟的消息队列来说,消 息内容格式不仅关系功能维度的扩展,还牵涉性能维度的优化。(如Kafka 的消息内容包含了业务会感知到的消息的 Header、Key、Value,还包含了时间戳、偏移量、协议版本、数据长度和大小、校验码等基础信息,最后还包含了压缩、事 务、幂等 Kafka 业务相关的信息)
消息队列中的数据最终都会删除,时间周期短的话几小时、甚至几分钟,正常情况一天、三 天、七天,长的话可能一个月,基本很少有场景需要在消息队列中存储一年的数据
消息队列的数据过期机制一般有手动删除和自动删除两种形式。从实现上看主要有三种思路: 消费完成执行 ACK 删除、数据根据时间和保留大小删除、 ACK 机制和过期机制相结合。