Kafka/RocketMQ架构对比

一、Broker设计对比

1. Kafka
Kafka架构图.png
  • Topic A共有2个分区,每个分区有两个副本(一个Leader一个Follower),分别分布在 Broker0 和 Broker1 上;
  • Broker0/Broker1每台机器既是Leader,也是Follower。具体来说,比如机器Broker0对于Partition0来说是Master,对于Partition1来说又是Follower;
2. RocketMQ
RocketMQ架构图.png
  • Broker集群中有Broker0和Broker1为Leader,每个Broker有两个Slave,共6台机器;
  • Master0/Slave0_0/Slave0_1/Master1/Slave1_0/Slave1_1每台机器通过配置固定只能充当Master或Slave;
  • TopicA在Broker0上有两个队列,在Broker1上有两个队列;
3. 对比

1.Kafka的Leader/Follower是个逻辑概念,1台机器可以同时具有Leader角色和Follower角色;RocketMQ的Master/Slave是个物理概念,1台机器,只能是Master或者Slave。在集群初始配置的时候,指定死的;
2.Kafka的Broker是个物理概念,1个broker就对应1台机器。 RocketMQ的Broker是个逻辑概念,1个broker = 1个master + 多个slave,brokerName相同,Master的brokerId=0,Slave的brokerId>0;
3.消息层次结构
Kafka的三层消息:
第一层:主题层,每个主题可以配置M个分区,而每个分区又可以配置N个副本;
第二层:分区层,每个分区的N个副本中只能有一个充当领导者角色,对外提供服务;其他N-1个副本是追随者副本,只是提供数据冗余之用;
第三层:消息层,分区中包含若干条消息,每条消息的位移从0开始,依次递增;
RocketMQ的三层消息:
第一层:主题层,每个主题可以创建在N个Broker上,可以指定Broker集群创建或者指定具体某个Broker创建;
第二层:队列层,主题在每个Broker上可以创建M个队列,生产者通过路由算法把消息发送到队列所在的Broker进行处理;
第三层:消息层,所有Topic的消息统一存储在Broker的CommitLog中,每条消息的位移依次递增,CommitLog异步生成ConsumerQueue文件存储消息在CommitLog中的位移;

二、ZK和NameSrv对比

1. Kafka - ZK

Kafka 主要使用 ZooKeeper 来保存它的元数据、监控 Broker 和分区的存活状态,并利用 ZooKeeper 来进行选举KafkaController,进而决定出每个分区的Leader副本。

Kafka 在 ZooKeeper 中保存的元数据,主要就是 Broker 的列表和主题分区信息两棵树。这份元数据同时也被缓存到每一个 Broker 中。客户端并不直接和 ZooKeeper 来通信,而是在需要的时候,通过 RPC 请求去 Broker 上拉取它关心的主题的元数据,然后保存到客户端的元数据缓存中,以便支撑客户端生产和消费。


ZK节点信息.png
2. RocketMQ - NameSrv

在 RocketMQ 中,NameServer 最主要的功能就是,为客户端提供寻址服务,协助客户端找到主题对应的 Broker 地址。此外,NameServer 还负责监控每个 Broker 的存活状态。

为了规避可用性和一致性的问题,NameServer 各节点之间是不需要任何通信的,也不会通过任何方式互相感知,每个节点都可以独立提供全部服务。每个 Broker 都需要和所有的 NameServer 节点进行通信。当 Broker 保存的 Topic 信息发生变化的时候,它会主动通知所有的 NameServer 更新路由信息,因为每个 NameServer 节点都可以独立提供完整的服务,所以,对于客户端来说,包括生产者和消费者,只需要选择任意一个 NameServer 节点来查询路由信息就可以了。客户端在生产或消费某个主题的消息之前,会先从 NameServer 上查询这个主题的路由信息,然后根据路由信息获取到当前主题和队列对应的 Broker 物理地址,再连接到 Broker 节点上进行生产或消费。

由于 NameServer 的功能非常简单,NameServer 就使用5个 Map 来保存了所有的路由信息

public class RouteInfoManager {
    // topic队列信息, topic -> topic对应的queue list,消息发送时根据路由表进行负载均衡
    private final HashMap> topicQueueTable;
    // broker信息,brokerName -> broker详细信息
    private final HashMap brokerAddrTable;
    // broker集群-cluster信息, clusterName -> broker集群中的brokerName list
    private final HashMap> clusterAddrTable;
    // 存活的broker信息,brokerAddr -> broker存活信息,Namesrv每次收到心跳包时会替换该信息;
    // BrokerLiveInfo中lastUpdateTimestamp存储上一次收到Broker心跳包时间
    private final HashMap brokerLiveTable;
    // broker filter, brokerAddr -> broker上的Filter信息,用于类模式的消息过滤
    private final HashMap/* Filter Server */> filterServerTable;

