kafka整体系统架构 kafka架构图_kafka


鄙人愚钝,在经历了三天两夜,我终于是搞懂了这Kafka的内部结构,之前看很多文章都是不清晰,脑海中始终没有一个清晰的构图…

1. Kafka简介

Apache Kafka最早是由LinkedIn开源出来的分布式消息系统,现在是Apache旗下的一个子项目,并且已经成为开源领域应用最广泛的消息系统之一。Kafka社区非常活跃,从0.9版本开始,Kafka的标语已经从“一个高吞吐量,分布式的消息系统”改为"一个分布式流平台"。

Kafka和传统的消息系统不同在于:

  • kafka是一个分布式系统,易于向外扩展。
  • 它同时为发布和订阅提供高吞吐量
  • 它支持多订阅者,当失败时能自动平衡消费者
  • 消息的持久化

2. Kafka核心组成

kafka整体系统架构 kafka架构图_kafka_02


如上图:可以看到Producer、Broker、Topic、Partition、Consumer Group、Consumer等组件。

在一套kafka架构中有多个Producer,多个Broker,多个Consumer,每个Producer可以对应多个Topic,每个Consumer只能对应一个Consumer Group。

组件

介绍

Producer

消息生产者:向Broker发送消息的客户端

Producer

消息消费者:从Broker读取消息的客户端

Broker

消息中间间处理节点:一个Kafka节点就是一个Broker,一个或多个Broker可以组成一个Kafka集群(一般集群建议奇数台,最少三台)

Topic

主题:发布到Kafka集群的每条消息都需要指定一个Topic,Kafka的消息都是保存在Topic中

Partition

分区:物理上的存储概念,一个Topic可以分为多个Partition,每个Partition内部都是有序的,但是多个分区组合起来的数据并不是有序的

Consumer Group

消费者组:每个Consumer属于一个特定的Consumer Group(CG),一条消息可以发送到多个不同的CG,但同一个分区的数据只有该CG中的一个Consumer能消费该数据

下面对一些组件更细的讲解一下:

2.0 Zookeeper

zookeepers(负责选举,均衡,meta记录,消费记录)

zookeeper在集群中与broker和consumer进行交互,维护数据和集群高可用。

  • 记录consumer消费message的位置信息;
  • partitions故障时进行Leader Election
  • kafka的meta信息在zookeeper如何存储

注:Producer不在zk中注册,consumer在zk注册。

kafka在zookeeper的结构图如下:

kafka整体系统架构 kafka架构图_主副本_03

2.1 分区(水平扩展、高并发)

kafka为每个主题维护了分布式的分区(partition)日志文件,每个partition在kafka存储层面是append log。任何发布到此partition的消息都会被追加到log文件的尾部,在分区中的每条消息都会按照时间顺序分配到一个单调递增的顺序编号,也就是我们的offset,offset是一个long型的数字,我们通过这个offset可以确定一条在该partition下的唯一消息。在partition下面是保证了有序性,但是在topic下面没有保证有序性。

kafka整体系统架构 kafka架构图_kafka_04

应了上表说的:每个Partition内部都是有序的,但是多个分区组合起来的数据并不是有序的

消息生产者发送消息的时候:

  • 指定了patition,则直接使用该patition
  • 未指定patition,但是指定key,通过key值进行Hash,然后对分区数量取余,保证了同一个Key值的会被路由到同一个分区,如果想队列的强顺序一致性,可以让所有的消息都设置为同一个Key。
  • patition、key都未指定,使用轮询选出一个patition

kafka整体系统架构 kafka架构图_主副本_05


分区的原因:

1. 方便在集群中水平扩展,每个patition可以通过调整以适应它所在的机器,而一个Topic又可以有多个patition组成,因此整个集群就可以适应任意大小的数据了。

2. 可以提高高并发,因为可以以patition为单位读写了。(多个请求过来可能分发在不同的patition)

2.2 副本机制(高可用)

上图,看到:Kafka集群管理消息,这一层中有三个Broker,其中Broker1中有一个TopicA,但这个TopicA 有两个分区:Partition0、Partition1

应了上表中说的,一个Topic可以有多个Partition

可以看到,在Broker2中也有一个TopicA,这个TopicA 也有两个分区:Partition0、Partition1。

在Broker1中:Partition0是Leader、Partition1是Follower
但Broker2中:Partition0是Follower、Partition1是Leader

我们称Broker2中的Partition0为Broker1中Partition0的副本,其实这两个都可以叫做副本,Broker1中的Partition0为主副本。

疑问:为什么要把Partition0的副本放在Broker2?

解答:我问你,如果把Leader、Follower都放在同一台机器,那当这台机器挂了,岂不是这个分区的数据全没了?那还要副本干什么?

Kafka的副本机制是多个服务端节点对其他节点的主题分区的日志进行复制。当集群中的某个节点出现故障,访问故障节点的请求会被转移到其他正常节点(这一过程通常叫Reblance),kafka每个主题的每个分区都有一个主副本以及0个或者多个副本,副本保持和主副本的数据同步,当主副本出故障时就会被替代。

同一个patition可能会有多个replication(对应server.properties配置文件中的default.replication.factor=N)。没有replication的情况下,一旦Broker宕机,其上所有的patition的数据都不可被消费,同时producer也不能再将数据存于其上的patition。引入replication之后,同一个patition可能会有多个replication,而这时需要在这些replication之间选出一个Leader,producer和consumer只与这个Leader交互,其他replication作为Follower从Leader中复制数据。

