深入客户端

Posted by CHuiL on July 26, 2021

分区分配策略

RangeAssignor分配策略

默认分配策略,关注的是一个消费者组对应一个主题 该策略会保证分区尽可能均匀的分配给所有的消费者。会按照消费者的名称字典序排序,然后平均分配,如果不够,会给靠前的消费者多分配一个分区。

    n = 分区数/消费者数量
    m = 分区数%消费者数量
    前m个消费者会分配n+1个分区,后面的消费者会分配n个分区

注意,这里的分区数是指某一个主题的分区数,并不说该消费者所有订阅的主题的总分区数,也就是他是按照各个主题的分区来进行分配的。 如果说由消费者 c0,c1 和主题 t1,t2 并且每个主题有3个分区,对应为t0p0,t0p1,t0p2 t1p0,t1p1,t1p2 那么会有以下分配

    c0 : t0p0,t0p1,t1p0,t1p1
    c1 : t0p2,t1p2

RoundRobinAssignor

关注的是一个消费者组内的分配,可以对应多个主题 将消费者组内所有消费者以及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询的方式逐个将分区依次分配给每个消费者。

假设组内有3个消费者c0,c1,c2; 有3个主题,t0,t2,t3 分别拥有1,2,3个分区,c0订阅t0,c1订阅t0,t1 ,c2订阅t0,t1,t2
最终分配结果

    c0: t0p0
    c1: t1p0
    c2: t1p1,t2p0,t2p1,t2p2

这样的分配并不是很优。

stickyAssignor

同样关注一个消费组内容对于多个主题的情况,不过这种策略比前面两种稍微复杂一些,他要满足以下两个目的

  • 分区的分配要尽可能均匀
  • 分区的分配尽可能与上次分配的保持相同

这里第二个目的,表现在进行分区重分配的时候,原有的分区分配给了哪个消费者,那么会尽量保持原来的关系,不会变动,一旦变动,可能回到导致原先在消费者处理一般的消息在被分配到另外一个消费者后会重新消费一次,浪费了资源。

以前面的例子 假设组内有3个消费者c0,c1,c2; 有3个主题,t0,t2,t3 分别拥有1,2,3个分区,c0订阅t0,c1订阅t0,t1 ,c2订阅t0,t1,t2
分配后为

    c0:t0p0
    c1:t1p0,t1p1
    c2:t2p0,t2p1,t2p2

如果c0脱离消费组,重新分配后为

    c1:t0p0,t1p0
    c2:t1p0,t2p0,t2p1,t2p2

事务

消息传输保障

有三个层级

  • at most once:至多一次。消息可能丢失,但是绝对不会重复
  • at least once:最少一次。消息绝不会丢失,但可能会重复传输
  • exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次

对于kafka来说,生产者发送成功的消息,会被提交到日志文件,并且由于有多副本的存在的,所以消息不会丢失。如果生产者网络问题导致重试来确保消息已经写入成功,那么挤会导致消息重复。这里的保证就是at least once;

对消费者而言,他的保障主要取决于何时提交了消费位移。如果选择在接受到消息之后,先处理完消息在提交位移,那么在处理的过程中宕机了,重启后会重复消费,此时的保障就位at least once,如果在接受到消息之后里面提交,在消息处理过程中宕机,重启后不会重复消费,也就是消息丢失了,此时就为at most once

幂等

kafka生产者保证消息幂等,即保证消息不会重复写入,可以通过生产者客户端参数enable.idempotence设置为true来开启。他的相关原理,是为每一个生产者实例生成一个producerId(PID),在往一个分区发送消息时,会为每对<PID,分区>生成一个递增序列号(SN_new),由生产者发送给broker,broker在接收到这个序列号后,会在内存中保存下来(SN_old),然后对于生产者发送过来的消息,都会根据<PID,分区>获取到序列号然后进行判断

  • 如果SN_new < SN_old + 1 ;即发送来的消息序列号滞后,说明重复写入,要丢弃。
  • 如果SN_new > SN_old + 1 ;即发送来的消息序列号超前,说明有数据尚未写入,出现了乱序,会抛出异常。这是一个严重的异常。
  • 如果SN_new = SN_old + 1 ;说明序号正常。写入消息,更新SN_old

可以看出,这个幂等,保证是单个生产者会话中单分区的幂等。也就是说,如果同一个消息,第一次被发给了分区0,第二次被发给了分区1,那么该消息任然可能重复写入;并且保证的消息内容的幂等。

kafka事务

Kafka中的事务特性主要用于以下两种场景:

  • 生产者发送多条消息可以封装在一个事务中,形成一个原子操作。多条消息要么发送成功,要么都发送失败
  • read-process-write模式:将消息消费和生产封装在一个事务中,形成一个原子操作。在一个流式处理的应用中,常常一个服务需要从上游接收消息,然后经过处理后送达到下游,这就对应着消息的消费和生成。

transactionalId需要用户显示设置,而PID是由kafka内部分配,开启事务就需要开启幂等。并且transactionalId和PID一一对应。

  • 跨分区原子写:Kafka的事务特性就是要确保跨分区的多个写操作的原子性。
  • 拒绝僵尸实例(Zombie fencing):Kafka事务特性通过transaction-id属性来解决僵尸实例问题。所有具有相同transaction-id的Producer都会被分配相同的pid,同时每一个Producer还会被分配一个递增的epoch。Kafka收到事务提交请求时,如果检查当前事务提交者的epoch不是最新的,那么就会拒绝该Producer的请求。从而达成拒绝僵尸实例的目标。保证了跨分区的幂等性。