NameServer 是专为 RocketMQ 设计的轻量级名字服务,它的源码非常精简,八个类 ,少于 1000 行代码。
这篇文章, 笔者会从基础概念、Broker 发送心跳包、NameServer 维护路由、Zookeeper vs NameServer 四个模块揭秘名字服务的设计精髓。
1 基础概念
NameServer 是一个非常简单的 Topic 路由注册中心,其角色类似 Dubbo 中的 zookeeper ,支持 Broker 的动态注册与发现。
RocketMQ 集群工作流程:
1、NameServer 启动服务,监听 TCP 端口 , 集群多节点之间无任何信息交互,然后等待 Broker、Producer 、Consumer 连上来;
2、Broker 启动后,每隔 30 秒向所有的 NameServer 发送心跳命令 ;
3、NameServer 接收到请求之后,保存路由信息在本地内存里 ,将响应结果返给 Broker 服务;
4、Producer 启动之后,会随机的选择一个 NameServer ,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息;
5、Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息。
2 Broker 发送心跳包
我们贴一段 Broker 发送心跳命令的源码:
1、Broker 会每隔 30 秒向所有的 NameServer 发送心跳命令 ;
使用 CountDownLatch 实现多线程同步,可以获取发往所有的 NameServer 的心跳命令的响应结果
2、心跳命令包含两个部分:请求头和请求体
3 NameServer 维护路由
NameServer 在接收到 Broker 发送的心跳请求之后,通过默认的处理器来处理请求,保存路由信息成功后,注册成功状态返回给 Broker 服务。
源码中,我们可以看到路由信息保存在 HashMap 中 。
1、topicQueueTable:Topic 消息队列路由信息,包括 topic 所在的 broker 名称,读队列数量,写队列数量,同步标记等信息,rocketmq 根据 topicQueueTable 的信息进行负载均衡消息发送。
2、brokerAddrTable:Broker 节点信息,包括 brokername,所在集群名称,还有主备节点信息。
3、clusterAddrTable:Broker 集群信息,存储了集群中所有的 Brokername。
4、brokerLiveTable:Broker 状态信息,NameServer 每次收到 Broker 的心跳包就会更新该信息。
当 Broker 向 NameServer 发送心跳包(路由信息),NameServer 需要对 HashMap 进行数据更新,但我们都知道 HashMap 并不是线程安全的,高并发场景下,容易出现 CPU 100% 问题,所以更新 HashMap 时需要加锁,RocketMQ 使用了 JDK 的读写锁 ReentrantReadWriteLock 。
下面我们看下路由信息如何更新和读取:
1、写操作:更新路由信息,操作写锁
2、读操作:查询主题信息,操作读锁
我们可以将 NameServer 实现注册中心的方式总结为: RPC 服务 + HashMap 存储容器 + 读写锁 + 定时任务 。
1、NameServer 监听固定的端口,提供 RPC 服务
2、HashMap 作为存储容器
3、读写锁控制锁的颗粒度
4、定时任务
- 每个 Broker 每隔 30 秒注册主题的路由信息到所有 NameServer
- NameServer 定时任务每隔 10 秒清除已宕机的 Broker , 判断宕机的标准是:当前时间减去 Broker 最后一次心跳时间大于 2 分钟
4 Zookeeper vs NameServer
那为什么 RocketMQ 不用 Zookeeper 做为注册中心呢 ?
我们先温习下 CAP 理论。
CAP 理论是分布式架构中重要理论。
1、一致性 (Consistency) :所有节点在同一时间具有相同的数据;
2、可用性 (Availability) :保证每个请求不管成功或者失败都有响应 (某个系统的某个节点挂了,但是并不影响系统的接受或者发出请求) ;
3、分隔容忍 (Partition tolerance) :系统中任意信息的丢失或失败不会影响系统的继续运作。 (在整个系统中某个部分,挂掉了,或者宕机了,并不影响整个系统的运作或者说使用) 。
Zookeeper 是一个典型的 CP 注册中心 ,通过使 ZAB 协议来保证节点之间数据的强一致性。
笔者曾经遇到过一起神州专车服务宕机事故,zookeeper 集群不堪重负,一直在选主 。 架构负责人修改了 zookeeper 的 jvm 参数,重启集群后,才临时解决了问题。
因为 MetaQ 集群和服务治理共用一组 zookeeper 集群 。
- MetaQ 消费者负载均衡时,会频繁的争抢锁 ,同时也会频繁的提交 offset ;
- 专车的注册服务也越来越多,注册信息通过 Hession 序列化存储在 zookeeper 的节点。
为了减少 zookeeper 集群的性能压力,架构团队将 MetaQ 使用的 zookeeper 集群独立出来。
这次事故让我认识到: Zookeeper 作为 CP 注册中心,大规模使用场景下,它就变得很脆弱,我们要非常小心的使用。
淘宝中间件博客出了一篇文章 : 阿里巴巴为什么不用 ZooKeeper 做服务发现 ?
文章有两个观点,笔者认为非常有借鉴意义。
1、当数据中心服务规模超过一定数量 (服务规模 = F {服务 pub 数,服务 sub 数} ),作为注册中心的 ZooKeeper 很快就会像下图的驴子一样不堪重负。
2、可以使用 ZooKeeper,但是大数据请向左,而交易则向右,分布式协调向左,服务发现向右。
相比 ZooKeeper ,NameServer 是一个典型的 AP 注册中心,它有如下优点:
1、代码不到 1000 行,实现简单,易于维护;
2、性能极好,除了网络消耗,基本都是本地内存操作;
3、服务都是无状态,且节点之间并不交互,运维简单;
RocketMQ 的设计者之所以选择自研名字服务,遵循着架构设计的准则,笔者总结为:简单、高效、适当妥协。
没有完美的技术,只有合适的技术,与诸君共勉。