1 NameServer介绍
Name Server 是专为 RocketMQ 设计的轻量级名称服务,具有简单、可集群横吐扩展、无状态,节点之间互不通信等特点。整个Rocketmq集群的工作原理如下图所示:
可以看到,Broker集群、Producer集群、Consumer集群都需要与NameServer集群进行通信:
Broker集群:
Broker用于接收生产者发送消息,或者消费者消费消息的请求。一个Broker集群由多组Master/Slave组成,Master可写可读,Slave只可以读,Master将写入的数据同步给Slave。每个Broker节点,在启动时,都会遍历NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报。
Producer集群:
消息的生产者,通过NameServer集群获得Topic的路由信息,包括Topic下面有哪些Queue,这些Queue分布在哪些Broker上等。Producer只会将消息发送到Master节点上,因此只需要与Master节点建立连接。
Consumer集群:
消息的消费者,通过NameServer集群获得Topic的路由信息,连接到对应的Broker上消费消息。注意,由于Master和Slave都可以读取消息,因此Consumer会与Master和Slave都建立连接。
2 为什么要使用NameServer?
目前可以作为服务发现组件有很多,如etcd、consul,zookeeper等:
那么为什么rocketmq选择自己开发一个NameServer,而不是使用这些开源组件呢?
特别的,RocketMQ设计之初时参考的另一款消息中间件Kafka就使用了Zookeeper,Zookeeper其提供了Master选举、分布式锁、数据的发布和订阅等诸多功能。
事实上,在RocketMQ的早期版本,即MetaQ 1.x和MetaQ 2.x阶段,也是依赖Zookeeper的。但MetaQ 3.x(即RocketMQ)却去掉了ZooKeeper依赖,转而采用自己的NameServer。
而RocketMQ的架构设计决定了只需要一个轻量级的元数据服务器就足够了,只需要保持最终一致,而不需要Zookeeper这样的强一致性解决方案,不需要再依赖另一个中间件,从而减少整体维护成本。敏锐的同学肯定已经意识到了,根据CAP理论,RocketMQ在名称服务这个模块的设计上选择了AP,而不是CP:
-
一致性(Consistency):Name Server 集群中的多个实例,彼此之间是不通信的,这意味着某一时刻,不同实例上维护的元数据可能是不同的,客户端获取到的数据也可能是不一致的。
-
可用性(Availability):只要不是所有NameServer节点都挂掉,且某个节点可以在指定之间内响应客户端即可。
-
分区容错(Partiton Tolerance):对于分布式架构,网络条件不可控,出现网络分区是不可避免的,只要保证部分NameServer节点网络可达,就可以获取到数据。具体看公司如何实施,例如:为了实现跨机房的容灾,可以将NameServer部署的不同的机房,某个机房出现网络故障,其他机房依然可用,当然Broker集群/Producer集群/Consumer集群也要跨机房部署。
事实上,除了RocketMQ开发了自己的NameServer,最近 Kafka 社区也在 Wiki 空间上提交了一项新的改进提案“KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum”,其目的是为了消除 Kafka 对 ZooKeeper 的依赖,该提案建议用自管理的元数据仲裁机制替换原来的 ZooKeeper 组件。感兴趣的读者可以自行查阅相关资料。
3 NameServer如何保证数据的最终一致?
NameServer作为一个名称服务,需要提供服务注册、服务剔除、服务发现这些基本功能,但是NameServer节点之间并不通信,在某个时刻各个节点数据可能不一致的情况下,如何保证客户端可以最终拿到正确的数据。下面分别从路由注册、路由剔除,路由发现三个角度进行介绍。
3.1 路由注册
对于Zookeeper、Etcd这样强一致性组件,数据只要写到主节点,内部会通过状态机将数据复制到其他节点,Zookeeper使用的是Zab协议,etcd使用的是raft协议。
但是NameServer节点之间是互不通信的,无法进行数据复制。RocketMQ采取的策略是,在Broker节点在启动的时候,轮训NameServer列表,与每个NameServer节点建立长连接,发起注册请求。NameServer内部会维护一个Broker表,用来动态存储Broker的信息。
同时,Broker节点为了证明自己是存活的,会将最新的信息上报给NameServer,然后每隔30秒向NameServer发送心跳包,心跳包中包含 BrokerId、Broker地址、Broker名称、Broker所属集群名称等等,然后NameServer接收到心跳包后,会更新时间戳,记录这个Broker的最新存活时间。
NameServer在处理心跳包的时候,存在多个Broker同时操作一张Broker表,为了防止并发修改Broker表导致不安全,路由注册操作引入了ReadWriteLock读写锁,这个设计亮点允许多个消息生产者并发读,保证了消息发送时的高并发,但是同一时刻NameServer只能处理一个Broker心跳包,多个心跳包串行处理。这也是读写锁的经典使用场景,即读多写少。