    public RouteInfoManager() {
        this.topicQueueTable = new HashMap>(1024);
        this.brokerAddrTable = new HashMap(128);
        this.clusterAddrTable = new HashMap>(32);
        this.brokerLiveTable = new HashMap(256);
        this.filterServerTable = new HashMap>(256);
    }
    ......
}

topicQueueTable 保存的是主题和队列信息,其中每个队列信息对应的类 QueueData 中,还保存了 brokerName。需要注意的是,这个 brokerName 并不真正是某个 Broker 的物理地址,它对应的一组 Broker 节点,包括一个主节点和若干个从节点。

brokerAddrTable 中保存了集群中每个 brokerName 对应 Broker 信息,每个 Broker 信息用一个 BrokerData 对象表示:

public class BrokerData implements Comparable {
    private String cluster;
    private String brokerName;
    private HashMap brokerAddrs;
}

BrokerData 中保存了集群名称 cluster,brokerName 和一个保存 Broker 物理地址的 Map:brokerAddrs,它的 Key 是 BrokerID,Value 就是这个 BrokerID 对应的 Broker 的物理地址。

3. 对比

先思考下:我们都知道RMQ是借鉴Kafka设计出来的,那RMQ为什么不像Kafka一样直接使用开源组件zk,而要自己写一个NameServer呢?

3.1 zk的问题:

1.zk并不适合频繁的写操作,在Kafka 0.8版本之前的消费者位移就是保存在zk上的,之后的版本Kafka修改了方案,改为使用内部主题_consumer_offsets的方式来保存,从而规避了zk的这个弊病;
2.kafka集群的可用性严重依赖 zk ,而安装、运维和调优一套zk集群的代价是很高的。只有摆脱zk才能让Kafka变成一个独立的框架;

3.2 NameServer的改进:

1.Kafka使用zk主要有两方面的功能,一是保存集群元数据,二是选举Leader副本。而RMQ的Master/Slave的角色是配置死的,当一个Master挂了之后,你可以写到其他Master上,但不会说一个Slave切换成Master,这样就从设计上规避了选举的问题。那么剩下的就是元数据的保存,这用个简单的NameServer就搞定了;
2.NameServer很轻量,无状态,每个节点都保存全量完整的路由信息,当某个NameServer下线了,Broker仍然可以向其它NameServer同步其路由信息,可用性也能得到很好保证;

3.3 NameServer的缺点:

1.每个 Broker 需要与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer,通信成本较高;
2.不同于zk的CP设计,NameServer是AP的设计,集群中各个节点间不进行通信,强一致性无法保证,需要通过一系列设计来保证最终一致性;

保证最终一致性的设计:

  1. 路由注册:Broker每隔30秒向NameServer发送心跳包,心跳包中包含 BrokerId、Broker地址、Broker名称、Broker所属集群名称等,NameServer收到心跳包更新该Broker最新时间戳;
  2. 路由剔除:NameServer中有一个定时任务,每隔10秒扫描一下Broker表,如果某个Broker的心跳包最新时间戳距离当前时间超多120秒,也会判定Broker失效并将其移除;
  3. 路由发现:Producer和Consumer 每隔30秒定时从NameServer获取最新的路由表;
  4. 生产者重试机制:如果客户端刚拉取最新的路由表后某个Broker宕机,通过Producer重试机制解决最长30秒的延迟问题;消息发送默认采用round-robin机制来选择发送到哪一个队列,如果发送失败,默认重试2次。由于之前发送失败的Queue必然位于某个Broker上,在重试过程中,这个失败的Broker上的Queue都不会选择;
3.4 其他

1.事实上,在RocketMQ的早期版本,即MetaQ 1.x和MetaQ 2.x阶段,也是依赖Zookeeper的。但MetaQ 3.x(即RocketMQ)却去掉了ZooKeeper依赖,转而采用自己的NameServer;
2.Kafka 社区最近几年最大的提案“KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum”,其目的也是为了消除 Kafka 对 ZooKeeper 的依赖,社区也打算自己写一套 Raft 的算法来实现 Controller 的选举;

---------------over--------------

你可能感兴趣的:(Kafka/RocketMQ架构对比)