kafka不基本慨念(一)

146 阅读14分钟

「这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战

前言

  • 关于作者:励志不秃头的一个CURD的Java农民工
  • 关于文章:上次说了写kafka的基本概念,在文章的最后说了未待续,所以这次就来点不基本的概念吧,相信看完对kafka的认识会更上一个阶段的。

kafka Broker

核心总控制器Controller

在Kafka集群中会有一个或者多个broker,其中有一个broker会被选举为控制器(Kafka Controller),它负责管理整个集群中所有分区和副本的状态。

  • 当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
  • 当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
  • 当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。

Controller选举机制

  1. 在kafka集群启动时,每个broker都会尝试在zookeeper创建一个/controller节点,zookeeper会保证有且仅有一个broker能创建成功,这个broker就会成为整个集群的总控制器,集群其他broker会一直监听这个临时节点
  2. 在controller角色的broker宕机了,此时zookeeper临时节点会消失,其余broker会发现临时节点消失,就会竞争再次创建临时节点,zookeeper又会保证有且仅有一个broker能创建成功,成为新的Controller

Controller的职责

  1. 监听broker相关的变化
    • 为Zookeeper中的/brokers/ids/节点添加BrokerChangeListener,用来处理broker增减的变化。
  2. 监听topic相关的变化
    • 为Zookeeper中的/brokers/topics节点添加TopicChangeListener,用来处理topic增减的变化
    • 为Zookeeper中的/admin/delete_topics节点添加TopicDeletionListener,用来处理删除topic的动作
  3. 从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理。
    • 对于所有topic所对应的Zookeeper中的/brokers/topics/[topic]节点添加PartitionModificationsListener,用来监听topic中的分区分配变化
  4. 更新集群的元数据信息(Metadata),同步到其他普通的broker节点中。

消息分发

发布机制

采用push模式发布到broker,每条消息都被append到patition中,属于顺序写磁盘,保证了kafka吞吐率

消息路由分发

  1. 指定了patition,直接使用
  2. 指定了key,根据key的hashcode对patition的数量取余,例如:有8个patition,key是1;那么"1".hashCode() % 8=1
  3. 自定义消息路由
  4. 如果没有指定patition和key,默认时随机分片
key:ProducerConfig.PARTITIONER_CLASS_CONFIG
value:自定义实现的消息路由路径
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.xx.SamplePartition");


public class SamplePartition implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        /*
            key-1
            key-2
            key-3
         */
        String keyStr = key + "";
        String keyInt = keyStr.substring(4);
        System.out.println("keyStr : "+keyStr + "keyInt : "+keyInt);

        int i = Integer.parseInt(keyInt);

        return i%2;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

写入流程

  1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
  2. producer 将消息发送给该 leader
  3. leader 将消息写入本地 log
  4. followers 从 leader pull 消息,写入本地 log 后 向leader 发送 ACK
  5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK

Topic消息副本机制

kafka为了提高partion的可靠性而设计出来的,避免当其中一个partition不可用的时候,这部分数据就无法消费,因此kafka提供了副本的概念,通过副本机制来实现冗余备份

每个分区可以有多个副本,在副本集合中会存在一个leader的副本,所有读写请求都是由leader副本来处理,其余的副本都作为follower副本,follower副本会从leader副本同步消息日志,具有一定的消息延时,一般情况下,同一个分区的多个副本会被均匀分配到集群中不同的broker上,当leader不可用后,可以重新选举新的leader副本对外提供服务,副本机制提高了kafka集群的高可用topic一般建议设置副本数为>=2

ISR副本

包含了leader副本和所有与leader副本保持同步的follower副本,是整个副本集合的子集
ISR数据保存在Zookeeper的 /brokers/topics//partitions//state节点中

ISR集合中的副本必须满足两个条件:

  1. 副本所在节点必须维持着与zookeeper的连接
  2. 副本最后一条消息的offset与leader副本的最后一条消息的offset之间的差值不能超过指定的阈值(replica.lag.time.max.ms) ;
    • replica.lag.time.max.ms:如果该follower在此时间间隔内一直没有追上过leader的所有消息,则该follower就会被剔除isr列表

follower副本把leader副本LEO之前的日志全部同步完成时,则认为follower副本已经追赶上了leader副本,这个时候会更新这个副本的lastCaughtUpTimeMs标识,kafk副本管理器会启动一个副本过期检查的定时任务,这个任务会定期检查当前时间与副本的lastCaughtUpTimeMs的差值是否大于参数 replica.lag.time.max.ms 的值,如果大于,则会把这个副本踢出ISR集合

副本的leader选举

controller感知到分区leader所在的broker挂了(controller监听了很多zk节点可以感知到broker存活)

controller会从ISR列表(参数unclean.leader.election.enable=false的前提下)里挑第一个broker作为leader(第一个broker最先放进ISR列表,可能是同步数据最多的副本)——一致性

