前言

如果看官对一些基本术语不了解,请先翻阅


正文

在了解ISR之前,我们先了解kafka是如何保证数据可靠性的,以方便我们更好的理解ISR。

ACK应答机制

为了保证数据可靠性,kafka使用了ack机制。

当消息被生产者发送给kafka时,如果数据成功写入partition,需要给生产者发送ack(acknowledge),以此来确保消息被成功写入;如果生产者未收到ack,此消息会被producer重发;

我们知道,Topic对应着多个Partition,而每个Partition都有副本,Partition的Leader负责处理读写消息事件,Follower负责同步Leader的数据,在leader故障时,能从Follower中选出新的Leader来接班;那么问题来了,什么时候发送ack?

 第一种方案:半数follower同步完成发送ack,优点相对于第二种方案来说只需要第二种方案时间的1/2即可完成同步,也就是延迟比全部完成数据同步再发送ack的方案要低一半;缺点也很明显,假设我们需要n个同步完成数据发送ack,那么我们总共需要2n+1个副本(follower一定不会在同一台机器)保证至少有一半以上的副本完成同步,2n+1个副本意味着重复数据多了两倍,我们知道kafka应用于大数据方面,两倍的数据量是无法忍受的,所以kafka选择了第二种方案,牺牲同步时间以换取空间;

 第二种方案:全部的follower都同步完成再发送ack,这种方案缺点很明显,就是花费同步的时间较多,优点也相对于半数机制来说,如果n台节点故障,我们只需要n+1台机器就能保证一定有一台机器完成副本同步,但我们仍然需要所有的副本同步消息再发送ack;

 现在的问题是,选择了第二种方案,如果有一台机器故障,Leader就要一直等待直到机器故障恢复且同步完成才能回复ack,为此出现了ISR的说法。

--------------------------------------------------------------------------------------------------------------------------------

 什么是ISR?

       ISR全称In-Sync-Replica-Set,就是与Leader保持数据同步的的follower集合,它由Leader动态维护,如果follower超过一定的时间阈值(参数replica.lag.time.max.ms)未与Leader同步数据,此follower将会被踢出ISR中。Leader只需要等待ISR中所有副本都同步完成即可回复producer ack。

--------------------------------------------------------------------------------------------------------------------------------

再谈ACK

副本同步流程如图        

亿级kafka数据同步到hive kafka数据同步机制_幂等性

 ACK有三种机制

1.acks=0,producer不等待ack,也就是数据发给partition就算完了,当机器故障时,存在丢数据的情况,也就是;

    (1)、数据发送给broker时,leader故障,由于不等待ack,没有重发机制,消息丢失。

        

亿级kafka数据同步到hive kafka数据同步机制_幂等性_02

    (2)、数据发送给broker时,leader接收到了数据,follower还没同步数据,leader故障,也会导致消息丢失

        

亿级kafka数据同步到hive kafka数据同步机制_幂等性_03

-------------------------------------------------------------------------------------------------------------------------------

        2.acks=1,producer等待ack,只要Leader写完数据就返回ack,不关心follower是否完成   同步,存在丢失数据情况;

         (1)、数据发送给broker时,leader写完了数据并返回ack,follower还没同步数据,leader故障,导致消息丢失

 3.acks=all,producer等待ISR的ack,ISR中所有的follower都同步完成数据才返回ack,存在重复数据、数据丢失的情况;

 此状态下有极端情况是:ISR中只剩一个Leader,此时acks=all退化到acks=1,将有可能丢失数据。

 (1)、leader写完数据,ISR的follower也将数据同步完成时,leader故障,producer没收到ack,将数据重发,造成数据重复的情况。

--------------------------------------------------------------------------------------------------------------------------------

生产消息的三种语义

  • At most once(至多一次)——消息可能会丢失但绝不重传。
  • At least once(最少一次)——消息可以重传但绝不丢失。
  • Exactly once(精准一次)——这正是人们想要的, 每一条消息只被传递一次.

如何做到Exactly once?

答案是Exactly once = at least once + 幂等性;何为幂等性?如果一次请求的结果与多次请求的结果一致我们就称之为此接口具有幂等性。

--------------------------------------------------------------------------------------------------------------------------------

如何防止消息丢失与消息重复?

 在acks设置为all时,只要不退化到isr中只剩leader的情况(也就是acks=1时),那么就不会丢失数据,同时如果我们想要消息不重复怎么办?kafka为此添加了Producer的enable.idompotence参数(如果设置了此参数就无需再设置acks参数),将其设置为true表示开启幂等性,有了不丢失数据做保障再加上幂等性,就能保证数据不丢失也不重复。

       开启了幂等性之后的producer在连接broker时会被分配一个Producer ID,并且为每一个partition都维护一个顺序号Sequence Number,消息被发送到broker时会将Producer ID,Partition,Sequence Number做一个记录,如果有与之相同的消息再发进来,就会被认为是重复数据,以此来保证写消息的幂等性

       如果Producer故障重启,Producer ID将会被重置,所以Kafka只能保证单分区、单会话的幂等性。如果想做到跨分区、跨会话的幂等性,可以使用事务,kafka在0.11版本引入了事务,它使其一组操作将具有原子性,要么全部成功要么全部失败、不会出现中间态的,且producer需要显示的指定txid,与producer id进行绑定,就算机器故障重启、也能通过txid还原pid,从而实现跨分区、跨会话的Exactly once(笔者对事务还未进行深入学习,请读者参考其他资料)。其实没有绝对的防止消息丢失消息重复哈,我们只是在不稳定态的网络环境中追求程序的高可用性罢了。

--------------------------------------------------------------------------------------------------------------------------------

 Kafka事务如何使用

public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9092,127.0.0.1:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);//开启幂等性设置 
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "p0-txid");//显示设定transaction id
        KafkaProducer producer = new KafkaProducer(properties);
        producer.initTransactions();
        try {
            String msg = "MSG";
            producer.beginTransaction();
            producer.send(new ProducerRecord("Topic-A", msg.toString()));
            producer.send(new ProducerRecord("Topic-B", msg.toString()));
            producer.send(new ProducerRecord("Topic-C", msg.toString()));
            producer.commitTransaction();
        } catch (ProducerFencedException e) {
            e.printStackTrace();
            producer.close();
        } catch (KafkaException e1) {
            e1.printStackTrace();
            producer.abortTransaction();
        }
        producer.close();
    }

下篇:Consumer的三种分区分配策略