其他更多java基础文章:
java基础学习(目录)
这系列是根据极客时间《Kafka核心技术与实战》这个课程做的笔记
本篇目录
- Kafka副本机制
- 请求是怎么被处理的(Kafka网络模型)
- Kafka Controller
- 高水位和Leader Epoch
Kafka副本机制
所谓副本机制(Replication),也可以称之为备份机制,通常是指分布式在多台网络互连的机器上保存有相同的数据拷贝。
副本机制的价值:
- 提供数据冗余
- 提供高伸缩性
- 改善数据局部性
但 Kafka的副本机制,只实现了提供数据冗余的价值。
副本定义:
Kafka有主题的概念,每个主题又分为若干个分区。副本的概念是在分区层级下定义的,每个分区配置有若干个副本。
- 所谓副本(Replica),本质是一个只能追加写消息的提交日志。
根据Kafka副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的Broker上,从而能够对抗部分Broker宕机带来的数据不可用。
副本角色:
- 为解决分区下多个副本的内容一致性问题,常用方案就是采用基于领导者的副本机制。
- 在kafka中,副本分两类:领导者副本和追随者副本。每个分区在创建时都选举一个副本,称为领导者副本,其余的副本自动成为追随者副本。
- Kafka的副本机制比其他分布式系统严格。Kafka的追随者副本不对外提供服务。所有的请求都要由领导者副本处理。 追随者副本唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步。
- 当领导者副本所在Broker宕机了,Kafka依托于Zookeeper提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个新的领导者。当老的Leader副本重启回来后,只能作为追随者副本加入到集群中。
Kafka副本机制的优点:
- 方便实现“Read-your-writes”
- 含义:当使用生产者API向Kafka成功写入消息后,马上使用消息者API去读取刚才生产的消息。
- 如果允许追随者副本对外提供服务,由于副本同步是异步的,就可能因为数据同步时间差,从而使客户端看不到最新写入的消息。
- 方便实现单调读(Monotonic Reads)
- 单调读:对于一个消费者用户而言,在多处消息消息时,他不会看到某条消息一会存在,一会不存在。
- 如果允许追随者副本提供读服务,由于消息是异步的,则多个追随者副本的状态可能不一致。若客户端每次命中的副本不同,就可能出现一条消息一会看到,一会看不到。
In-sync Replicas(ISR)同步副本
- 追随者副本定期的异步拉取领导者副本中的数据,这存在不能和Leader实时同步的风险。
- Kafka引入了In-sync Replicas。ISR中的副本都是于Leader同步的副本,相反,不在ISR中的追随者副本就是被认为是与Leader不同步的。
- Leader 副本天然就在ISR中,即ISR不只是追随者副本集合,他必然包括Leader副本。甚至某些情况下,ISR只有Leade这一个副本。
- follower副本是否与leader同步的判断标准取决于Broker端参数
replica.lag.time.max.ms参数值。默认为10秒,只要一个Follower副本落后Leader副本的时间不连续超过10秒,那么Kafka就认为该Follower副本与leader是同步的,即使此时Follower副本中保存的消息明显小于Leader副本中的消息。 - 如果同步过程持续慢于Leader副本消息的写入速度,那么
replica.lag.time.max.ms时间后,此Follower副本就会被认为是与Leader副本不同步的,因此不能再放入ISR中。此时,kafka会自动收缩ISR的进度,将该副本“踢出”ISR。ISR是一个动态调整的集合,而非静态不变的。
Unclean 领导者选举(Unclean Leader Election)
- ISR是可以动态调整的,所以会出现ISR为空的情况,由于Leader副本天然就在ISR中,如果ISR为空了,这说明Leader副本也挂掉了,Kafka需要重新选举一个新的Leader。
- Kafka把所有不在ISR中的存活副本都会称为非同步副本。通常,非同步副本落后Leader太多,如果让这些副本做为新的Leader,就可能出现数据的丢失。在kafka中,选举这种副本的过程称为Unclean领导者选举。
- Broker端参数
unclean.leader.election.enable控制是否允许Unclean领导者选举。开启Unclean领导者选举可能会造成数据丢失,但它使得分区Leader副本一直存在,不至于停止对外提供服务,因此提升了高可用性。禁止Unclean领导者选举的好处是在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。
Kafka网络模型
Apache Kafka 自己定义了组请求协议,用于实现各种交互操作。常见有:
- PRODUCE 请求用于生产消息
- FETCH请求是用于消费消息
- METADATA请求是用于请求Kafka集群元数据信息。
Kafka定义了很多类似的请求格式,所有的请求都是通过TCP网络以Socket的方式进行通讯的。
KaKfa Broker端处理请求
Kafka使用Reactor模式,Reactor模式是事件驱动架构的一种实现方式,特别适应用于处理多个客户端并发向服务端发送请求的场景。
Kafka的请求处理流程
-
Reactor模式中,多个客户端发送请求到Reactor。Reactor有个请求分发线程Dispatcher,它会将不同的请求下发到多个工作线程中处理。
Acceptor线程只用于请求分发,不涉及具体逻辑处理,因此有很高的吞吐量。而工作线程可以根据实际业务处理需要任意增减,从而动态调节系统负载能力。 -
kakfa中,Broker端有个SocketServer组件,类似于Reactor模式中的Dispatcher,他也有对应的Acceptor线程和一个工作线程池,在kafka中,被称为网络线程池。Broker端参数
num.network.threads,用于调整该网络线程池的线程数,默认为3,表示每台Broker启动时,会创建3个网络线程,专门处理客户端发送的请求。 -
Acceptor线程采用轮询的方式将入站请求公平的发送到所有网络线程中。
-
当网络线程接收到请求后,Kafka在这个环节又做了一层异步线程池的处理。
- 当网络线程拿到请求后,她不是自己处理,而是将请求放入到一个共享请求队列中。
- Broker端还有个IO线程池,负责从该队列中取出请求,执行真正的处理。如果是PRODUCE生产请求,则将消息写入到底层的磁盘日志中;如果是FETCH请求,则从磁盘或页缓存中读取消息。
-
IO线程池中的线程是执行请求逻辑的线程。Broker端参数num.io.threads控制了这个线程数,默认为8,表示每台Broker启动后自动创建8个IO线程处理请求。
-
请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的。原因在于Dispatcher只是用于请求分发而不负责响应回传,因此只能让每个网络线程自己发送Repsone给客户端,所有这些Response没必要放在一个公共的地方。
-
Purgatory组件,专门用来缓存延时请求(Delayed Requset)。如设置了acks=all的PRODUCE请求,该请求要必须等待ISR中所有副本都接收了消息后才能返回,此时处理该请求的IO线程就必须瞪大其他Broker的写入结果。当请求不能立即处理时,他就会暂存在Purgatory中。待满足了完成条件,IO线程会继续处理该请求,并将Response放入到对应的网络线程的响应队列中。
延伸阅读 简谈Kafka中的NIO网络通信模型
下图为0.11.0版本网络模型
数据类请求与控制类的请求
到目前为止,我提及的请求处理流程对于所有请求都是适用的,也就是说,Kafka Broker 对所有请求是一视同仁的。但是,在 Kafka 内部,除了客户端发送的 PRODUCE 请求和 FETCH 请求之外,还有很多执行其他操作的请求类型,比如负责更新 Leader 副本、 Follower 副本以及 ISR 集合的 LeaderAndIsr 请求,负责勒令副本下线的 StopReplica 请 求等。与 PRODUCE 和 FETCH 请求相比,这些请求有个明显的不同:它们不是数据类的请求,而是控制类的请求。 也就是说,它们并不是操作消息数据的,而是用来执行特定的 Kafka 内部动作的。
Kafka 社区把 PRODUCE 和 FETCH 这类请求称为数据类请求,把 LeaderAndIsr、 StopReplica 这类请求称为控制类请求。 细究起来,当前这种一视同仁的处理方式对控制类 请求是不合理的。为什么呢?因为控制类请求有这样一种能力:它可以直接令数据类请求失 效!
那么,社区是如何解决的呢?很简单,你可以再看一遍今天的第三张图,社区完全拷贝了这 张图中的一套组件,实现了两类请求的分离。也就是说,Kafka Broker 启动后,会在后台 分别创建网络线程池和 IO 线程池,它们分别处理数据类请求和控制类请求。至于所用的 Socket 端口,自然是使用不同的端口了,你需要提供不同的listeners 配置,显式地指定 哪套端口用于处理哪类请求。
你可以这样设定来控制control-plane请求由哪个监听器来控制:
control.plane.listener.name=CONTROLLER
listener.security.protocol.map = INTERNAL:PLAINTEXT, EXTERNAL:SSL, CONTROLLER:SSL
listeners = INTERNAL://192.1.1.8:9092, EXTERNAL://10.1.1.5:9093, CONTROLLER://192.1.1.8:9094
Kafka Controller
控制器组件(Controller),是Apache Kafka的核心组件。它的主要作用是Apache Zookeeper的帮助下管理和协调整个Kafka集群。 集群中任意一台Broker都能充当控制器的角色,但在运行过程中,只能有一个Broker成为控制器。
Controller的产生 控制器是被选出来的,Broker在启动时,会尝试去Zookeeper中创建/controller节点。Kafka当前选举控制器的规则是:第一个成功创建/controller节点的Broker会被指定为控制器。
Controller的功能
- 主题管理(创建,删除,增加分区)
当执行kafka-topics脚本时,大部分的后台工作都是控制器来完成的。 - 分区重分配
Kafka-reassign-partitions脚本提供的对已有主题分区进行细粒度的分配功能。 - Preferred领导者选举
Preferred领导者选举主要是Kafka为了避免部分Broker负载过重而提供的一种换Leade的方案。 - 集群成员管理(新增Broker,Broker主动关闭,Broker宕机)
控制器组件会利用watch机制检查Zookeeper的/brokers/ids节点下的子节点数量变更。当有新Broker启动后,它会在/brokers下创建专属的znode节点。一旦创建完毕,Zookeeper会通过Watch机制将消息通知推送给控制器,这样,控制器就能自动地感知到这个变化。进而开启后续新增Broker作业。
侦测Broker存活性则是依赖于刚刚提到的另一个机制:临时节点。每个Broker启动后,会在/brokers/ids下创建一个临时的znode。当Broker宕机或主机关闭后,该Broker与Zookeeper的会话结束,这个znode会被自动删除。同理,Zookeeper的Watch机制将这一变更推送给控制器,这样控制器就能知道有Broker关闭或宕机了,从而进行善后。 - 数据服务
控制器上保存了最全的集群元数据信息,其他所有Broker会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。
控制器保存的数据
控制器中保存的这些数据在Zookeeper中也保存了一份。每当控制器初始化时,它都会从Zookeeper上读取对应的元数据并填充到自己的缓存中。
内部设计原理
0.11版本之前控制器的内部设计相当复杂,控制器是多线程的设计,会在内部创建很多线程。如:
- 为每个Broker创建一个对应的Socket连接,然后在创建一个专属的线程,用于向这些Broker发送特定的请求。
- 控制连接zookeeper,也会创建单独的线程来处理Watch机制通知回调。
- 控制器还会为主题删除创建额外的I/O线程。这些线程还会访问共享的控制器缓存数据,为了维护数据安全性,控制在代码中大量使用ReetrantLock同步机制,进一步拖慢了整个控制器的处理速度。
在0.11版对控制器的低沉设计进了重构。
- 最大的改进是:把多线程的方案改成了单线程加事件对列的方案。
- 单线程+队列的实现方式:社区引入了一个事件处理线程,统一处理各种控制器事件,然后控制器将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费。
- 单线程不代表之前提到的所有线程都被干掉了,控制器只是把缓存状态变更方面的工作委托给了这个线程而已。
- 第二个改进:将之前同步操作Zookeeper全部改为异步操作。
- Zookeeper本身的API提供了同步写和异步写两种方式。同步操作zk,在有大量主题分区发生变更时,Zookeeper容易成为系统的瓶颈。
未来的趋势将逐渐脱离zookeeper
Kafka即将移除Zookeeper依赖?
Kafka重大更新,将不在依赖zookeeper
高水位和Leader Epoch
kafka的水位概念: kafka的水位不是时间戳,与时间无关。他是和位置信息绑定的,它是用消息位移来表征的。
Kafka源码使用的表述是高水位。在Kafka中也有低水位(Low Watermark),它是与Kafka删除消息相关的概念。
高水位作用
- 定义消息可见性,用来标识分区下的哪些消息是可以被消费者消费的。
- 帮助Kafka完成副本同步。
“已提交消息” 和 “未提交消息”
- 在分区高水位以下的消息被认为是已提交消息,反之就是未提交消息。
- 消费者只能消费已提交消息。
- 这不是Kafka的事务,因为事务机制会影响消息者所能看到的消息的范围,他不只是简单依赖高水位来判断。他依靠一个名为LSO(Log Stable Offset)的位移值来判断事务型消费者的可见性。
- 位移值等于高水位的消息也属于未提交消息。即,高水位消息的消息是不能被消费者消费的。
- 日志末端位移的概念:Log End Offset,简写是LEO。他表示副本写入下一条消息的位移值。 同一个副本对象,其高水位值不会大于LEO值。
- 高水位和LEO是副本对象的两个重要属性。Kafka所有副本都有对应的高水位和LEO值,而不仅仅是Leader副本。只是Leader副本比较特殊,Kafka使用Leader副本的高水位来定义所在分区的高水位。即,分区的高水位就是其Leader副本的高水位。
高水位更新机制
- 在Leader副本所在Broker上,还保存了其他Follower副本的LEO值。而其他Broker上仅仅保存该分区的某个Follower副本。Kafka将Leader副本所在Broker上保存的这些Follower副本称为远程副本。
- Kafka副本机制在运行过程中,会更新Broker1上Follower副本的高水位和LEO值,同时也会更新Broker0上Leader副本的高水位和LEO,以及所有远程副本的LEO。但它不会更新远程副本的高水位值。
- Broker0上保存这些远程副本的作用是帮助Leader副本确定其高水位,即分区高水位。
什么叫与 Leader 副本保持同步。判断的条件有两个。
- 该远程 Follower 副本在 ISR 中。
- 该远程 Follower 副本 LEO 值落后于 Leader 副本 LEO 值的时间,不超过 Broker 端参数
replica.lag.time.max.ms的值。如果使用默认值的话,就是不超过 10 秒。
总结
一、Leader副本
-
处理生产者请求的逻辑:
- 写入消息到本地磁盘。
- 更新分区高水位值
- 获取Leader副本所在Broker端保存的所有远程副本LEO值{LEO-1,LEO-2,……,LEO-n}。
- 获取Leader副本高水位值:currentHW。
- 更新currentHW = max(currentHW ,min(leo-1,leo-2,……leo-n)).
-
处理follwer副本拉取消息的逻辑:
- 读取磁盘(或页缓存)中的消息数据
- 使用Follower副本发送请求中的位移值更新远程副本LEO值。
- 更新分区高水位值(具体步骤与处理生产者请求的步骤相同)
二、Follower副本
从Leader拉取消息的处理逻辑:
- 写入消息到本地磁盘
- 更新LEO值
- 更新高水位值
- 获取Leader发送的高水位值:currentHW。
- 获取步骤2中更新过的LEO值:currentLEO。
- 更新高水位为min(currentHW,currentLEO)。
Leader Epoch
Leader Epoch概念
用来规避因高水位更新错配导致的各种不一种问题。所谓Leader Epoch大致可以认为是Leader版本。
Leader Epoch的组成
由两部分数据组成:
- Epoch。一个单调增加的版本号。每当领导权发生变更时,都会增加该版本号。小版本号的Leader被认为是过期的Leader,不能在行使Leader权利。
- 起始位移(Start Offset)。Leader副本在改Epoch值上写入的首条消息的位移。
Kafka Broker会在内存中为每个分区都缓存Leader Epoch数据,同时他还会定期地将这些信息持久化到一个checkpoint文件中。
Q:老师,在启用了Leader Epoch机制后,Follower副本B重启回来后,是如何知道Leader得LEO值?这也是Leader Epoch 机制里面得吗?
A: Follower副本B重启回来后,它会从leader处拉取最新的leader epoch项对应的end offset,如果是最新的epoch,那么其end offset对应的就是LEO值