R
写给RocketMQ架构应用入门,内容涉及它的设计机理以及推到出来的应用注意事项,入门人员请看。
稍微涉及技术细节,留以我设计中间件时参考,将来整理深度文档时会抽取走,入门人员可以无视。
以下RocketMQ简称为 RQ,理论部分采用版本为 3.2.4,测试部分采用版本为 3.2.6。
MQ 的需求
我们对 MQ 的需求,相比 JMS 标准有几点要求更高:
1. 必须优美灵活地支持集群消费。
2. 尽量支持消息堆积。
3. 服务高可用性和消息可靠性。
4. 有起码的运维工具做集群管理和服务调整。
其他 提供顺序消息、事务、回溯等面向特别场景的功能更好,目前暂不需要。
RQ 架构
RQ 的基本组成包括 nameserver、broker、producer、consumer 四种节点,前两种构成服务端,后两种在客户端上。
还有其他辅助的进程,不提。
NameServer 的基本概念
在没有 NameServer 的中间件中,服务端集群就由干活的 broker 组成 ,其中的实例分主从两种角色。那么客户端就要知道,需要连接到哪个地址的 broker 上去做事情,于是客户端就需要配置服务端机器的 IP 地址,如果服务端部署结构复杂,客户端的配置结构也挺复杂,更讨厌的是甚至可能需要客户端也得更新地址配置。由于有了两种思路的方案:
一是引入 NameServer,负责提供地址。客户端只需要知道 NameServer 机器的地址,需要找服务器干活的时候,先问 NameServer 我该去找哪个服务器。这样,因为 NameServer 很简单而不容易出故障,所以极少发生架构调整。而结构复杂的 broker 部分,无论怎么调整,客户端都不用再操心。
RQ 2.x 用的是 Zookeeper 做 NameServer,3.x 用的是自己搞的独立服务,不知道为啥,不过代码貌似不复杂。
二是引入反向代理,就是把服务端地址配置成虚拟 IP 或域名这种思路,这一个地址背后其实可能是一个集群,客户端发起请求后,由网络设施来中转请求给具体服务器。
两者各有优劣,综合使用也挺正常。
NameServer 的工作机制
I.NameServer 自身之间的机制
可以启动多个实例,相互独立,不需要相互通信,可以理解为多机热备。
II.NameServer 与 broker 之间
Broker 启动后,向指定的一批 NameServer 发起长连接,此后每隔 30s 发送一次心跳,心跳内容中包含了所承载的 topic 信息;NameServer 会每隔 2 分钟扫描,如果 2 分钟内无心跳,就主动断开连接。
当然如果 Broker 挂掉,连接肯定也会断开。
一旦连接断开,因为是长连接,所以 NameServer 立刻就会感知有 broker 挂掉了,于是更新 topic 与 broker 的关系。但是,并不会主动通知客户端。
III.NameServer 与客户端之间
客户端启动时,要指定这些 NameServer 的具体地址。之后随机与其中一台 NameServer 保持长连接,如果该 NameServer 发生了不可用,那么会连接下一个。
连接后会定时查询 topic 路由信息,默认间隔是 30s,可配置,可编码指定 pollNameServerInteval。
(注意是定时的机制,不是即时查询,也不是 NameServer 感知变更后推送,所以这里造成接收消息的实时性问题)。
NameServer 的部署与应用
I. 运行 NameServer
启动: nohup mqnamesrv &
终止:sh ./mqshutdown
Useage: mqshutdown broker | namesrv
II. Broker 指定 NameServer
有几种方式 1.启动命令指定nohup mqbroker -n
"192.168.36.53:9876;192.168.36.80:9876"
&
2.环境变量指定export NAMESRV_ADDR=
192.168.36.53:
9876
;
192.168.36.80
:
9876
3.配置指定bin 和 conf 下面的配置文件里面有
root @rocketmq -master1 bin]# sh mqbroker -m namesrvAddr= |
III.客户端指定 NameServer
有几种方式:
1.编码指定
producer.setNamesrvAddr(
"192.168.36.53:9876;192.168.36.80:9876"
);
所以这里指定的并不是 broker 的主主或主从机器的地址,而是 NameServer 的地址。
2.java 启动参数-Drocketmq.namesrv.addr=
192.168.36.53:9876;192.168.36.80:9876
3.环境变量export NAMESRV_ADDR=192.168.36.53:9876;192.168.36.80:9876
4.服务
客户端还可以配置这个域名 jmenv.tbsite.alipay.net 来寻址,就不用指定 IP 了,这样 NameServer 集群可以做热升级。
该接口具体地址是http://jmenv.tbsite.net:8080/rocketmq/nsaddr
(理论上,最后一种可用性最好;实际上,没试出来。)
Broker 的机制
I.消息的存储
1.topic 与 broker 是多对多的关系,一个 topic 可以做分区配置的,使得可以分散为队列交付给多个 btoker。分区的设计采用的是偏移量做法。2.Broker 是把消息持久化到磁盘文件的,同步刷盘就是写入后才告知 producer 成功;异步刷盘是收到消息后就告知 producer 成功了,之后异步地将消息从内存(PageCache)写入到磁盘上。(注意此处涉及到磁盘写入速度是否大于网卡速度的问题,应用的不好可能造成消息堆积)3.磁盘空间达到 85%之后,不再接收消息,会打印日志并且告知 producer 发送消息失败。4.持久化采取的是 ext4 文件系统,存储的数据结构另有其他文档,运维时需要处理文件目录时另说。
II.消息的清理
1.每隔 10s 扫描消息,可以通过 cleanResourceInterval 配置。2.每天 4 点清理消息,可以通过 deleteWhen 配置。磁盘空间达到阈值时也会启动。3.文件保留时长为 72 小时,可以通过 fileReservedTime 配置。也就是消息堆积的时限。
III.消息的消费
1.IO 用的是文件内存映射方式,性能较高,只会有一个写,其他的读。顺序写,随机读。
2. 零拷贝原理:
以前使用 linux 的 sendfile 机制,利用 DMA(优点是 CPU 不参与传输),将消息内容直接输出到 sokect 管道,大块文件传输效率高。缺点是只能用 BIO。
于是此版本使用的是 mmap+write 方式,代价是 CPU 多耗用一些,内存安全问题复杂一些,要避免 JVM Crash。
IV.Topic 管理
1.客户端可以配置是否允许自动创建 Topic,不允许的话,要先在 console 上增加此 Topic。同时提供管理操作。
V.物理特性
1.CPU:Load 高,但使用率低,因为大部分时间在 IO Wait。
2. 内存:依旧需要大内存,否则 swap 会成为瓶颈。
3. 磁盘:IO 密集,转速越高、可靠性越高越好。
VI.broker 之间的机制
单机的刷盘机制,虽然保障消息可靠性,但是存在单点故障影响服务可用性,于是有了 HA 的一些方式。
1.主从双写模式,在消息可靠性上依然很高,但是有小问题。
a.master 宕机之后,客户端会得到 slave 的地址继续消费,但是不能发布消息。
b.客户端在与 NameServer 直接网络机制的延迟下,会发生一部分消息延迟,甚至要等到 master 恢复。
c.发现 slave 有消息堆积后,会令 consumer 从 slave 先取数据。
2 异步复制,消息可靠性上肯定小于主从双写
slave 的线程不断从 master 拉取 commitLog 的数据,然后异步构建出数据结构。类似 mysql 的机制。
VII.与 consumer 之间的机制
1.服务端队列
topic 的一个队列只会被一个 consumer 消费,所以该 consumer 节点最好属于一个集群。
那么也意味着,comsumer 节点的数量>topic 队列的数量,多出来的那些 comsumer 会闲着没事干。
举简单例子说明:
假设 broker 有 2 台机器,topic 设置了 4 个队列,那么一个 broker 机器上就承担 2 个队列。
此时消费者所属的系统,有 8 台机器,那么运行之后,其中就只有 4 台机器连接到了 MQ 服务端的 2 台 broker 上,剩下的 4 台机器是不消费消息的。
所以,此时要想负载均衡,要把 topic 的分区数量设高。
2.可靠性
consumer 与所有关联的 broker 保持长连接(包括主从),每隔 30s 发送心跳,可配置,可以通过 heartbeatBrokerInterval 配置。
broker 每隔 10s 扫描连接,发现 2 分钟内没有心跳,则关闭连接,并通知该 consumer 组内其他实例,过来继续消费该 topic。
当然,因为是长连接,所以 consumer 挂掉也会即时发生上述动作。所以,consumer 集群的情况下,消费是可靠的。
而因为 consumer 与所有 broker 都持有连接,所以可以两种角色都订阅消息,规则由 broker 来自动决定(比如 master 挂了之后重启,要先消费哪一台上的消息)。
3.本地队列
consumer 有线程不断地从 broker 拉取消息到本地队列中,消费线程异步消费。轮询间隔可指定 pullInterval 参数,默认 0;本地队列大小可指定 pullThresholdForQueue,默认 1000。
而不论 consumer 消费多少个队列,与一个 broker 只有一个连接,会有一个任务队列来维护拉取队列消息的任务。
4.消费进度上报
定时上报各个队列的消费情况到 broker 上,时间间隔可设 persistConsumerOffsetInterval。
上述采取的是 DefaultMQPushConsumer 类做的描述,可见所谓 push 模式还是定时拉取的,不是所猜测的服务端主动推送。不过拉取采用的是长轮询的方式,实时性基本等同推送。
VIII.与 producer 的机制
1.可靠性
a.producer 与 broker 的网络机制,与 consumer 的相同。如果 producer 挂掉,broker 会移除 producer 的信息,直到它重新连接。
b.producer 发送消息失败,最多可以重试 3 次,或者不超过 10s 的超时时间,此时间可通过 sendMsgTimeout 配置。如果发送失败,轮转到下一个 broker。
c.producer 也可以采用 oneway 的方式,只负责把数据写入客户端机器 socket 缓冲区。这样可靠性较低,但是开销大大减少。(适合采集小数据日志)
2.消息负载
发送消息给 broker 集群时,是轮流发送的,来保障队列消息量平均。也可以自定义往哪一个队列发送。
3.停用机制
当 broker 重启的时候,可能导致此时消息发送失败。于是有命令可以先停止写权限,40s 后 producer 便不会再把消息往这台 broker 上发送,从而可以重启。
sh mqadmin wipeWritePerm -b brokerName -n namesrvAddr
IX.通信机制
1.组件采用的是 Netty.4.0.9。
2.协议是他们自己定的新玩意,并不兼容 JMS 标准。协议具体内容有待我开发 C#版客户端时看详情。
3.连接是可以复用的,通过 header 的 opaque 标示区分。
Broker 的集群部署
一句话总结其特征就是:不支持主从自动切换、slave 只能读不能写,所以故障后必须人工干预恢复负载。
集群方式 | 运维特点 | 消息可靠性(master 宕机情况) | 服务可用性(master 宕机情况) | 其他特点 | 备注 |
---|---|---|---|---|---|
一组主主 | 结构简单,扩容方便,机器要求低 | 同步刷盘消息一条都不会丢 | 整体可用未被消费的消息无法取得,影响实时性 | 性能最高 | 适合消息可靠性最高、实时性低的需求。 |
一组主从 | 异步有毫秒级丢失;同步双写不丢失; | 差评,主备不能自动切换,且备机只能读不能写,会造成服务整体不可写。 | 不考虑,除非自己提供主从切换的方案。 | ||
多组主从(异步复制) | 结构复杂,扩容方便 | 故障时会丢失消息; | 整体可用,实时性影响毫秒级别该组服务只能读不能写 | 性能很高 | 适合消息可靠性中等,实时性中等的要求。 |
多组主从(同步双写) | 结构复杂,扩容方便 | 不丢消息 | 整体可用,不影响实时性该组服务只能读不能写。不能自动切换? | 性能比异步低 10%,所以实时性也并不比异步方式太高。 | 适合消息可靠性略高,实时性中等、性能要求不高的需求。 |
第四种的官方介绍上,比第三种多说了一句:“不支持主从自动切换”。这句话让我很恐慌,因为第三种也是不支持的,干嘛第四种偏偏多说这一句,难道可用性上比第三种差?
于是做了实验,证明第三种和第四种可用性是一模一样的。那么不支持主从切换是什么意思?推断编写者是这个意图:
因为是主从双写的,所以数据一致性非常高,那么 master 挂了之后,slave 本是可以立刻切换为主的,这一点与异步复制不一样。异步复制并没有这么高的一致性,所以这一句话并不是提醒,而是一个后续功能的备注,可以在双写的架构上继续发展出自动主从切换的功能。
架构测试总结:
1.其实根本不用纠结,高要求首选同步双写,低要求选主主方案。
2.最好不用一个机器上部署多个 broker 实例。端口容易冲突,根源问题还没掌握。
所以,建议采用多台机器,一台起一个 broker,构成同步双写的架构。也就是官方提供的这种物理和逻辑架构。
注意几个特征:
a.客户端是先从 NameServer 寻址的,得到可用 Broker 的 IP 和端口信息,然后自己去连接 broker。
b.生产者与所有的 master 连接,但不能向 slave 写入;而消费者与 master 和 slave 都建有连接,在不同场景有不同的消费规则。
c.NameServer 不去连接别的机器,不主动推消息。
客户端的概念
1.Producer Group
Producer 实例的集合。
Producer 实例可以是多机器、但机器多进程、单进程中的多对象。Producer 可以发送多个 Topic。
处理分布式事务时,也需要 Producer 集群提高可靠性。
2.Consumer Group
Consumer 实例 的集合。
Consumer 实例可以是多机器、但机器多进程、单进程中的多对象。
同一个 Group 中的实例,在集群模式下,以均摊的方式消费;在广播模式下,每个实例都全部消费。
3.Push Consumer
应用通常向 Consumer 对象注册一个 Listener 接口,一旦收到消息,Consumer 对象立刻回调 Listener 接口方法。所以,所谓 Push 指的是客户端内部的回调机制,并不是与服务端之间的机制。
4.Pull Consumer
应用通常主动调用 Consumer 从服务端拉消息,然后处理。这用的就是短轮询方式了,在不同情况下,与长轮询各有优点。
发布者和消费者类库另有文档,不提。
重要问题总结:
1.客户端选择推还是拉,其实考虑的是长轮询和短轮询的适用场景。
2.服务端首选同步双写架构,但依然可能造成故障后 30s 的消息实时性问题(客户端机制决定的)。
3.Topic 管理,需要先调查客户端集群机器的数目,合理设置队列数量之后,再上线。