1. MQTT协议介绍
MQTT(Message Queuing Telemetry Transport)协议是IBM在1999年发布的一种基于发布/订阅模式的轻量级通讯协议专为低带宽,不稳定的网络环境设计,该协议构建于TCP/IP协议上。
1.1 MQTT起源
MQTT的诞生于1999年,最初是用来解决工业控制领域普遍采用轮询机制导致效率低下和网络负载高的问题来提出的协议。MQTT最初的名称为"Argo轻量级有线协议"(Argo Lightweight On The Wire Protocol),后来改名为 "MQ integrator Pervasive Device Protocol" (MQlpdp)。1999年,MQTT1.0发布
1.2 协议特点
- 轻量级:MQTT协议的报文非常的小,适合资源受限的设备。
- 简单易用: MQTT协议的语法简单易懂,易于开发和使用。
- 可扩展增强:MQTT协议支持发布订/阅模式,可连接大量设备。
- 可靠性高: MQTT协议支持多种消息质量等级,可以确保消息的可靠传输。
1.3 发展历程
MQTT的两个主要版本
MQTT1.0发布后随即被广泛的应用于工业控制系统中。2003年IBM联合Eurotech成立了MQTT协议工作组,负责推动MQTT的标准化工作。MQTT协议在2010年正式发布3.1的正式标准(非OASIS标准)。2013年 MQTT协议被正式提交给OASIS进行标准化(OASIS),2014年3.1.1版本正式发布,3.1.1 版本发布后2015左右开始在物联网领域得到应用,随后再2018年对协议进行了增强推出了MQTT5.0。增加了其灵活性和属性增强(消息属性拓展),最终在2019年MQTT5.0正式标准发布。
MQTT5.0版本升级内容
| 特性 | 描述 |
|---|---|
| 属性支持 | 引入了属性(Properties),为消息和控制包添加了灵活的元数据支持。 |
| 增强的错误报告 | 增加了原因码(Reason Codes)和原因字符串(Reason Strings),提供更详细的错误报告。 |
| 共享订阅 | 允许多个客户端共享同一个订阅,以实现负载均衡。 |
| 消息过期 | 支持消息过期期限(Message Expiry Interval),指定消息的有效期。 |
| 主题别名 | 通过主题别名(Topic Aliases)减少传输时的带宽。 |
| 会话过期 | 引入会话过期时间(Session Expiry Interval),允许会话在客户端断开连接后保留一段时间。 |
| 请求/响应模式 | 支持请求/响应模式,允许客户端和服务端之间的同步通信。 |
| 用户属性 | 用户属性(User Properties)允许用户自定义键值对附加到 MQTT 消息中。 |
| 增强的认证 | 增强了认证机制,支持多步认证流程。 |
| 流量控制 | 引入了最大消息量(Maximum Packet Size)和消息速率限制(Receive Maximum)等流量控制功能。 |
1.4 MQTT协议概览
整体MQTT的消息格式
1.4.1 固定头部分
固定头存在于所有MQTT数据包中,其结构如下:
1.4.1.1. 消息类型
| 名称 | 值 | 流方向 | 描述 |
|---|---|---|---|
| Reserved | 0 | 不可用 | 保留位 |
| CONNECT | 1 | 客户端到服务器 | 客户端请求连接到服务器 |
| CONNACK | 2 | 服务器到客户端 | 连接确认 |
| PUBLISH | 3 | 双向 | 发布消息 |
| PUBACK | 4 | 双向 | 发布确认(QoS 1) |
| PUBREC | 5 | 双向 | 消息发布收到确认(QoS 2) |
| PUBREL | 6 | 双向 | 发布收到(QoS 2) |
| PUBCOMP | 7 | 双向 | 发布释放(QoS 2) |
| SUBSCRIBE | 8 | 客户端到服务器 | 客户端请求订阅 |
| SUBACK | 9 | 服务器到客户端 | 订阅确认 |
| UNSUBSCRIBE | 10 | 客户端到服务器 | 请求取消订阅 |
| UNSUBACK | 11 | 服务器到客户端 | 取消订阅确认 |
| PINGREQ | 12 | 客户端到服务器 | PING请求 |
| PINGRESP | 13 | 服务器到客户端 | PING应答 |
| DISCONNECT | 14 | 客户端到服务器 | 中断连接 |
| Reserved(3.1)/AUTH(5.0) | 15 | 双向 | 3.1版本为保留位,5.0版本以后为认证类型 |
注意: QoS1质量的消息至少需要2次交互才可以完成消息的发送或者接收。 QoS2质量的消息需要共4次交互才能完成整消息发送或者接收。
QoS2和QoS1的消息过程
MQTT QoS 2 Message Flow MQTT QoS 1 Message Flow
,------. ,------. ,------. ,------.
|Client| |Broker| |Client| |Broker|
`---+--' `---+--' `---+--' `---+--'
| PUBLISH (QoS 2) | | PUBLISH (QoS 1) |
|----------------->| |----------------->|
| | | |
| PUBREC | | PUBACK |
|<-----------------| |<-----------------|
| | ,---+--. ,---+--.
| PUBREL | |Client| |Broker|
|----------------->| `------' `------'
| |
| PUBCOMP |
|<-----------------|
,---+--. ,---+--.
|Client| |Broker|
`------' `------'
1.4.1.2. 标志位
DUP: 标志着消息可能出现重复,在QoS1和QoS2场景下将会被设置为1,默认为0;结合可变头中的messageId以保证消息传输完成,但不能用来检测消息是否重复发送; Qos: 发布消息的质量
| 名字 | 标记(2位) | 描述 |
|---|---|---|
| QoS0 | 00 | 最多一次,即:<=1 |
| QoS1 | 01 | 至少一次,即:>=1 |
| QoS2 | 10 | 一次,即:=1 |
| reserved | 11 | 预留 |
RETAIN: 发布保留消息(持久化),链接关闭或者没有订阅者后仍不会被清除,新的设备完成连接将后将会收到该类型的消息,类似聊天群公告,但每个TOPIC下只能有一个RETAIN类型的消息存在;
REATIN消息的更新:发送一个新的RETAIN消息即可;
REATIN消息的删除:发送一个载荷为空的消息即可,注意不能是空字符串;
1.4.1.3. 剩余长度
剩余长度(remain length):剩余长度是一个可变字节整数,这意味着长度可以用1到4个字节来表示,具体取决于数据量的大小。其中每个每个字节的首位为标记位标记是否有后续的字节,也就是每个字节有7位来表示长度。最小占用一个字节最大值为127,最大占用4个字节:127+(127×128)+(127×1282)+(127×1283)=268435455(约256MB)。其中表示为控制包中剩余的长度并不包含自身部分也就是不包含固定头的长度。
1.4.2 可变头
MQTT数据包中包含了一个可变头,它位于固定头和报文之间,并不是所有类型的报文都有可变头,这里不展开各报文类型的可变头结构,另外可变头的内容也和报文的类型不同各有差异。 其中连接报文有协议名称和链接标志本次不展开参考官方文档,本次只选择其中两个部分展示;
1.4.2.1. 数据包ID(Packet Identifier)
数据包ID作为QoS1,QoS2相关报文涉及到的可变头中都需要包含,另外QoS0的PUBLISH报文必须不能携带数据包ID。
以下是是否携带 Packet Identifier对照表。
| MQTT Control Packet | Packet Identifier field |
|---|---|
| CONNECT | NO |
| CONNACK | NO |
| PUBLISH | YES (If QoS > 0) |
| PUBACK | YES |
| PUBREC | YES |
| PUBREL | YES |
| PUBCOMP | YES |
| SUBSCRIBE | YES |
| SUBACK | YES |
| UNSUBSCRIBE | YES |
| UNSUBACK | YES |
| PINGREQ | NO |
| PINGRESP | NO |
| DISCONNECT | NO |
| AUTH | NO |
1.4.2.2. 属性(Properties)
可变头中的最后一个字段,其中分布报文类型有 CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT 以及 AUTH 。官方文档
可变属性有以下标识符
用户属性的格式为:标识符+属性长度(不包括长度字段本身)+ 属性内容
| Identifier | Name (usage) | Type | Packet / Will Properties | 备注 | |
|---|---|---|---|---|---|
| Dec | Hex | ||||
| 1 | 0x01 | Payload Format Indicator | Byte | PUBLISH, Will Properties | 是否被UTF-8编码。可选有:0-原始二进制数据 1-被UTF-8编码后的数据 |
| 2 | 0x02 | Message Expiry Interval | Four Byte Integer | PUBLISH, Will Properties | 消息的过期时间 |
| 3 | 0x03 | Content Type | UTF-8 Encoded String | PUBLISH, Will Properties | 消息类型,增加消息的可描述性,可选;例如: Application/json |
| 8 | 0x08 | Response Topic | UTF-8 Encoded String | PUBLISH, Will Properties | 请求响应模式,处理后需要发送的topic(处理该消息的client手动发送) |
| 9 | 0x09 | Correlation Data | Binary Data | PUBLISH, Will Properties | Response Topic的标识数据 |
| 11 | 0x0B | Subscription Identifier | Variable Byte Integer | PUBLISH, SUBSCRIBE | topic的标识,方便接收方处理 |
| 17 | 0x11 | Session Expiry Interval | Four Byte Integer | CONNECT, CONNACK, DISCONNECT | 会话的过期时间,如果会话断开但cleanSession(cleanStart)=true的话,会话会被保留至设置的时间,若中间重新连接将会出清除旧的会话,并且生成新的会话;如果cleanSession(cleanStart)=false时,断开连接后在设置的时间内,将会服用旧的会话; |
| 18 | 0x12 | Assigned Client Identifier | UTF-8 Encoded String | CONNACK | 客户端id,必须唯一,5.0之后如果不携带则会在broker生成一个并返回,3.1.1版本则需要强制携带 |
| 19 | 0x13 | Server Keep Alive | Two Byte Integer | CONNACK | 5.0新增;在服务端针对链接的ack的时候携带,broker期望的 keep alive时间;有限制按规范若ack携带使用broker 期望的值 |
| 21 | 0x15 | Authentication Method | UTF-8 Encoded String | CONNECT, CONNACK, AUTH | 5.0新增;增强认证方式,不止用户名密码,可以使用OAuth,Kerberos等。 |
| 22 | 0x16 | Authentication Data | Binary Data | CONNECT, CONNACK, AUTH | 5.0新增;配套上述Authentication Method,认证数据; |
| 23 | 0x17 | Request Problem Information | Byte | CONNECT | 客户端设置服务端异常消息内容,如果设置为0,则遇到错误时减少或者不返回错误信息,以减少带宽和提高隐私性。 |
| 24 | 0x18 | Will Delay Interval | Four Byte Integer | Will Properties | 医嘱消息的延时时间,如果没有这是则立刻发布;常用的作用是防止设备在网络不稳定的场景下,发送不必要的医嘱消息; |
| 25 | 0x19 | Request Response Information | Byte | CONNECT | 客户端是否希望服务器在返回 CONNACK 报文时提供响应信息。 |
| 26 | 0x1A | Response Information | UTF-8 Encoded String | CONNACK | 0x19设置为1时,链接响应对应的返回内容; |
| 28 | 0x1C | Server Reference | UTF-8 Encoded String | CONNACK, DISCONNECT | Server Reference是一个UTF-8编码的字符串,它通常包含一个URI或类似的地址,指示客户端应该尝试连接的另一个服务器或者端点。 使用场景: - 复杂均衡 在负载均衡环境中,服务器可能会根据当前的负载或其他策略,引导客户端连接到负载较少的服务器。 - 故障转移 如果当前服务器正在维护或者不可用,它可以使用Server Reference 将客户端重定向到一个备用服务器。 - 地理分布 对于跨地域的部署,服务器可能会指示客户端连接到地理上更近的服务器,以减少延迟。 连接被拒绝的情况: Server Reference 通常在连接被断开时结合错误码同时下发,根据错误码和对应的 Server Reference决定下一步行动 |
| 31 | 0x1F | Reason String | UTF-8 Encoded String | CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH | 异常或者错误原因,常在各种应答报文中出现,具体请参考2.4 Reason Code 备注:为了可读性并没有使用数字代号的方式; |
| 33 | 0x21 | Receive Maximum | Two Byte Integer | CONNECT, CONNACK | 定义了客户端或者服务端可以处理的并发未确认PUBLISH 报文的最大数量。 |
| 34 | 0x22 | Topic Alias Maximum | Two Byte Integer | CONNECT, CONNACK | 通过该属性告知客户端它愿意接受的最大主题别名数量,若不设置默认为0表示不使用别名; |
| 35 | 0x23 | Topic Alias | Two Byte Integer | PUBLISH | 主题别名,在首次使用别名后可不再显示传递Topic只携带topic alias即可; 使用分两步: 1. 第一步携带 Topic Alias PUBLISH Topic Name: devices/sensors/temperature/room1 Topic Alias: 1 Payload: 22.5°C 2. 第二步忽略 Topic Name即可。 PUBLISH Topic Alias: 1 Payload: 23.0°C 若需要重新设置别名则重复第一步即可; |
| 36 | 0x24 | Maximum QoS | Byte | CONNACK | Broker 在连接应答时针对 QoS的最高限制进行说明;0/1/2; |
| 37 | 0x25 | Retain Available | Byte | CONNACK | Broker 在连接应答时针对是否支持 Retain 消息进行说明;0不支持;1支持; |
| 38 | 0x26 | User Property | UTF-8 String Pair | CONNECT, CONNACK, PUBLISH, Will Properties, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, AUTH | 用户自定义参数 格式: 0x26[key-len]xxxx[val-len]xxxx 0x26[key-len]xxxx[val-len]xxxx 注意长度:均为2个字节; |
| 39 | 0x27 | Maximum Packet Size | Four Byte Integer | CONNECT, CONNACK | 后续publish报文的最大值;如果不设置默认为65535; |
| 40 | 0x28 | Wildcard Subscription Available | Byte | CONNACK | Broker 在连接应答时针对是否支持Topic通配符 + # 的支持的回应。 |
| 41 | 0x29 | Subscription Identifier Available | Byte | CONNACK | Broker 在连接应答时针对是否支持订阅标识符的回应;0-不支持;1-支持; |
| 42 | 0x2A | Shared Subscription Available | Byte | CONNACK | Broker 在连接应答时针对是否支持共享订阅的回应;0-不支持;1-支持; |
1.4.3 载荷
载荷即当前消息的核心内容并无其限制,不过最大限度为256MB(固定头的标志位限制)
以下是各报文类型是否包含Payload的表格展示;参考2.3 Payload
| MQTT Control Packet | Payload |
|---|---|
| CONNECT | Required |
| CONNACK | None |
| PUBLISH | Optional |
| PUBACK | None |
| PUBREC | None |
| PUBREL | None |
| PUBCOMP | None |
| SUBSCRIBE | Required |
| SUBACK | Required |
| UNSUBSCRIBE | Required |
| UNSUBACK | Required |
| PINGREQ | None |
| PINGRESP | None |
| DISCONNECT | None |
| AUTH | None |
1.4.4 Topic和Topic Filter
消息的主题和将要订阅消息的主题(可通配) Topic和Topic Filter在格式上虽然相似,但它们的用途和使用方式有所不同。
格式:
Topic: 用于指定发布消息的目标主题。格式为多层级字符串,以斜杠(/)分隔,例如:a/b/c/d、ab/x、a/b。
Topic Filter: 用于订阅时的主题过滤器。格式与Topic相似,但可以包含通配符,以匹配多个主题。例如:a/+/c/d(+匹配单层级)和a/b/#(#匹配多层级,必须放在末尾)。
使用场景:
Topic: 由消息发布者使用,需要明确指定消息的具体目标主题。
Topic Filter: 由消息订阅者使用,可通过通配符同时监听多个相关的Topic,从而接收多种主题的消息。
1.4.5 共享订阅
共享策略指向共享组内的成员转发消息时如何确定哪一个成员负责接收该消息,MQTT协议在这方面没有明确规定,结合共享订阅的使用场景,BifroMQ提供两种策略实现:
- 随机选择($share/{groupName}/topicFilter):使用$share作为共享订阅的前缀,适用于消息处理逻辑无顺序性要求的应用。
- 有序绑定($oshare/{groupName}/topicFilter):使用$oshare作为共享订阅的前缀,这种模式将来自同一客户端连接和同一主题的消息按发布顺序转发给同一个订阅者,适用于消息有顺序性要求的应用。
注意:共享策略与共享组名的组合是唯一的,即使用$share和$oshare创建相同组名的共享订阅,实际上会创建组名相同但共享策略不同的两个共享订阅组。
2. BifroMQ的基础特性
2.1 简介及背景介绍
BifroMQ是一个基于Java实现的高性能、分布式、原生多租户可拓展的MQTT Broker,前身来自百度物联网团队的Iot Broker开源而来。
BifroMQ 的名称灵感来自于北欧神话中的Bifröst,一座彩虹之桥,链接着人类的世界 Midgard 和神祇居住的世界 Asgard。Bifröst 作为两个世界间的坚实而灵活的通道,BifroMQ 同样旨在成为连接各种系统或应用的枢纽,通过消息传递实现它们之间的交流。这与 MQTT 中间件在分布式系统中扮演的角色,即处理和转发消息,极为相似。
此外,Bifröst 的坚固性寓意着 BifroMQ 在稳定性和可靠性方面的卓越表现,而它的灵活性则象征着 BifroMQ 在可扩展性和适应性上的优势。总的来说,BifroMQ 是一个坚固、灵活的 MQTT 中间件,作为连接不同系统或应用的桥梁。 —— bifromq.io/zh-Hans/doc…
2.2 BifroMQ与其他MQTT Broker的比较
| 功能 | BifroMQ | EMQX |
|---|---|---|
| 发布订阅 | ✔️ | ✔️ |
| 共享订阅 | ✔️ | ✔️ |
| 数据持久化(运行) | ✔️(内置) | ✔️(拓展) |
| Session消息 | ✔️ | ✔️ |
| 保留消息 | ✔️ | ✔️ |
| 动态认证 | ✔️(插件方式) | ✔️ |
| 多协议 | ✔️ | ✔️ |
| 延迟消息 | ✔️ | ✔️ |
| ACL控制 | ✔️(插件方式) |
2.3 涉及的技术栈
| 技术栈名字 | 描述 |
|---|---|
| MQTT | 轻量级的发布/订阅消息传输协议 |
| Netty | 异步事件驱动网络应用框架 |
| gRPC (Protobuf) | Google开发的现代的开源远程过程调用(RPC)框架 |
| RxJava | 响应式框架,主要进行执行任务编排 |
| Caffeine | 高性能缓存组件 |
| Micrometer | 进程资源监控,Prometheus的指标收集客户端 |
| Gossip | 集群管理,分布式协议 |
| Raft | 存储相关,分布式系统的一致性算法 |
| CRDT | 分布式系统的数据结构,旨在解决多个副本之间的数据一致性问题 |
| RocksDB | 高性能的嵌入式键值数据库,最初由Facebook基于Google的LevelDB进行开发 |
2.4 BifroMQ如何实现MQTT协议的基本编解码的?
BifroMQ使用了Netty作为MQTT协议的原生编解码组件进行MQTT协议的网络交互,BifromqMQ本身不专门对MQTT协议的协议细节和各报文类进行重复的处理。
接收数据:通过 ByteToMessageDecoder 解码为 MQTT 协议对象。
发送数据:通过 MessageToByteEncoder 将 MQTT 协议对象编码为字节流。
2.5 BifroMQ的特色功能和优势
- 全面支持 MQTT 协议,支持 MQTT 3.1、3.1.1 和 5.0 协议标准,发布/订阅模型(Pub/Sub),会话管理(包括持久会话和临时会话),保留消息和遗嘱消息。
- 高性能消息分发,分布式架构,优化消息投递的性能,适合大规模连接和高吞吐场景。
- 持久化和高可靠性,内置base-kv存储引擎,支持分布式存储,多副本和故障恢复。
- 原生支持多租户(Multi-Tenancy),允许不同的用户/应用独立运行和管理,每个租户的隔离性可以确保数据安全和资源独立。
- 灵活的插件设计,可扩展性,模块化设计,支持通过插件机制扩展功能。
- 实时监控和指标,提供详细的运行时性能指标,包括连接数、消息吞吐量、延迟等。
3. BifroMQ的分布式特性
3.1 BifroMQ系统架构
首先BifroMQ是基于去中心化集群管理的模块化架构,BifroMQ按照具体能力进行了分层,分成了 Agent Cluster(Overlay Cluster)和Host Cluster(Underlay Cluster)两层架构,其中底层 Host Cluster负责集群建立BifroMQ实例进程。Agent Cluster将负责具体功能的逻辑服务,通过base-rpc框架为基础统一实现,利用Agent Cluster的能力来实现服务的注册和发现。
3.2 BifroMQ如何实现分布式集群
BifroMQ采用Gossip类的Membership协议实现汲取成员的失败检测,并且对Membership信息的同步机制进行了优化,是的整体上具有以下的技术特性:
- 集群构建无需依赖传统的注册中心或命名服务,有效消除了单点故障的运维风险,显著提升了集群的高可用性。
- 通过采用SWIM协议,Underlay Cluster能够确保节点间探活机制的准确性,保障集群拓扑的最终一致性。
- 使用CRDT技术实现集群Membership信息在节点间同步,可以同时做到极低的带宽占用和同步时效。
3.2.1 集群的组建过程
在去中心化的集群架构中,每个集群成员的地位是平等的,不存在专门负责管理集群拓扑信息的管控角色节点。从更高的视角来观察,运行BifroMQ StandardCluster服务进程的任何节点,实际上都可以视为一个拥有“单个节点”的独立集群。因此,集群的构建实质上是各独立集群的融合过程。为了实现这一过程,base-cluster框架提供了“join”操作,以便于这些独立集群的有效合并。
3.2.2 失败检测与自动驱逐
在BifroMQ中,当节点正常退出时,它会主动清理自身在集群中的注册身份,并与集群中的其他节点同步此变化。此外,每个节点持续进行对其他进行节点失败检测(Failure Detection)。一旦检测到异常,它会立即将该节点的相关信息从本地的集群Membership中移除,并迅速完成集群间Membership的同步,防止这些不再活跃的节点对集群的正常功能产生影响。
3.2.3 脑裂恢复
对于去中心化的集群服务来说,网络分区(Network Partition)导致的导致集群脑裂是个不能避免的问题,即单个集群分裂成了多个互相隔离的集群。 base-cluster对于可能出现的脑裂故障做了保障,进一步提高了集群部署的高可用性,具体过程如下:当网络发生分区时,分裂被隔离的集群彼此都会检测到对方节点的失败,这些失败被移除的节点会加入至本地的"愈合列表",列表中的成员会被定时尝试join,直到超过部署时设定的 MTTR(Mean Time To Repair)。不难发现,脑裂恢复的过程实际上与上文提到的集群构建过程是一致的。
3.3 BifroMQ的集群类型
BifroMQ的Agent Cluster的集群类型分为两类: 无状态集群(RPC服务),有状态集群(bask-kv内置的分布式存储引擎);
3.3.1 RPC服务集群
RPC服务集群成员通常被定义为客户端和服务端两种角色。利用Agent Cluster的特性,RPC的客户端与服务端无需依赖外部的注册中心,即可实现高效的服务端发现与多样的客户端请求逻辑。 实现的有:DistService,InboxService,RetainService等
3.3.2 有状态服务
在BifroMQ的有状态子服务集群中,内置了强一致性的分布式KV存储引擎(base-kv)。这个引擎具备基于Multi-Raft的sharding功能,是BifroMQ高度可靠性的关键组成部分。集群的Membership信息由Agent Cluster维护,而每个Range分片内的副本则通过Raft协议来实现强一致性同步。因此,要确保有状态服务的高可靠性,就必须充分利用并符合Raft协议的特性要求。 本次存储引擎部分不展开
3.3.3 服务组件列表
下列各服务列表均可单独作为独立的负载集群相互配合使用。
- bifromq-mqtt:负责处理 MQTT 协议的长连接负载
- bifromq-dist:负责维护 MQTT 订阅状态的高可靠存取和大规模消息路由
- bifromq-inbox:负责 CleanSession=false 模式下订阅方会话消息的高可靠存取
- bifromq-retain:负责 Retain 消息的高可靠存取
4. BifroMQ的代码设计
4.1 BifroMQ的工程架构设计
工程结构中主要子工程分为两类,其中一类为底层的RPC和集群维持以及分布式存储等,另一类为实现Broker针对MQTT协议规范的核心工程。
| 工程名字 | 描述 |
|---|---|
| base-cluster | 集群的最最基层的建设组件,提供Gossip类的Membership协议实现去中心化无主集群 |
| base-crdt | 维护分布式环境中的数据一致性,提高可用性和容错能力 |
| base-env | 环境变量相关,包括用户使用SPI方式自定义自己的环境配置 |
| base-hookloader | 加载SPI类 |
| base-kv | 分布式存储相关 |
| base-logger | 日志相关 |
| base-rpc | 服务间通信的基于GRPC的封装 |
| base-scheduler | 可控制速率的消息发送组件,通过线程池或协程池管理并发任务,提升系统的执行效率,动态分配资源,避免资源过载。 |
| bifromq-apiserver | 对外暴露标准化的 API,方便用户通过程序化方式管理和使用 BifroMQ。 |
| bifromq-deliverer | 实现消息从发布者到订阅者的高效路由和分发和对消息持久化 |
| bifromq-dist | 负责维护 MQTT 订阅状态的高可靠存取和大规模消息路由 |
| bifromq-inbox | 负责 CleanSession=false 模式下订阅方会话消息的高可靠存取 |
| bifromq-metrics | 监控和指标收集模块 |
| bifromq-mqtt | 负责处理 MQTT 协议的长连接负载 |
| bifromq-plugin | 负责拓展相关模块 |
| bifromq-retain | 负责 Retain 消息的高可靠存取 |
| bifromq-sysprops | 系统属性配置模块,专门用于管理和提供系统级的配置属性。这些属性通常用于控制系统的全局行为、调整性能参数以及为其他模块提供所需的配置。 |
| build | Broker启动引导模块和插件样例模块 |
4.2 BifroMQ的核心代码解析
4.2.1 启动类拆解
启动类:Build -> build-bifromq-starters -> com.baidu.bifromq.starter.StandaloneStarter#main
大致过程
main() -> 配置加载 -> 系统属性设置 -> 初始化核心模块
-> 集群注册 -> 注册组件 -> 启动网络服务 -> 注册关闭事件
整个启动过程分为两个部分:
1、初始化部分 代码位置:com.baidu.bifromq.starter.StandaloneStarter#init
- 缺省配置补全
- 插件加载
- 各组件的初始化,agent、crdt、distClient/distServer、以及mqttBroker等
2、启动部分
4.2.2 会话管理
下面的内容请结合给出的代码引用位置结合源码进行分析,否则将会比较吃力。另外当前工程进行了很深度抽象,有不少的部分分布在各种父类中不是很好找。
服务端点
文章前文介绍中负责mqtt的会话管理的是 MqttBroker 组件,所以服务端点也在该组件中。通过 com.baidu.bifromq.starter.StandaloneStarter#start (图4.2.2.1-1) 中的 mqttBroker.start();(图4.2.2.1-1) 启动部分中可以看到 com.baidu.bifromq.mqtt.AbstractMQTTBroker#start 绑定TCP端口,也就意味着服务端点的实际位置(本次以TCP端口绑定为例,图4.2.2.1-2),结合 图4.2.2.1-3 和 图4.2.2.1-4 可以看出来,本质MQTT协议的编解码和TCP服务容器等基础操作均由Netty框架支持;图4.2.2.1-4 中的 new MQTTPreludeHandler(builder.connectTimeoutSeconds)才是我们真正处理一个MQTT协议报文的开始。
图4.2.2.1-1
图4.2.2.1-2
图4.2.2.1-3
图4.2.2.1-4
如何建立连接
1、连接预处理 承接上文的 MQTTPreludeHandler 类,该类为BifroMQ的前置处理器,主要负责连接报文和协议版本的分拣。如 图4.2.2.2-1,我们只关注 chnanelRead函数,首先当钱 MQTTPreludeHandler 是一个Netty的ChannelDuplexHandler双工通道处理器,其中 chnanelRead 为入站处理过程中读取MQTT连接的报文后的主要过程(如需要先了解Netty相关知识)。 图4.2.2.2-1可以看出来当前处理器只处理connect类型的报文(其他报文拦截代码部分被折叠了)并且按照MQTT协议版本进行了链接处理器的分类,其中3和4版本为同一个处理器。 红框中的 addAfter函数 和 ctx.fireChannelRead(connectMessage); 表示添加一个处理器并立即执行。后续的操作将会移除当前处理器后续将被废弃。
2、真正的连接处理部分 我们进入具体的连接处理器中,如 v5版本处理器:com.baidu.bifromq.mqtt.handler.v5.MQTT5ConnectHandler,其中v3和v5的大部分内容均在其父类,其中图4.2.2.2-2即在其父类中,可以看到 chnanelRead 中分别进行了 协议报文检查;租户总链接数检查;租户session内存检查;租户每秒连接数检查;以及通用型校验;上述检查即为租户资源的逻辑隔离的部分体现,保证了broker的安全性; 后续可以看到连接按照 isCleanSession进行了区分,该区分表示当前的连接会话是否进行持久化操作,结合图4.2.2.2-3和图4.2.2.2-4在isCleanSession为false的时候,inbox组件介入并提供持久化能力(本次不进行展开)。后续setupTransientSessionHandler 或者 setupPersistentSessionHandler 将会进行实际会话的构建。
图4.2.2.2-1
图4.2.2.2-2
图4.2.2.2-3
图4.2.2.2-4
连接会话形成的步骤
我们以瞬时会话(isCleanSession=true)为例,结合图4.2.2.3-1和图4.2.2.3-2可以看到,在会话处理器之前增加了消息裁剪过滤器和速率控制器以及租户粒度指标收集动作。后续在会话处理器生成后替换当前连接处理器作为该连接通道的核心处理器,至此MQTTPreludeHandler 预检处理器和 MQTTConnectHandler链接处理器已经完全退出当前处理器流水线。
会话的管理位置,由图4.2.2.3-3 可以看到 MQTTSessionHandler(会话处理器MQTTTransientSessionHandler和MQTTPersistentSessionHandler的父类)的handlerAdded函数可以看到sessionCtx.localSessionRegistry.add(channelId(), this); 该动作即为将当前处理器作为会话放入会话管理器中。
补充:handlerAdded 的意思是在加入Netty的Piple时的执行动作,可以粗浅理解为初始化动作。
图4.2.2.3-1
图4.2.2.3-2
图4.2.2.3-3
5. 插件和拓展相关
拓展插件在BifroMQ中共有五种,分别为 Auth-Provider-Plugin、Event-Collector-Plugin、Resource-Throttler-Plugin、SettingProvider-Plugin、SubBroker-Plugin。使用时需要将对应的插件包放入 plugins 目录和配置对应的 standalone.yml 即可。
插件的实现方式
- 引入对应的BifroMQ提供的拓展依赖包。
- 插件实现共两种方式: 2.1. 继承抽象类com.baidu.bifromq.plugin.BifroMQPlugin; 2.2.使用类注解:org.pf4j.Extension 和 实现 (IAuthProvider,IEventCollector,IResourceThrottler,ISettingProvider,ISubBroker);
其中第一种为通用的插件没有特定意义;第二种为BifroMQ预设的几类特定功能插件,下面将会进行简单的介绍;
快速开始插件开发 执行以下 Maven 命令,快速启动 BifroMQ 插件开发:
mvn archetype:generate \
-DarchetypeGroupId=com.baidu.bifromq \
-DarchetypeArtifactId=bifromq-plugin-archetype \
-DarchetypeVersion=<BIFROMQ_VERSION> \
-DgroupId=<YOUR_GROUP_ID> \
-DartifactId=<YOUR_ARTIFACT_ID> \
-Dversion=<YOUR_PROJECT_VERSION> \
-DpluginName=<YOUR_PLUGIN_CLASS_NAME> \
-DpluginContextName=<YOUR_PLUGIN_CONTEXT_CLASS_NAME> \
-DbifromqVersion=<BIFROMQ_VERSION> \
-DinteractiveMode=false
请将 <YOUR_GROUP_ID>、<YOUR_ARTIFACT_ID>、<YOUR_PROJECT_VERSION>、<YOUR_PLUGIN_CLASS_NAME> 和 <YOUR_PLUGIN_CONTEXT_CLASS_NAME> 替换为您的具体信息。该命令生成一个准备就绪的、结构清晰的多模块项目,专为 BifroMQ 插件开发而设计。 重要提示:原型的版本应为 3.2.0 或更高版本,因为该原型从 3.2.0 版本开始兼容。确保 <BIFROMQ_VERSION> 设置正确。 除了插件开发的基础代码框架外,生成的 BifroMQ 插件项目还为您提供以下机制:
- PluginContext:定义插件上下文,方便传递插件运行所需信息。
- 配置文件:使用独立的 config.yaml 文件来配置插件。
- 日志配置:使用独立的 logback.xml 文件来配置插件日志。
5.1 IAuthProvider
会话链接时身份验证组件,进行身份识别和权限校验等操作,如不实现默认 com.baidu.bifromq.plugin.authprovider.DevOnlyAuthProvider。若链接时不携带用户名和密码默认: tenantId=DevOnly ,userId=DevUser。
若手动创建工程,需引入的BifroMQ提供的工程依赖
<dependency>
<groupId>com.baidu.bifromq</groupId>
<artifactId>bifromq-plugin-auth-provider</artifactId>
<version>X.Y.Z</version> <!--replace X.Y.Z with the latest version number-->
</dependency>
注意:认证插件在运行时需要在配置中指定全限定名才会生效。具体standalone.yml文件位置:authProviderFQN;
认证接口
其中MQTT5的认证自动适配下方的MQTT3的接口,只需要实现下方MQTT3认证方法即可。
/**
* Implement this method to hook authentication logic of mqtt3 client into BifroMQ.
*
* @param authData the authentication data
*/
CompletableFuture<MQTT3AuthResult> auth(MQTT3AuthData authData);
5.2 IEventCollector
BifroMQ会在运行过程中发生的各种事件进行上报,其中启动时,BifroMQ会扫描Plugins目录并加载所有的对应实现,避免report方法内包含过度复杂的逻辑,因为当前插件在核心工作线程执行,有性能风险。
若手动创建工程,需引入的BifroMQ提供的工程依赖
<dependency>
<groupId>com.baidu.bifromq</groupId>
<artifactId>bifromq-plugin-event-collector</artifactId>
<version>X.Y.Z</version> <!--replace X.Y.Z with the latest version number-->
</dependency>
实现方法
当事件生成时,BifroMQ调用所有已加载EventCollector的report()方法。方法签名如下:
public void report(Event<?> event);
事件类型
BifroMQ运行时生成的事件对象类型都继承自Event 类。每个特定的事件类对应一个EventType枚举,可以通过对象上的type() 方法访问,以便于实现事件过滤逻辑。hlc()方法允许检索事件对象的时间戳。BifroMQ的时间戳是偏序的,反映了相关事件发生的顺序。这对于在BifroMQ的分布式部署中处理事件特别有用。
5.3 IResourceThrottler
租户级资源限制插件,为了防止任何单个租户过度使用造成对其他租户的影响,需要在全局层面运行时控制每个租户的资源投入。租户级全局资源限制需要实时监控每个租户的集群资源使用情况,BifroMQ提供了租户级Metrics,用于衡量资源量级和速率,指标类型包括:Gauge、Counter和Summary,TenantResourceType定义的资源类型参考文档。
若手动创建工程,需引入的BifroMQ提供的工程依赖
<dependency>
<groupId>com.baidu.bifromq</groupId>
<artifactId>bifromq-plugin-resource-throttler</artifactId>
<version>X.Y.Z</version> <!--replace X.Y.Z with the latest version number-->
</dependency>
BifroMQ一次只允许运行一个Resource Throttler实例。需要在配置文件中指定实现类的完全限定名(FQN):
resourceThrottlerFQN: "YOUR_SETTING_PROVIDER_CLASS"
Metrics
因为hasResource方法会被频繁调用,BifroMQ记录并输出以下指标,以帮助插件实现者观察插件接口方法的性能指标:
| Metric Name | Meter Type | Tag (method) | Description |
|---|---|---|---|
| call.exec.timer | TIMER | ResourceThrottler/hasResource | Latency for hasResource call |
| call.exec.fail.count | COUNTER | ResourceThrottler/hasResource | Fail counter for hasResource call |
5.4 ISettingProvider
BifroMQ定义了一类租户级设置(Settings),允许在运行时修改,从而实现针对单个租户动态调整BifroMQ的服务行为。Setting Provider Plugin的目的是在运行时为这些设置提供自定义值。
若手动创建工程,需引入的BifroMQ提供的工程依赖
<dependency>
<groupId>com.baidu.bifromq</groupId>
<artifactId>bifromq-plugin-setting-provider</artifactId>
<version>X.Y.Z</version> <!--replace X.Y.Z with the latest version number-->
</dependency>
BifroMQ只允许运行一个Setting Provider实例。需要在配置文件指定要加载的具体实现类的完全限定名(FQN):
settingProviderFQN: "YOUR_SETTING_PROVIDER_CLASS"
此方法将由BifroMQ的线程池调用,因此在实现Setting Provider Plugin时需要牢记以下两点:
- 避免在provide方法中包含繁重的业务逻辑,以确保它能够快速返回,否则会对BifroMQ的性能产生负面影响。
- 当无法快速确定设置的值时,可以返回null,此时设置将继续使用其当前值。插件实现可以将多个Setting的请求聚合后异步处理。
初始值调整
初始的默认值,这些设置的初始值可以通过JVM启动参数进行调整。这使得在每个租户的运行时定制之前,可以对系统范围内的默认值进行调整。例如,要默认禁用MQTT版本3.1客户端连接,可以使用参数 -DMQTT3Enabled=false 启动JVM。
通过Setting Provider插件进行定制
租户设置的管理责任被转移到业务端实现。通过开发和集成自定义Setting Provider插件来实现对这些设置的定制和运行时调整。这种方法允许企业根据变化的操作条件、监管要求或特定的业务逻辑动态调整设置。
5.5 ISubBroker
当前未对外开放,本次不进行展开
SubBroker Plugin是BifroMQ集成"多租户订阅下游服务"的标准方式,逻辑上BifroMQ内置了两个"多租户订阅下游服务":MQTTServer和InboxService,分别实现MQTT的clean session true和clean session false的协议能力。其他与BifroMQ对接的第三方多租户下游服务,可以实现该接口,实现与BifroMQ在运行时的集成。多租户系统间的对接要求所有多租户系统都共享同一个"租户源"。