如果参数unclean.leader.election.enabletrue,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选leader,这种设置,可以提高可用性,但是选出的新leader有可能数据少很多。

HW与LEO详解

HW俗称高水位,HighWatermark的缩写,取一个partition对应的ISR中最小的LEO(log-end-offset)作为HW,consumer最多只能消费到HW所在的位置。

每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。对于来自内部broker的读取请求,没有HW的限制。

总结:

  • Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。
    • 事实上,同步复制要求所有能工作的follower都复制完,这条消息才会被commit,这种复制方式极大的影响了吞吐率。
    • 而异步复制方式下,follower异步的从leader复制数据,数据只要被leader写入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机,则会丢失数据。而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率。
  • 需要结合消息发送端对发出消息持久化机制参数acks的设置
  • 随着follower副本不断和leader副本进行数据同步,follower副本的LEO会慢慢后移并且追赶到leader副本,这个追赶上的判断标准是当前副本的LEO是否大于或者等于leader副本的HW,这个追赶上也会使得被踢出的follower副本重新加入到ISR集合中。

副本数据同步原理

初始状态
  1. 初始状态下,leader和follower的HW和LEO都是0,leader副本会保存remote LEO,表示所有followerLEO,也会被初始化为0,这个时候,producer没有发送消息
  2. follower会不断地个leader发送FETCH请求,但是因为没有数据,这个请求会被leader寄存,当在指定的时间之后会强制完成请求,这个时间配置是(replica.fetch.wait.max.ms),如果在指定时间内producer有消息发送过来,那么kafka会唤醒fetch请求,让leader继续处理
第一种情况——leader处理完producer请求之后,follower发送一个fetch请求过来
  1. 生产者发送了一条消息,leader处理完producer请求后,leader副本收到请求后:

    1. 把消息追加到log文件,同时更新leader副本的LEO,LEO = 1
    2. 尝试更新leader HW值。这个时候由于follower副本还没有发送fetch请求,那么leader的remoteLEO仍然是0。leader会比较自己的LEO以及remote LEO的值发现最小值是0,与HW的值相同,所以不会更新HW,remote LEO = 0,HW=0
  2. follower发送一个fetch请求过来,leader处理逻辑:

    1. 读取log数据、更新remote LEO=0(follower还没有写入这条消息,这个值是根据follower的fetch请求中的offset来确定的)
    2.  尝试更新HW,因为这个时候LEO和remoteLEO还是不一致,所以仍然是HW=0
    3. 把消息内容和当前分区的HW值发送给follower副本,HW=0
  3. follower副本收到response以后

    1. 将消息写入到本地log,同时更新follower的LEO,LEO = 1
    2. 更新follower HW,本地的LEO和leader返回的HW进行比较取小的值,所以仍然是0,HW=0

扩展:

  • local LEO更新:本地LEO值,是依赖于实际消息的消息写入来更新的,follower发送FETCH请求并得到leader的数据响应时,每当一条消息写入底层日志成功那么local LEO就+1。
  • remote LEO更新:上面看到了follower local LEO值更新是发生在FETCH请求成功响应且消息成功写入时,而remote LEO 也就是leader上存储的follower LEO,是在这个环节之前,在收到请求之后拉取对应的消息log,响应之前来更新remote LEO的值的。

第一次交互结束以后,HW仍然还是0,这个值会在下一次follower发起fetch请求时被更新

  1. follower发第二次fetch请求,leader收到请求以后:
    1. 读取log数据
    2. 更新remote LEO=1, 因为这次fetch携带的offset是1,remote LEO=1
    3. 更新当前分区的HW,这个时候leader LEO和remote LEO都是1,所以HW的值也更新为1,HW=1
    4. 把数据和当前分区的HW值返回给follower副本,这个时候如果没有数据,则返回为空
  2. follower副本收到response以后
    1. 如果有数据则写本地日志,并且更新
    2. 更新follower的HW值,HW=1

目前为止,数据的同步就完成了,意味着消费端能够消费offset=1这条消息。

第二种情况

由于leader副本暂时没有数据过来,所以follower的fetch会被阻塞,直到等待超时或者leader接收到新的数据。当leader收到请求以后会唤醒处于阻塞的fetch请求。处理过程基本上和前面说的一致

  1. leader将消息写入本地日志,更新Leader的LEO
  2. 唤醒follower的fetch请求
  3. 更新HW(上方两阶段)

数据丢失

min.insync.replicas,这个参数表示ISR集合中的最少副本数,默认值是1,并只有在acks=all或-1时才有效

  • acks与min.insync.replicas搭配使用,才能为消息提供最高的持久性保证。
  • 因为leader副本默认就包含在ISR中,如果ISR中只有1个副本,acks=all也就相当于acks=1了,引入min.insync.replicas的目的就是为了保证下限:不能只满足于ISR全部写入,还要保证ISR中的写入个数不少于min.insync.replicas。
  • 常见的场景是创建一个三副本(即replication.factor=3)的topic,最少同步副本数设为2(即min.insync.replicas=2),acks设为all,以保证最高的消息持久性。
