Broker
zookeeper存储到kafka信息
- brokers:
- Ids: 记录有哪些服务器在线。
- topic: 记录主题的分区。分区的state记录leader分区和isr信息。
- consumers:
- 0.9版本之前通讯压力比较大,每个消费者都将相关的消费信息放到zk里面,那就需要频繁的操作。每次拉取之后都需要提交新的offset值。
- 辅助Leader选举。每一个broker节点都有一个controller。每个节点的controller都会去zk注册controller里面的节点,谁先注册上,谁就是辅助Leader选举的controller。
Broker工作流程
-
broker节点启动后注册到zk中,在zk的节点brokers/ids可以看到注册的节点数。
-
broker每个节点都有一个controller模块,都会争先的到zk的controller节点注册。选举出来的Controller监听brokers节点的变化。然后进行真正的leader选举。
- 选举规则: isr中存活的节点为前提,按照AR中排在前面的优先,例如AR[1,0,2],isr[1,0,2]。那么leader会按照1,0,2的顺序轮询。
-
选举出来的leader信息,通知到zk节点中的brokers/topics/first/partitions/0/state中。其他节点会从zk同步信息
-
生产者发送消息,follower主动从leader同步信息。kafka消息的存储机制:
- Kafka 是基于「主题 + 分区 + 副本 + 分段 + 索引」的结构:
- kafka以topic来进行消息管理,每个topic包含多个partition,每个partition对应一个逻辑log,有多个segment组成。
- Partition 分区内每条消息都会被分配一个唯一的消息 id,即我们通常所说的 偏移量 Offset, 因此 kafka 只能保证每个分区内部有序性,并不能保证全局有序性。
- 然后每个 Partition 分区又被划分成了多个 LogSegment,这是为了防止 Log 日志过大,Kafka 又引入了日志分段(LogSegment)的概念,将 Log 切分为多个 LogSegement,相当于一个巨型文件被平均分割为一些相对较小的文件,这样也便于消息的查找、维护和清理。这样在做历史数据清理的时候,直接删除旧的 LogSegement 文件就可以了。
- 每个partition在内存中对应一个index,记录每个segment中的第一条消息偏移。
-
如果leader挂了,那么其他broker种的Controller监听到节点变化,从新获取ISR。选举新的Leader。
节点的服役和退役
节点的服役
-
创建一个需要均衡的主题
vim topics-to-move.json { "topics": [ {"topic": "XXX"} ], "version": 1 }
-
生成一个均衡主题的计划。topic本身使用0,1,2进行存储消息,现在使用0,1,2,3进行存储
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate Current partition replica assignment {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[1,2,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]} Proposed partition reassignment configuration #生成的计划 {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
-
创建副本计划。(所有副本存储在 broker0、broker1、broker2、broker3 中)。
vim increase-replication-factor.json {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
-
执行副本计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --execute
-
验证副本计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --verify
节点的退役
-
创建一个需要均衡的主题
vim topics-to-move.json { "topics": [ {"topic": "XXX"} ], "version": 1 }
-
生成一个均衡主题的计划。将退役的节点从broker-list中剔除
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2" --generate Current partition replica assignment {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]} Proposed partition reassignment configuration {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]}]}
-
创建副本计划。(所有副本存储在 broker0、broker1、broker2中)。
vim increase-replication-factor.json {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]}]}
-
执行副本计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --execute
-
验证副本计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --verify
kafka-副本
副本一致性保证
kafka的副本主要是提高了数据的可靠性。默认副本是1个,生产环境一般配置为2个,副本太多会增加磁盘存储空间,增加网络数据传输。
副本中主要包含leader和Follower。生产者和消费者操作的是leader。Follower只做数据同步
分区中的副本统称为AR。AR=ISR(表示和 Leader 保持同步的 Follower 集合。如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms
参数设定,默认 30s。Leader 发生故障之后,就会从 ISR 中选举新的 Leader)+OSR(表示 Follower 与 Leader 副本同步时,延迟过多的副本)。
- LEO: Log End Offset的缩写,它表示了当前日志文件中下一条待写入消息的offset。就是每个副本的最后一个offset。也就是最新的offset+1
- 如何更新LEO值
- leader的LEO就保存在其所在的broker的缓存里,当leader副本log文件写入消息后,就会更新自己的LEO。
- 如果是remote LEO(leader中保存所有Follower的LEO),Leader端的Follower的LEO更新发生在Leader在处理Follower FETCH请求时。一旦Leader接收到Follower发送的FETCH请求,它先从Log中读取相应的数据,给Follower返回数据前,先更新Follower的LEO(remote LEO)。
- Follower副本的LEO值就是日志的LEO值,每当新写入一条消息,LEO值就会被更新。当Follower发送FETCH请求后,Leader将数据返回给Follower,此时Follower开始Log写数据,从而自动更新LEO值。
- Follower 副本所在节点只保存了自己的 LEO,而 Leader 副本所在的节点则保存了该分区下所有副本(包括leader)的 LEO。
- 如何更新LEO值
- HW: High Watermak的缩写, 俗称高水位,它表示了一个特定消息的偏移量(offset),消费者只能拉取到这个offset之前的消息。所有副本中最小的LEO。
- 一旦Follower向Log写完数据,它就会尝试更新HW值。比较自己的LEO值与fetch响应中leader副本的HW值,取最小者作为follower副本的HW值。
- Leader会尝试去更新分区HW的四种情况
- Follower副本成为Leader副本时:Kafka会尝试去更新分区HW。
- Broker崩溃导致副本被踢出ISR时:检查下分区HW值是否需要更新。
- 生产者向Leader副本写消息时:因为写入消息会更新Leader的LEO,有必要检查HW值是否需要更新
- Leader处理Follower FETCH请求时:首先从Log读取数据,之后尝试更新分区HW值。
- Leader如何更新自己的HW值?
- Leader broker上保存了一套Follower副本的LEO以及自己的LEO。当尝试确定分区HW时,它会选出所有满足条件的副本,比较它们的LEO(包括Leader的LEO),并选择最小的LEO值作为HW值。
- 满足条件
- 处于ISR中
- 副本LEO落后于Leader LEO的时长不大于
replica.lag.time.max.ms
参数值(默认是10s)
- 满足条件
- Leader broker上保存了一套Follower副本的LEO以及自己的LEO。当尝试确定分区HW时,它会选出所有满足条件的副本,比较它们的LEO(包括Leader的LEO),并选择最小的LEO值作为HW值。
- Leader 副本和 Follower 副本所在节点均只保存了各自的 HW。
HW和LOE如何更新
-
初始状态: 初始时Leader和Follower的HW和LEO都是0(严格来说源代码会初始化LEO为-1)。Leader中的Remote LEO指的就是Leader端保存的Follower LEO,也被初始化成0。此时,生产者没有发送任何消息给Leader,而Follower已经开始不断地给Leader发送FETCH请求了,但因为没有数据因此什么都不会发生。值得一提的是,Follower发送过来的FETCH请求因为无数据而暂时会被寄存到Leader端的purgatory中,待500ms (
replica.fetch.wait.max.ms
参数)超时后会强制完成。倘若在寄存期间生产者发来数据,则Kafka会自动唤醒该FETCH请求,让Leader继续处理。 -
produce发送消息。
- 生产者发送消息,Leader接收到生产者消息之后,将消息写入Log,同时自动更新Leader自己的Leo;
- 尝试更新Leader HW值。假设此时Follower尚未发送FETCH请求,Leader端保存的Remote LEO依然是0,因此Leader会比较它自己的LEO值和Remote LEO值,发现最小值是0,与当前HW值相同,故不会更新分区HW值(仍为0)
- 此时生产者请求处理完之后,Leader端的HW值依然是0,而LEO是1,Remote LEO也是0。
-
此时Follower发送FETCH请求。Leader端处理的顺序是
- 读取Log数据
- 更新remote LEO = 0(为什么是0? 因为此时Follower还没有写入这条消息。Leader如何确认Follower还未写入呢?这是通过Follower发来的FETCH请求中的Fetch offset来确定的)
- 尝试更新分区HW:此时Leader LEO = 1,Remote LEO = 0,故分区HW值= min(Leader LEO,Follower Remote LEO) = 0
- 把数据和当前分区HW值(依然是0)发送给Follower副本
-
而Follower副本接收到FETCH Response后依次执行下列操作:
- 写入本地Log,同时更新Follower自己管理的 LEO为1
- 更新Follower HW:比较本地LEO和 FETCH Response 中的当前Leader HW值,取较小者,Follower HW= 0
- 此时,第一轮FETCH RPC结束,我们会发现虽然Leader和Follower都已经在Log中保存了这条消息,但分区HW值尚未被更新,仍为0。
-
Follower发起第二轮请求
- Leader处理顺序
- 读取Log数据
- 更新Remote LEO = 1(这次为什么是1了? 因为这轮FETCH RPC携带的fetch offset是1,那么为什么这轮携带的就是1了呢,上一轮结束后Follower LEO被更新为1了)
- 尝试更新分区HW:此时leader LEO = 1,Remote LEO = 1,故分区HW值= min(Leader LEO,Follower Remote LEO) = 1。
- 把数据(实际上没有数据)和当前分区HW值(已更新为1)发送给Follower副本作为Response
- Follower副本接收到FETCH response后依次执行下列操作
- 写入本地Log,当然没东西可写,Follower LEO也不会变化,依然是1
- 更新Follower HW:比较本地LEO和当前Leader LEO取小者。由于都是1,故更新followerHW = 1 。
- 此时消息已经成功地被复制到Leader和Follower的Log中且分区HW是1,表明消费者能够消费offset = 0的消息。
- Leader处理顺序
-
副本的同步请求发送到Leader的时候,此时没有数据
- 当Leader无法立即满足FECTH返回要求的时候(比如没有数据),那么该FETCH请求被暂存到Leader端的purgatory中(炼狱),待时机成熟尝试再次处理。Kafka不会无限期缓存,默认有个超时时间(500ms),一旦超时时间已过,则这个请求会被强制完成。当寄存期间还没超时,生产者发送请求处理完之后会唤醒副本的FECTH请求。Leader端处理流程如下:
- Leader写Log(自动更新Leader LEO)
- 尝试唤醒在purgatory中寄存的FETCH请求
- 尝试更新分区HW
- 当Leader无法立即满足FECTH返回要求的时候(比如没有数据),那么该FETCH请求被暂存到Leader端的purgatory中(炼狱),待时机成熟尝试再次处理。Kafka不会无限期缓存,默认有个超时时间(500ms),一旦超时时间已过,则这个请求会被强制完成。当寄存期间还没超时,生产者发送请求处理完之后会唤醒副本的FECTH请求。Leader端处理流程如下:
节点出现故障的处理
Follower出现故障
|
|
|
Leader出现故障
|
|
- Leader故障之后,会从ISR剔除出去,然后按照选举规则选举出一个新的Leader。
- 为保证多副本之间的数据一致性,其余的Follower会先将各自的log文件中将高于HW的部分截掉,然后从新的Leader同步数据。
需要注意的是: 这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
在 Kafka 0.11.0.0 版本之前,Follower 都是基于 HW 机制去同步数据的,但是使用中发现这种机制会导致数据丢失和数据不一致的问题,所以从 0.11.0.0 版本开始,Kafka 引入了 Leader Epoch 机制,以此来解决基于 HW 进行同步所带来的问题。
数据丢失问题分析
假设某一时刻:副本A(Follower)的 LEO = 2 HW = 1, 副本B(Leader)的 LEO = 2 HW =1。 此时副本A 发起再一轮的拉取,携带自身LEO = 2,副本B接收到后更新自身 HW = 2,但是在返回给副本A 响应时,副本A 所在节点宕机。当副本A 所在节点重启时,会根据 HW = 1 进行日志截断,即删除 m2 这条数据。然后向副本B 所在节点发送拉取数据请求。
如果此时正好副本B 所在的节点也宕机了,那么原来的副本A 被选为 Leader 副本,其 HW =1。之后副本B 所在节点重启后,副本B 成为 Follower 副本,由于 Follower 副本的 HW 不能比 Leader 副本的 HW 高,所以副本B 还会进行一次日志截断,删除消息m2 并将 HW 调整为1。这就导致 m2 这条消息永远丢失了。
数据不一致问题分析:
假设某一时刻:副本A(Leader)已经写入2条消息 m1 和 m2 ,其 LEO = 2 HW = 2, 副本B(Follower)只同步了1条消息 m1,其 LEO = 1 HW =1。这里解释下为什么 Follower 的 LEO = 1,而 Leader 副本的 HW 却可以为2,正常来说,Leader 副本的 HW 应该是 ISR 列表中最小的 LEO。由于配置了 `min.isync.replicas` (Kafka ISR 列表中最小同步副本数)= 1,所以 Follower 对应的 Replica 对象就不在 ISR 列表,该列表中只有 Leader 副本对应的 Replica 对象,所以 Leader 副本的 HW 由自身 LEO 决定。
如果此时副本A 所在的节点和副本B 所在的节点同时宕机,随后副本B 所在节点先重启,那么副本B 成为 Leader 副本,其 HW = 1,LEO =1。这时客户端往该分区又写入一条消息 m3,即写入了副本B,其 HW 和 LEO 值均更新为2。当副本A 所在节点重启时,发现其 HW 和 LEO 值都和副本B 一致,那么就不会进行日志截断操作也不会再同步m3这条数据。
此时副本A 中有2条消息 m1 和 m2,而副本B 中也有2条消息,m1 和 m3,这就导致了两个副本的数据不一致问题。
Leader Epoch
从 0.11.0.0 版本开始,引入了leader epoch 的概念,在需要进行日志截断的时候使用 leader epoch 作为参考,而不是原来的 HW。leader epoch 代表 leader 的纪元信息,初始值为0。每当 leader 变更一次,该值就会加 1 。与此同时,每个副本中还会增加一个对应关系:LeaderEpoch -> StartOffset,其中StartOffset 表示该 LeaderEpoch 下写入的第一条消息的偏移量,这个StartOffset也可以理解为上一个LeaderEpoch下副本的 LEO 值。每个副本的Log下都有一个 leader-epoch-checkpoint 文件,在发生 leader epoch 变更时,会将LeaderEpoch -> StartOffset 的对应关系追加到这个文件。
所谓Leader epoch实际上是一对值:<epoch, offset>:
epoch表示Leader的版本号,从0开始,Leader变更过1次,epoch+1
offset对应于该epoch版本的Leader写入第一条消息的offset。因此假设有两对值:
<0, 0>
<1, 120>
则表示第一个Leader从位移0开始写入消息;共写了120条[0, 119];
而第二个Leader版本号是1,从位移120处开始写入消息。
如何解决数据丢失问题:
假设某一时刻,副本A和副本B的 LEO 都为2,副本A 的 HW 为1,副本B 的 HW 为2。副本A 和副本B 的LeaderEpoch(LE)和 StartOffset 均为0。
此时副本A 所在节点宕机,之后进行重启。这时不会立刻根据副本A 的HW 进行日志截断,而是先向 Leader 副本发送 OffsetsForLeaderEpochRequest 请求,携带自身的 LeaderEpoch 值。随后 Leader 副本会返回 OffsetsForLeaderEpochResponse 响应,携带当前的 LEO 值2。
副本B 接收到 Leader 副本返回的 LEO=2,和自身的 LEO 值相等,则不会再进行日志截断,也就保留了m2这条消息。
如果之后副本B所在节点宕机,副本A 被选为 Leader 副本,那么其LeaderEpoch更新为1,对应的StartOffset更新为2。后面无论副本B 所在节点是否重启,客户端的生产数据请求都可以追加到副本A 对应的日志文件中。
如何解决数据不一致问题:
假设某一时刻,副本A 有两条消息 m1 和 m2,HW 和 LEO 均为2;副本B 有一条消息 m1,HW 和 LEO 均为1。两个副本的LeaderEpoch 和 StartOffset均为0,此时两个副本所在的节点均宕机。
随后,副本B 所在的节点先重启,副本B 成为 Leader 副本,将LeaderEpoch 和 StartOffset 更新为1。然后客户端向副本B 发送消息m3,副本B 写入后,将 HW 和 LEO 更新为2。
之后副本A 所在的节点进行重启,副本A 成为 Follower 副本。然后向副本B 发送 OffsetsForLeaderEpochRequest 请求,携带自身的LeaderEpoch=0。副本B 根据收到的副本A 的 LeaderEpoch=0,发现和当前自身的 LeaderEpoch=1 不一致,则将自身 LeaderEpoch 对应的 StartOffset = 1 作为副本A 的LEO 返回。
副本A 根据返回的 LEO =1 进行日志截断,删除消息 m2。随后副本A 正常向副本B 拉取数据,同步消息m3,更新自身 HW 和 LEO 为2。至此,通过 LeaderEpoch 机制就解决了两个副本数据不一致的问题。
分区副本的分配
自动分配
如果kafka服务器有四个副本,且设置的分区数大于服务器台数,kafka分配副本的规则如下。
#创建16个分区,三个副本
bin/kafka-topics.sh --bootstrap-server node01:9092 --describe --topic second
Topic: second TopicId: WSdQ_UR0RnOdfMrDollB3Q PartitionCount: 16 ReplicationFactor: 3 Configs: segment.bytes=1073741824
#副本之间相差三位 0,3,1
Topic: second Partition: 0 Leader: 0 Replicas: 0,3,1 Isr: 0,3,1
Topic: second Partition: 1 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
Topic: second Partition: 2 Leader: 3 Replicas: 3,0,2 Isr: 3,0,2
Topic: second Partition: 3 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
#副本之间相差一位 0,1,2
Topic: second Partition: 4 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: second Partition: 5 Leader: 2 Replicas: 2,0,3 Isr: 2,0,3
Topic: second Partition: 6 Leader: 3 Replicas: 3,2,1 Isr: 3,2,1
Topic: second Partition: 7 Leader: 1 Replicas: 1,3,0 Isr: 1,3,0
#副本之间相差2位 0,2,3
Topic: second Partition: 8 Leader: 0 Replicas: 0,2,3 Isr: 0,2,3
Topic: second Partition: 9 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1
Topic: second Partition: 10 Leader: 3 Replicas: 3,1,0 Isr: 3,1,0
Topic: second Partition: 11 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
#副本之间相差3位 0,3,1
Topic: second Partition: 12 Leader: 0 Replicas: 0,3,1 Isr: 0,3,1
Topic: second Partition: 13 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
Topic: second Partition: 14 Leader: 3 Replicas: 3,0,2 Isr: 3,0,2
Topic: second Partition: 15 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
手动分配分区
假设生产上有四个副本,想让topic的分区只保存在其中两个副本中。而不是根据kafka的分区副本规则保存。
-
创建一个新的topic
#创建一个4个分区,两个副本的topic bin/kafka-topics.sh --bootstrap-server node01:9092 --create --partitions 4 --replication-factor 2 --topic three
-
查看分区副本的情况
bin/kafka-topics.sh --bootstrap-server node01:9092 --describe --topic three Topic: three TopicId: E95kRGLIS3C4dFCA5XcgdA PartitionCount: 4 ReplicationFactor: 2 Configs: segment.bytes=1073741824 Topic: three Partition: 0 Leader: 2 Replicas: 2,1 Isr: 2,1 Topic: three Partition: 1 Leader: 3 Replicas: 3,0 Isr: 3,0 Topic: three Partition: 2 Leader: 1 Replicas: 1,2 Isr: 1,2 Topic: three Partition: 3 Leader: 0 Replicas: 0,3 Isr: 0,3
-
创建副本计划
vim increase-replication-factor.json { "version":1, "partitions":[ {"topic":"three","partition":0,"replicas":[0,1]}, {"topic":"three","partition":1,"replicas":[0,1]}, {"topic":"three","partition":2,"replicas":[1,0]}, {"topic":"three","partition":3,"replicas":[1,0]}] }
-
执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --execute Current partition replica assignment {"version":1,"partitions":[{"topic":"three","partition":0,"replicas":[2,1],"log_dirs":["any","any"]},{"topic":"three","partition":1,"replicas":[3,0],"log_dirs":["any","any"]},{"topic":"three","partition":2,"replicas":[1,2],"log_dirs":["any","any"]},{"topic":"three","partition":3,"replicas":[0,3],"log_dirs":["any","any"]}]} Save this to use as the --reassignment-json-file option during rollback Successfully started partition reassignments for three-0,three-1,three-2,three-3
-
验证副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --verify Status of partition reassignment: Reassignment of partition three-0 is complete. Reassignment of partition three-1 is complete. Reassignment of partition three-2 is complete. Reassignment of partition three-3 is complete. Clearing broker-level throttles on brokers 0,1,2,3 Clearing topic-level throttles on topic three
-
查看分区副本的存储情况
bin/kafka-topics.sh --bootstrap-server node01:9092 --describe --topic three Topic: three TopicId: E95kRGLIS3C4dFCA5XcgdA PartitionCount: 4 ReplicationFactor: 2 Configs: segment.bytes=1073741824 Topic: three Partition: 0 Leader: 0 Replicas: 0,1 Isr: 1,0 Topic: three Partition: 1 Leader: 0 Replicas: 0,1 Isr: 0,1 Topic: three Partition: 2 Leader: 1 Replicas: 1,0 Isr: 1,0 Topic: three Partition: 3 Leader: 1 Replicas: 1,0 Isr: 0,1
Leader Partition 负载平衡
正常情况下,kafka本身会自动把Leader Partition均匀的分散在各个机器上,来保证每台机器的读写吞吐量都是均衡的。但是如果某些broker宕机,会导致Leader Partition过于集中在其他少部分的几台broker上,这会导致少数几台beoker上的读写请求压力过高,其他宕机的broker重启之后都是follower Partition,读写请求很低,造成集群负载不均衡。
auto.leader.rebalance.enable
默认是 true。 自动 Leader Partition 平衡。生产环境中,leader 重选举的代价比较大,可能会带来性能影响,建议设置为 false 关闭。leader.imbalance.per.broker.percentage
默认是 10%。每个 broker 允许的不平衡的 leader的比率。如果每个 broker 超过了这个值,控制器会触发 leader 的平衡。-
针对broker0节点,分区2的AR优先副本是0节点,但是0节点却不是Leader节点(存在副本宕机从新选举leader)。所以不平衡数加1,AR副本总数是4.所以broker0节点不平衡为1/4>10%。需要再均衡
-
broker2和broker3节点和broker0不平衡率一样,需要再均衡。
-
broker1的不平衡数为0,所以不需要再均衡。
-
当AR副本中排第一的节点,不是leader Partition 的时候,该节点的不平衡数加1,然后除以AR副本中所有的节点数,看是否大于10%。超过则触发再均衡。
leader.imbalance.check.interval.seconds
默认值 300 秒。检查 leader 负载是否平衡的间隔时间。
增加副本因子
在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。副本数的增加需要先制定计划,然后根据计划执行。
不能通过命令行的方法添加副本。
-
创建topic
bin/kafka-topics.sh --bootstrap-server node01:9092 --create --partitions 3 --replication-factor 1 --topic four Created topic four.
-
创建手动增加副本的存储计划(所有副本都指定存储在 broker0、broker1、broker2 中)。
vim increase-replication-factor.json {"version":1,"partitions":[ {"topic":"four","partition":0,"replicas":[0,1,2]}, {"topic":"four","partition":1,"replicas":[0,1,2]}, {"topic":"four","partition":2,"replicas":[0,1,2]}] }
-
执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server node01:9092 --reassignment-json-file increase-replication-factor.json --execute Current partition replica assignment {"version":1,"partitions":[{"topic":"four","partition":0,"replicas":[3],"log_dirs":["any"]},{"topic":"four","partition":1,"replicas":[1],"log_dirs":["any"]},{"topic":"four","partition":2,"replicas":[0],"log_dirs":["any"]}]} Save this to use as the --reassignment-json-file option during rollback Successfully started partition reassignments for four-0,four-1,four-2
-
查看详情
bin/kafka-topics.sh --bootstrap-server node01:9092 --describe --topic four Topic: four TopicId: WG70RfSsRDipj6VpEXgeVw PartitionCount: 3 ReplicationFactor: 3 Configs: segment.bytes=1073741824 Topic: four Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 2,0,1 Topic: four Partition: 1 Leader: 1 Replicas: 0,1,2 Isr: 1,2,0 Topic: four Partition: 2 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
文件存储机制
日志文件
kafka的消息是以主题为单位进行归类的,然后在存储的时候,有按照分区进行存储。每个分区对应一个log文件,该log文件就是存储生产者生产的消息。每个生产者的数据会不断的追加到log文件的末端,为防止log文件过大导致数据定位效率低下。kafka采用分片和索引机制,将每个分区分为多个segment。每个segment包括:.index文件、.log文件和.timeindex等文件。这些文件保存在一个文件夹下,该文件夹的命名规则为:topic名称+分区序列号。
后缀名 | 说明 |
---|---|
.index | 偏移量索引文件 |
.timestamp | 时间戳索引文件 |
.log | 日志文件 |
.snapshot | 快照文件 |
.deleted | |
.cleaned | 日志清理时临时文件 |
.swap | 日志压缩之后的临时文件 |
每个 LogSegment 都有一个基准偏移量,表示当前 LogSegment中第一条消息的 offset。偏移量是一个 64 位的长整形数,固定是20位数字,长度未达到,用0进行填补,索引文件和日志文件都由该作为文件名命名规则(00000000000000000000.index、00000000000000000000.timestamp、00000000000000000000.log)。如果日志文件名为 000000000000000004096.log ,则当前日志文件的一条数据偏移量就是4096(偏移量从 0 开始)。
#进入主题分区的文件夹(topic名称+分区序列号) 使用此命令可以查看文件
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log
offset:相对偏移量。position:物理偏移地址
offset是逐渐增加的整数,每个offset对应一个消息的偏移量。position:消息批字节数,用于计算物理地址。CreateTime:时间戳。magic:2代表这个消息类型是V2,如果是0则代表是V0类型,1代表V1类型。compresscodec:None说明没有指定压缩类型,kafka目前提供了4种可选择,0-None、1-GZIP、2-snappy、3-lz4。 crc:对所有字段进行校验后的crc值。
配置参数 | 默认值 | 说明 |
---|---|---|
log.index.interval.bytes | 4096(4K) | 增加索引项字节间隔密度,会影响索引文件中的区间密度和查询效率 |
log.segment.bytes | 1073741824(1G) | 日志文件最大值 |
log.roll.ms | 当前日志分段中消息的最大时间戳与当前系统的时间戳的差值允许的最大范围,单位毫秒 | |
log.roll.hours | 168(7天) | 当前日志分段中消息的最大时间戳与当前系统的时间戳的差值允许的最大范围,单位小时 |
log.index.size.max.bytes | 10485760(10MB) | 触发偏移量索引文件或时间戳索引文件分段字节限额 |
- index文件记录了消息偏移量与物理地址之间的映射关系。为稀疏索引。大约每往log文件写入4kb数据,会往index文件写入一条索引。
log.index.interval.bytes
。且index文件里面的offset是相对的offset(文件名的offest+相对的offest=该条消息的真正的offset)。这样做的能确保offest的值所占的空间不会过大。 - kafka的消息都是由n条消息组成一批。每一批消息对应有一个描述信息,记录了这批消息的大小,偏移量范围baseOffset和lastOffset,位置(position)以及大小(batchSize)等信息。
- 当满足如下条件的时候,会触发文件的切片
log.segment.bytes
Kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分成块的大小,超过配置参数就会写入新的文件- 当前日志分段中消息的最大时间戳与当前系统的时间戳的差值大于
log.roll.ms
或log.roll.hours
参数配置的值。如果同时配置了log.roll.ms
和log.roll.hours
参数,那么log.roll.ms
的优先级高。默认情况下,只配置了log.roll.hours
参数,其值为168,即 7 天。 - 偏移量索引文件或时间戳索引文件的大小达到 broker 端参数
log.index.size.max.bytes
配置的值。log.index.size.max.bytes
- 追加的消息的偏移量与当前日志分段的偏移量之间的差值大于 Integer.MAX_VALUE,即要追加的消息的偏移量不能转变为相对偏移量(offset-baseOffset > Integer.MAX_VALUE)。
- 为什么会是 Integer.MAX_VALUE(1024 * 1024 * 1024=1073741824):在偏移量索引文件中,每个索引项共占用 8 个字节,并分为两部分。相对偏移量和物理地址。**相对偏移量:**表示消息相对与基准偏移量的偏移量,占 4 个字节。**物理地址:**消息在日志分段文件中对应的物理位置,也占 4 个字节。4 个字节刚好对应 Integer.MAX_VALUE ,如果大于 Integer.MAX_VALUE ,则不能用 4 个字节进行表示了。
- 索引文件切分过程
- 索引文件会根据 log.index.size.max.bytes 值进行预先分配空间,即文件创建的时候就是最大值。当真正的进行索引文件切分的时候,才会将其裁剪到实际数据大小的文件。这一点是跟日志文件有所区别的地方。
- 如何在log文件中找到offest=600的记录。
-
日志清理
kafka日志清理策略有delete 和 compact 两种。
delete
log.cleanup.policy : delete 所有数据启用删除策略
-
基于时间:默认打开。
-
kafka中默认的日志保存时间为7天。可以通过配置进行调整保存时间
log.retention.hours
,最低优先级小时,默认 7 天。log.retention.minutes
,分钟log.retention.ms
,最高优先级毫秒log.retention.check.interval.ms
,负责设置检查周期,默认 5 分钟。
-
Kafka 依据segment中最大的时间戳进行定位。首先要查询该日志分段所对应的时间戳索引文件,查找时间戳索引文件中最后一条索引项,若最后一条索引项的时间戳字段值大于 0,则取该值,否则取最近修改时间。
-
删除过程
- 从日志对象中所维护日志分段的跳跃表中移除待删除的日志分段,保证没有线程对这些日志分段进行读取操作。
- 这些日志分段所有文件添加 上 .delete 后缀。
- 交由一个以 “delete-file” 命名的延迟任务来删除这些 .delete 为后缀的文件。延迟执行时间可以通过 file.delete.delay.ms 进行设置
-
如果活跃的日志分段中也存在需要删除的数据时?
- Kafka 会先切分出一个新的日志分段作为活跃日志分段,该日志分段不删除,删除原来的日志分段。
-
-
基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。
log.retention.bytes
,默认等于-1,表示无穷大。- 删除过程
- 计算需要被删除的日志总大小 (当前日志文件大小(所有分段)减去retention值)。
- 从日志文件第一个Segment 开始查找可删除的日志分段的文件集合。
- 执行删除。
- 删除过程
-
基于偏移量
-
根据日志分段的下一个日志分段的起始偏移量是否大于等于日志文件的起始偏移量,若是,则可以删除此日志分段。**注意:**日志文件的起始偏移量并不一定等于第一个日志分段的基准偏移量,存在数据删除,可能与之相等的那条数据已经被删除了。
-
删除过程
- 从头开始遍历每个日志分段,日志分段1的下一个日志分段的起始偏移量为21,小于logStartOffset,将日志分段1加入到删除队列中
- 日志分段 2 的下一个日志分段的起始偏移量为35,小于 logStartOffset,将 日志分段 2 加入到删除队列中
- 日志分段 3 的下一个日志分段的起始偏移量为57,小于logStartOffset,将日志分段3加入删除集合中
- 日志分段4的下一个日志分段的其实偏移量为71,大于logStartOffset,则不进行删除。
-
compact 日志压缩
log.cleanup.policy : compact 所有数据启用压缩策略
对于相同的key不同的value值,只保留最后一个版本
压缩后的offset可能是不连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息。实际上会拿到offset为7的消息,并且从这个位置进行消费。
这种策略只适合特殊的场景。比如消息的key是用户的ID。value是用的资料。通过这种压缩。整个消息集里就保存了所有用户最新的资料。
高效的读写数据
-
顺序写磁盘
- Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间
-
页缓存+零拷贝
-
pageCache页缓存:kafka重度依赖底层操作系统提供的pageCache功能。当上层有写操作时,操作协同只是将数据写入到pageCache,当操作发生时,先从PageCache中查找,如果找不到,再从磁盘读取。实际上pageCache是把尽可能更多的空闲内存当成磁盘来使用。
log.flush.interval.messages
强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。log.flush.interval.ms
每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理。
-
零拷贝: kafka的数据加工处理操作交由kafka生产者和kafka消费者处理。kafka Broker应用层不关系存储的数据。
-
假设流程:生产者生产数据交给kafka,kafka将数据交由pageCache之后就不管消息的存储了。消费者来读取消息的时候,pageCache将消息交给kafka,kafka在交给系统中的socket Cache。再交由网卡传输给消费者。
- 操作系统将数据从磁盘读入到内核空间的度缓冲区终
- kafka从内核的读缓冲区将数据拷贝到用户空间的缓冲区
- kafka将数据从用户空间的缓冲区再回写到内核空间的socket缓冲区中
- 操作系统将socket缓冲中的数据拷贝到网卡缓冲区,最后通过网络发送给客户端
-
实际kafka的工作机制是:消费者来消费消息的时候,pageCache直接将消息给网卡,然后传输给消费者了。没有经过kafka的应用层。
-
-
-
分区分段+索引:kafka的消息存储是按照分区存储的,分区实际上又是按照segment分段存储的。且还存在多音文件,这样通过offset找到索引文件,根据索引文件中的地址找到具体的消息。读数据采用稀疏索引,可以快速定位要消费的数据。
-
消息批量读写。数据在读写的时候也不是根据单条消息来进行的,都会封装一批一批的数据。且采用批量压缩