zoukankan      html  css  js  c++  java
  • kafka:(8) 事务、幂等

    一、消息传输保障

      一般而言,消息中间件的消息传输保障有3个层级,分别如下。
    (1) at most once:至多一次。消息可能会丢失,但绝对不会重复传输。
    (2) at least once : 最少一次。消息绝不会丢失,但可能会重复传输。
    (3) exactly once :恰好一次。每条消息肯定会被传输一次且仅传输一次。

      Kafka 的消息传输保障机制非常直观。当生产者向Kafka发送消息时,一旦消息被成功提交到日志文件,由于多副本机制的存在,这条消息就不会丢失。如果生产者发送消息到Kafka之后,遇到了网络问题而造成通信中断,那么生产者就无法判断该消息是否己经提交。虽然Kafka无法确定网络故障期间发生了什么,但生产者可以进行多次重试来确保消息已经写入Kafka,这个重试的过程中有可能会造成消息的重复写入,所以这里Kafka提供的消息传输保障为at least once 。

      对消费者而言,消费者处理消息和提交消费位移的顺序在很大程度上决定了消费者提供哪一种消息传输保障。如果消费者在拉取完消息之后,应用逻辑先处理消息后提交消费位移,那么在消息处理之后且在位移提交之前消费者宕机了,待它重新上线之后,会从上一次位移提交的位置拉取,这样就出现了重复消费,因为有部分消息已经处理过了只是还没来得及提交消费位移,此时就对应at least once。如果消费者在拉完消息之后,应用逻辑先提交消费位移后进行消息处理,那么在位移提交之后且在消息处理完成之前消费者岩机了,待它重新上线之后,会从己经提交的位移处开始重新消费,但之前尚有部分消息未进行消费,如此就会发生消息丢失,此时就对应at most once 。

    二、幂等

      所谓的幕等,简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能之后就可以避免这种情况。
      开启幕等性功能的方式很简单,只需要显式地将生产者客户端参数enab le.idempotence设置为true即可(默认值为false )

    properties.put(ProducerConfig.ENABLE_IDEMPOTENCECONFIG, true);
    #或者
    properties.put (“enable.idempotence”, true);

      不过如果要确保幂等性功能正常,还需要确保生产者客户端的 retries、acks、max.in.flight.requests.per.connection 这几个参数不被配置错。实际上在使用幂等性功能的时候,用户完全可以不用配置(也不建议配置)这几个参数。

    • 如果用户显式地指定了 retries 参数,那么这个参数的值必须大于0,否则会报出 ConfigException。如果用户没有显式地指定 retries 参数,那么 KafkaProducer 会将它置为 Integer.MAX_VALUE。
    • 同时还需要保证 max.in.flight.requests.per.connection 参数的值不能大于5(这个参数的值默认为5),否则也会报出 ConfigException。
    • 如果用户还显式地指定了 acks 参数,那么还需要保证这个参数的值为 -1(all),如果不为 -1(这个参数的值默认为1),那么也会报出 ConfigException。

      为了实现生产者的幂等性,Kafka 为此引入了 producer id(以下简称 PID)和序列号(sequence number)这两个概念。每个新的生产者实例在初始化的时候都会被分配一个 PID,这个 PID 对用户而言是完全透明的。对于每个 PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将 <PID,分区> 对应的序列号的值加1。broker 端会在内存中为每一对 <PID,分区> 维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_new)比 broker 端中维护的对应的序列号的值(SN_old)大1(即 SN_new = SN_old + 1)时,broker 才会接收它。如果 SN_new< SN_old + 1,那么说明消息被重复写入,broker 可以直接将其丢弃。如果 SN_new> SN_old + 1,那么说明中间有数据尚未写入,出现了乱序,暗示可能有消息丢失,对应的生产者会抛出 OutOfOrderSequenceException,这个异常是一个严重的异常,后续的诸如 send()、beginTransaction()、commitTransaction() 等方法的调用都会抛出 IllegalStateException 的异常。引入序列号来实现幂等也只是针对每一对 <PID,分区> 而言的,也就是说,Kafka 的幂等只能保证单个生产者会话(session)中单分区的幂等

    三、事务

      幂等性并不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。

      Kafka中的事务可以使应用程序将消费消息、生产消息、提交消费位移当作原子操作来处理,同时成功或失败,即使该生产或消费会跨多个分区。

      为了实现事务,应用程序必须提供唯一的 transactionalId,这个 transactionalId 通过客户端参数 transactional.id 来显式设置

    properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactionId");
    # 或者
    properties.put("transactional.id", "transactionId");

      事务要求生产者开启幂等特性,因此通过将 transactional.id 参数设置为非空从而开启事务特性的同时需要将 enable.idempotence 设置为 true(如果未显式设置,则 KafkaProducer 默认会将它的值设置为 true),如果用户显式地将 enable.idempotence 设置为 false,则会报出 ConfigException。

      transactionalId 与 PID 一一对应,两者之间所不同的是 transactionalId 由用户显式设置,而 PID 是由 Kafka 内部分配的。另外,为了保证新的生产者启动后具有相同 transactionalId 的旧生产者能够立即失效,每个生产者通过 transactionalId 获取 PID 的同时,还会获取一个单调递增的 producer epoch。如果使用同一个 transactionalId 开启两个生产者,那么前一个开启的生产者会报错。

      从消费者的角度分析,事务能保证的语义相对偏弱。出于以下原因,Kafka 并不能保证已提交的事务中的所有消息都能够被消费:

    • 对采用日志压缩策略的主题而言,事务中的某些消息有可能被清理(相同key的消息,后写入的消息会覆盖前面写入的消息)。
    • 事务中消息可能分布在同一个分区的多个日志分段(LogSegment)中,当老的日志分段被删除时,对应的消息可能会丢失。
    • 消费者可以通过 seek() 方法访问任意 offset 的消息,从而可能遗漏事务中的部分消息。
    • 消费者在消费时可能没有分配到事务内的所有分区,如此它也就不能读取事务中的所有消息。

      KafkaProducer 提供了5个与事务相关的方法:

    void initTransactions();
    void beginTransaction() throws ProducerFencedException;
    void sendOffsetsToTransaction(Map offsets, String consumerGroupId) throws ProducerFencedException;
    void commitTransaction() throws ProducerFencedException;
    void abortTransaction() throws ProducerFencedException;

      initTransactions() 方法用来初始化事务,这个方法能够执行的前提是配置了 transactionalId,如果没有则会报出 IllegalStateException;beginTransaction() 方法用来开启事务;sendOffsetsToTransaction() 方法为消费者提供在事务内的位移提交的操作;commitTransaction() 方法用来提交事务;abortTransaction() 方法用来中止事务,类似于事务回滚。

      在消费端有一个参数 isolation.level,与事务有着莫大的关联,这个参数的默认值为“read_uncommitted”,意思是说消费端应用可以看到(消费到)未提交的事务,当然对于已提交的事务也是可见的。这个参数还可以设置为“read_committed”,表示消费端应用不可以看到尚未提交的事务内的消息。举个例子,如果生产者开启事务并向某个分区值发送3条消息 msg1、msg2 和 msg3,在执行 commitTransaction() 或 abortTransaction() 方法前,设置为“read_committed”的消费端应用是消费不到这些消息的,不过在 KafkaConsumer 内部会缓存这些消息,直到生产者执行 commitTransaction() 方法之后它才能将这些消息推送给消费端应用。反之,如果生产者执行了 abortTransaction() 方法,那么 KafkaConsumer 会将这些缓存的消息丢弃而不推送给消费端应用。

      日志文件中除了普通的消息,还有一种消息专门用来标志一个事务的结束,它就是控制消息(ControlBatch)。控制消息一共有两种类型:COMMIT 和 ABORT,分别用来表征事务已经成功提交或已经被成功中止。KafkaConsumer 可以通过这个控制消息来判断对应的事务是被提交了还是被中止了,然后结合参数 isolation.level 配置的隔离级别来决定是否将相应的消息返回给消费端应用,如下图所示, ControlBatch 对消费端应用不可见。

      

      为了实现事务的功能,Kafka 还引入了事务协调器(TransactionCoordinator)来负责处理事务,这一点可以类比一下组协调器(GroupCoordinator)。每一个生产者都会被指派一个特定的 TransactionCoordinator,所有的事务逻辑包括分派 PID 等都是由 TransactionCoordinator 来负责实施的。TransactionCoordinator 会将事务状态持久化到内部主题 __transaction_state 中。

      

  • 相关阅读:
    [转]如何分析监控的关键指标
    性能测试指标&说明 [解释的灰常清楚哦!!]
    页面性能测试&提升方式
    转:使用 JMeter 完成常用的压力测试
    细雨学习笔记:JMeter 的主要测试组件总结
    压力测试了解
    影响性能的因素
    网址收藏
    Windows安装TensorFlow
    ionic常见问题及解决方案
  • 原文地址:https://www.cnblogs.com/zjxiang/p/15423504.html
Copyright © 2011-2022 走看看