三、Kafka架构原理
3.1 整体架构图
一个典型的kafka集群中包含若干个Producer,若干个Broker,若干个Consumer,以及一个zookeeper集群; kafka通过zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行Rebalance(负载均衡);Producer使用push模式将消息发布到Broker;Consumer使用pull模式从Broker中订阅并消费消息。
3.1.1 controller
Kafka的核心组件。它的主要作用是在Apache ZooKeeper的帮助下管理和协调整个Kafka集群。controller控制器是重度依赖ZooKeeper
Broker在启动时,会尝试去ZooKeeper中创建/controller节点。Kafka当前选举控制器的规则是:第一个成功创建/controller节点的Broker会被指定为控制器。
-
主题管理(创建、删除、增加分区)
-
分区重分配
-
Preferred领导者选举
Preferred领导者选举主要是Kafka为了避免部分Broker负载过重而提供的一种换Leader的方案
-
集群成员管理(新增Broker、Broker主动关闭、Broker宕机)
-
数据服务
向其他Broker提供数据服务。控制器上保存了最全的集群元数据信息,其他所有Broker会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。
3.1.2 broker
kafka为每个主题维护了分布式的分区(partition)日志文件,每个partition在kafka存储层面是append log。任何发布到此partition的消息都会被追加到log文件的尾部,在分区中的每条消息都会按照时间顺序分配到一个单调递增的顺序编号,也就是我们的offset,offset是一个long型的数字,我们通过这个offset可以确定一条在该partition下的唯一消息。在partition下面是保证了有序性,但是在topic下面没有保证有序性。
3.1.3 zk
zk与Producer, Broker, Consumer的关系
Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。
- Producer端直接连接broker.list列表,从列表中返回TopicMetadataResponse,该Metadata包含Topic下每个partition leader建立socket连接并发送消息.
- Broker端使用zookeeper用来注册broker信息,以及监控partition leader存活性.
- Consumer端使用zookeeper用来注册consumer信息,其中包括consumer消费的partition列表等,同时也用来发现broker列表,并和partition leader建立socket连接,并获取消息。
Producer向zookeeper中注册watcher,了解topic的partition的消息,以动态了解运行情况,实现负载均衡。Zookeepr不管理producer,只是能够提供当前broker的相关信息。
每个Broker服务器在启动时,都会到Zookeeper上进行注册,即创建/brokers/ids/[0-N]的节点,然后写入IP,端口等信息,Broker创建的是临时节点,所有一旦Broker上线或者下线,对应Broker节点也就被删除了,因此我们可以通过zookeeper上Broker节点的变化来动态表征Broker服务器的可用性,Kafka的Topic也类似于这种方式。
Controller 的HA中zk起到的作用
在 broker 启动的时候,都会去尝试创建 /controller 这么一个临时节点,由于 zk 的本身机制,哪怕是并发的情况下,也只会有一个 broker 能够创建成功。因此创建成功的那个就会成为 controller,而其余的 broker 都会对这 controller 节点加上一个 Watcher,
一旦原有的 controller 出问题,临时节点被删除,那么其他 broker 都会感知到从而进行新一轮的竞争,谁先创建那么谁就是新的 controller。
当然,在这个重新选举的过程中,有一些需要 controller 参与的动作就无法进行,例如元数据更新等等。需要等到 controller 竞争成功并且就绪后才能继续进行,所以 controller 出问题对 kafka 集群的影响是不小的。
消费者注册
消费者服务器在初始化启动时加入消费者分组的步骤如下
注册到消费者分组。每个消费者服务器启动时,都会到Zookeeper的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该临时节点。
对 消费者分组 中的 消费者 的变化注册监听。每个 消费者 都需要关注所属 消费者分组 中其他消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。
对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。
进行消费者负载均衡。为了让同一个Topic下不同分区的消息尽量均衡地被多个 消费者 消费而进行 消费者 与 消息 分区分配的过程,通常,对于一个消费者分组,如果组内的消费者服务器发生变更或Broker服务器发生变更,会发出消费者负载均衡。
3.1.4 日志
日志可能是一种最简单的不能再简单的存储抽象,只能追加、按照时间完全有序(totally-ordered)的记录序列。日志看起来的样子:
在日志的末尾添加记录,读取日志记录则从左到右。每一条记录都指定了一个唯一的顺序的日志记录编号。
日志记录的次序(ordering)定义了『时间』概念,因为位于左边的日志记录表示比右边的要早。日志记录编号可以看作是这条日志记录的『时间戳』。把次序直接看成是时间概念,刚开始你会觉得有点怪异,但是这样的做法有个便利的性质:解耦了时间和任一特定的物理时钟(physical clock )。引入分布式系统后,这会成为一个必不可少的性质。
日志和文件或数据表(table)并没有什么大的不同。文件是一系列字节,表是由一系列记录组成,而日志实际上只是一种按照时间顺序存储记录的数据表或文件。
3.1.4 Topic
对于每一个topic, Kafka集群都会维持一个分区日志,如下所示:
在Kafka中的每一条消息都有一个topic。一般来说在我们应用中产生不同类型的数据,都可以设置不同的主题。一个主题一般会有多个消息的订阅者,当生产者发布消息到某个主题时,订阅了这个主题的消费者都可以接收到生产者写入的新消息。
- 一个 Topic 是由多个队列组成的,被称为【Partition(分区)】。
- 生产者发送消息的时候,这条消息会被路由到此 Topic 中的某一个 Partition。
- 消费者监听的是所有分区。
- 生产者发送消息时,默认是面向 Topic 的,由 Topic 决定放在哪个 Partition,默认使用轮询策略。
- 也可以配置 Topic,让同类型的消息都在同一个 Partition。例如,处理用户消息,可以让某一个用户所有消息都在一个 Partition。用户1发送了3条消息:A、B、C,默认情况下,这3条消息是在不同的 Partition 中(如 P1、P2、P3)。在配置之后,可以确保用户1的所有消息都发到同一个分区中(如 P1)。这是为了提供消息的【有序性】。
- 消息在不同的 Partition 是不能保证有序的,只有一个 Partition 内的消息是有序的。
3.1.5 Partition
一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号
1.每个partion(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。
2.每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。
3.1.6 producer
生产者的发送消息方式有三种
- 同步发送。不管结果如何直接发送
- 同步发送。也可以称之后异步阻塞,因为使用的是future.get,发送并返回结果
- 异步发送并回调
消费传递保障
-
最多一次
-
至少一次
-
正好一次
Kafka 分别通过 幂等性(Idempotence)和事务(Transaction)这两种机制实现了 精确一次(exactly once)语义。更多详见3.5
produce分区策略
- DefaultPartitioner 默认分区策略
- 如果消息中指定了分区,则使用它
- 如果未指定分区但存在key,则根据序列化key使用murmur2哈希算法对分区数取模。
- 如果不存在分区或key,则会使用粘性分区策略
- UniformStickyPartitioner 纯粹的粘性分区策略
- UniformStickyPartitioner 是不管你有没有key, 统一都用粘性分区来分配。
- RoundRobinPartitioner 分区策略
- 如果消息中指定了分区,则使用它
- 将消息平均的分配到每个分区中。
- 与key无关
粘性分区
Kafka2.4版本之后,支持粘性分区。Producer在发送消息的时候,会将消息放到一个ProducerBatch中, 这个Batch可能包含多条消息,然后再将Batch打包发送。消息的发送必须要等到Batch满了或者linger.ms
时间到了,才会发送。
linger.ms
,默认是0ms,当达到这个时间后,kafka producer就会立刻向broker发送数据。batch.size
,默认是16kb,当产生的消息数达到这个大小后,就会立即向broker发送数据。
3.1.7 Consumer
Kafka采取拉取模型(poll),由自己控制消费速度,以及消费的进度,消费者可以按照任意的偏移量进行消费。比如消费者可以消费已经消费过的消息进行重新处理,或者消费最近的消息等等。
每个Consumer只能对应一个ConsumerGroup.
消费者组中的消费者实例个数不能超过分区的数量。否则会出现部分消费者闲置的情况发生。
consumer分区策略
- RoundRobinAssignor. 针对所有topic分区。它是采用轮询分区策略,是把所有的partition和所有的consumer列举出来,然后按照hashcode进行排序,最后再通过轮询算法来分配partition给每个消费者。
- RangeAssignor. RangeAssignor作用域为每个Topic。对于每一个Topic,将该Topic的所有可用partitions和订阅该Topic的所有consumers展开(字典排序),然后将partitions数量除以consumers数量,算数除的结果分别分配给订阅该Topic的consumers,算数除的余数分配给前一个或者前几个consumers。
- StickyAssignor. StickyAssignor有两个目标:首先,尽可能保证分区分配均衡(即分配给consumers的分区数最大相差为1);其次,当发生分区重分配时,尽可能多的保留现有的分配结果。当然,第一个目标的优先级高于第二个目标。
- CooperativeStickyAssignor. 上述三种分区分配策略均是基于eager协议,Kafka2.4.0开始引入CooperativeStickyAssignor策略。CooperativeStickyAssignor与之前的StickyAssignor虽然都是维持原来的分区分配方案,最大的区别是:StickyAssignor仍然是基于eager协议,分区重分配时候,都需要consumers先放弃当前持有的分区,重新加入consumer group;而CooperativeStickyAssignor基于cooperative协议,该协议将原来的一次全局分区重平衡,改成多次小规模分区重平衡。
kafka2.4版本支持:一个是kafka支持从follower副本读取数据,当然这个功能并不是为了提供读取性能。kafka存在多个数据中心,不同数据中心存在于不同的机房,当其中一个数据中心需要向另一个数据中心同步数据的时候,由于只能从leader replica消费数据,那么它不得不进行跨机房获取数据,而这些流量带宽通常是比较昂贵的(尤其是云服务器)。即无法利用本地性来减少昂贵的跨机房流量。
所以kafka推出这一个功能,就是帮助类似这种场景,节约流量资源。
再平衡触发三种情况
- consumer group中的新增或删除某个consumer,导致其所消费的分区需要分配到组内其他的consumer上;
- consumer订阅的topic发生变化,比如订阅的topic采用的是