在Kafka中并不是所有的副本都能被拿来替代主副本,所以在kafka的Leader节点中维护着一个ISR(In sync Replicas)集合,翻译过来也叫正在同步中集合,在这个集合中的需要满足两个条件:

  • 节点必须和ZK保持连接
  • 在同步的过程中这个副本不能落后主副本太多

ISR中保存着所有的副本存在的位置,如:[0,2,1],类似这种结构存储的,其中0、2、1代表Broker的ID(此ID是唯一的)


[0,2,1]:表示当前Leader在ID为0的机器上,2、1表示另外两个副本所在 的机器


注意:其实2排在1前面并非是没有道理的,这说明ID为2的机器副本上的更加接近主副本,当Leader挂了,就会选举2为新的Leader。

问:Kafka设置副本数量大于Broker时报错。

解:之前说过,Kafka副本是保存在不同的Broker中,如果副本数量大于Broker数量,此时至少有一个Broker中有两个一样的副本,那么意义何在?

2.3 高性能的日志存储

kafka一个topic下面的所有消息都是以partition的方式分布式的存储在多个节点上。同时在kafka的机器上,每个Partition其实都会对应一个日志目录,在目录下面会对应多个日志分段(LogSegment)。LogSegment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值,数值大小为64位,20位数字字符长度,没有数字用0填充,如下,假设有1000条消息,每个LogSegment大小为100,下面展现了900-1000的索引和Log:

kafka整体系统架构 kafka架构图_kafka整体系统架构_06

由于kafka消息数据太大,如果全部建立索引,即占了空间又增加了耗时,所以kafka选择了稀疏索引的方式,这样的话索引可以直接进入内存,加快偏查询速度。

简单介绍一下如何读取数据,如果我们要读取第911条数据首先第一步,找到他是属于哪一段的,根据二分法查找到他属于的文件,找到0000900.index和00000900.log之后,然后去index中去查找 (911-900) =11这个索引或者小于11最近的索引,在这里通过二分法我们找到了索引是[10,1367]然后我们通过这条索引的物理位置1367,开始往后找,直到找到911条数据。

上面讲的是如果要找某个offset的流程,但是我们大多数时候并不需要查找某个offset,只需要按照顺序读即可,而在顺序读中,操作系统会对内存和磁盘之间添加page cahe,也就是我们平常见到的预读操作,所以我们的顺序读操作时速度很快。但是kafka有个问题,如果分区过多,那么日志分段也会很多,写的时候由于是批量写,其实就会变成随机写了,随机I/O这个时候对性能影响很大。所以一般来说Kafka不能有太多的partition。针对这一点,RocketMQ把所有的日志都写在一个文件里面,就能变成顺序写,通过一定优化,读也能接近于顺序读。

可以思考一下:1.为什么需要分区,也就是说主题只有一个分区,难道不行吗?2.日志为什么需要分段

1.分区是为了水平扩展 2.日志如果在同一个文件太大会影响性能。如果日志无限增长,查询速度会减慢

3. 消费模型

消息由生产者发送到kafka集群后,会被消费者消费。一般来说我们的消费模型有两种:推送模型(push)和拉取模型(pull)

基于推送模型的消息系统,由消息代理记录消费状态。消息代理将消息推送到消费者后,标记这条消息为已经被消费,但是这种方式无法很好地保证消费的处理语义。比如当我们把已经把消息发送给消费者之后,由于消费进程挂掉或者由于网络原因没有收到这条消息,如果我们在消费代理将其标记为已消费,这个消息就永久丢失了。如果我们利用生产者收到消息后回复这种方法,消息代理需要记录消费状态,这种不可取。如果采用push,消息消费的速率就完全由消费代理控制,一旦消费者发生阻塞,就会出现问题。

Kafka采取拉取模型(poll),由自己控制消费速度,以及消费的进度,消费者可以按照任意的偏移量进行消费。比如消费者可以消费已经消费过的消息进行重新处理,或者消费最近的消息等等。

kafka整体系统架构 kafka架构图_kafka整体系统架构_07

当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:(上图针对-1的流程)

  • 0 :意味着producer不等待broker同步完成的确认,继续发送下一条(批)信息
    提供了最低的延迟。但是最弱的持久性,当服务器发生故障时,就很可能发生数据丢失。例如leader已经死亡,producer不知情,还会继续发送消息broker接收不到数据就会数据丢失
  • 1:意味着producer要等待leader成功收到数据并得到确认,才发送下一条message。此选项提供了较好的持久性较低的延迟性。
    Partition的Leader死亡,follwer尚未复制,数据就会丢失
  • -1:意味着producer得到follwer确认,才发送下一条数据
    持久性最好,延时性最差。

三种机制性能递减,可靠性递增。

4. 存储策略

无论消息是否被消费,Kafka都会保留消息,也就是说消息可以被重复消费。

有两种策略可以删除旧数据:

  1. 基于时间:log.retention.hours=168(配置文件中:默认保存一个星期的数据)
  2. 基于大小:log.retention.bytes=1073741824(1G)

需要注意的是,因为Kafka读取特定的小时时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高Kafka性能无关。