丢失原因

当我们设置min.insync.replicas = 1 时,一旦消息被写入leader端log即被认为是“已提交”,而延迟一轮FETCH RPC更新HW值的设计使得follower HW值是异步延迟更新的,倘若在这个过程中leader发生变更,那么成为新leader的follower的HW值就有可能是过期的,使得clients端认为是成功提交的消息被删除。 数据丢失场景

  1. 开始时,副本 A 和副本 B 都处于正常状态,A 是 Leader 副本。某个使用了默认 acks 设置的生产者程序向 A 发送了两条消息,A 全部写入成功,此时 Kafka 会通知生产者说两条消息全部发送成功。
  2. Leader 副本的高水位也已经更新了,但 Follower 副本高水位还未更新倘若此时副本 B 所在的 Broker 宕机,当它重启回来后,副本 B 会执行日志截断操作,将 LEO 值调整为之前的高水位值,也就是 1。这就是说,位移值为 1 的那条消息被副本 B 从磁盘中删除,此时副本 B 的底层磁盘文件中只保存有 1 条消息,即位移值为 0 的那条消息。
  3. 当执行完截断操作后,副本 B 开始从 A 拉取消息,执行正常的消息同步。如果就在这个节骨眼上,副本 A 所在的 Broker 宕机了,那么 Kafka 就别无选择,只能让副本 B 成为新的 Leader
  4. 当 A 回来后,需要执行相同的日志截断操作,即将高水位调整为与 B 相同的值,也就是 1。这样操作之后,位移值为 1 的那条消息就从这两个副本中被永远地抹掉了。这就是上图展示的数据丢失场景。
解决方案

在kafka0.11.0.0版本之后,引入了一个leader epoch来解决这个问题

leader epoch:由 epoch+offset 组成

  • epoch代表leader的版本号,从0开始递增,当leader发生过变更,epoch就+1
  • offset则是对应这个epoch版本的leader写入第一条消息的offset

eg:(0,0), (1,50) ,表示第一个leader从offset=0开始写消息,一共写了50条。第二个leader版本号是1,从offset=50开始写

这个信息会持久化在对应的分区的本地磁盘上,文件名是 /tmp/kafka-log/topic/leader-epoch-checkpoint 。
leader broker中会保存这样一个缓存,并且定期写入到checkpoint文件中

当leader写log时它会尝试更新整个缓存: 如果这个leader首次写消息,则会在缓存中增加一个条目;否则就不做更新。而每次副本重新成为leader时会查询这部分缓存,获取出对应leader版本的offset 情况分析 follower宕机并且恢复之后,有两种情况

  1. 如果这个时候leader副本没有挂,也就是意味着没有发生leader选举,那么follower恢复之后并不会去截断自己的日志,而是先发送一个OffsetsForLeaderEpochRequest请求给到leader副本,leader副本收到请求之后返回当前的LEO。

    1. 如果follower副本的leaderEpoch和leader副本的epoch相同, leader的leo只可能大于或者等于follower副本的leo值,所以这个时候不会发生截断
    2. 如果follower副本和leader副本的epoch值不同,发生截断
  2. 如果leader副本宕机了重新选举新的leader,那么原本的follower副本就会变成leader,意味着epoch从0变成1,使得原本follower副本中LEO的值的到了保留。

举例分析 场景和之前大致是类似的,只不过引用 Leader Epoch 机制后,Follower 副本 B 重启回来后,需要向 A 发送一个特殊的请求去获取 Leader 的 LEO 值。在这个例子中,该值为 2。当获知到 Leader LEO=2 后,B 发现该 LEO 值不比它自己的 LEO 值小,而且缓存中也没有保存任何起始位移值 > 2 的 Epoch 条目,因此 B 无需执行任何日志截断操作。这是对高水位机制的一个明显改进,即副本是否执行日志截断不再依赖于高水位进行判断

现在,副本 A 宕机了,B 成为 Leader。同样地,当 A 重启回来后,执行与 B 相同的逻辑判断,发现也不用执行日志截断,至此位移值为 1 的那条消息在两个副本中均得到保留。后面当生产者程序向 B 写入新消息时,副本 B 所在的 Broker 缓存中,会生成新的 Leader Epoch 条目:[Epoch=1, Offset=2]。之后,副本 B 会使用这个条目帮助判断后续是否执行日志截断操作。

这样,通过 Leader Epoch 机制,Kafka 完美地规避了这种数据丢失场景。

好了,以上就是Kafka的不基础概念,你以为这就结束了,当然不是,下一篇文章继续讲讲kafka的核心概念,这样,。我是新生代农民工L_Denny,我们下篇文章见。未待续。。。。。。