稳定性

  • kafka的消息传输机制很直观,如果生产者向broker发送消息,commit之后.会被存到副本里面,他就不会丢失了.

  • 如果在发送之后,网络出现问题,producer无法判断消息是否commit了,但是可以retry多次,直到确认已经在broker那commit.也就是至少一次,at least once

幂等性

概述

所谓幂等性,对接口的多次调用所产生的结果和一次调用的结果是一致的。

生产者在重试的时候可能会重复写入消息,而使用kafka幂等功能可以避免

幂等性条件
  • 只能保证Producer在单个会话内不丢不重,如果Producer出现意外挂掉再重启是无法保证的(幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重)
  • 幂等性不能跨多个topic-partition,只能保证单个partition内的幂等性,当涉及多个topic-partition时,这中间的状态没有同步
代码
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);//开启幂等性,默认为flase
properties.put(ProducerConfig.ACKS_CONFIG, "all");//当enable.idempotence为true,这里默认为all(-1)

事务

概述

事务可以弥补幂等性不能跨partition的缺憾,可以保证对多个分区写入操作的原子性。

操作的原子性是指多个操作要么全部成功,要么全部失败。

实现事务,必须提供唯一transactionalId和开启幂等性(默认开启).

//transactionalId
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactionalId")
//开启幂等性
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true)

事务方法有以下五个:

//初始化事务
initTransactions()
//开启事务
beginTransaction()
//为消费组提供事务内的唯一提交操作
sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata>)
//提交
commitTransaction()
//回滚
abortTransaction()
代码
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);//开启幂等性,默认为flase
properties.put(ProducerConfig.ACKS_CONFIG, "all");//当enable.idempotence为true,这里默认为all
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactionalId")
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//初始化
producer.initTransactions();
//开启事务
producer.beginTransaction();   
//主题,key,value,key可以不写
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "kafka-demo", "111");
ProducerRecord<String, String> record1 = new ProducerRecord<>(topic, "kafka-demo", "222");
ProducerRecord<String, String> record2 = new ProducerRecord<>(topic, "kafka-demo", "333 ");
try {
	producer.send(record);
    System.out.println(1 / 0);
    producer.send(record1);
    producer.send(record2);
	//提交事务
	producer.commitTransaction();
} catch (Exception e) {
	e.printStackTrace();
    //回滚事务
    producer.abortTransaction();
} finally {
    producer.close();
}

结果:用了事务,当业务有异常,这批消息都不会发送成功

控制器

概述

kafka集群中有一个broker会被选举为控制器(kafka controller),负责管理整个集群中所有分区和副本状态。

  • 某个分区leader故障,由控制器选举新leader
  • 某个分区ISR变化时,由控制器负责通知所有broker更新其元数据信息
  • kafka-topics.sh 增加分区时,由控制器负责分区重新分配

控制器选举依赖于zookeeper。

zookeeper的leader选举
  1. zookeeper节点状态:

    • LOOKING:寻找Leader状态,处于该状态需要进入选举流程
    • LEADING:领导者状态,处于该状态的节点说明是角色已经是Leader
    • FOLLOWING:跟随者状态,表示Leader已经选举出来,当前节点角色是follower
    • OBSERVER:观察者状态,表明当前节点角色是observer
  2. 事务ID ZXID

    ZooKeeper状态的每次变化都接收一个ZXID(ZooKeeper事务id)形式的标记。ZXID是一个64位的数字,由Leader统一分配,全局唯一,不断递增。
    ZXID展示了所有的ZooKeeper的变更顺序。每次变更会有一个唯一的zxid,如果zxid1小于zxid2说明zxid1在zxid2之前发生

  3. Zookeeper集群初始化启动时Leader选举

    在集群初始化阶段,当有一台服务器ZK1启动时,其单独无法进行和完成Leader选举,当第二台服务器ZK2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程开始,过程如下:

    1. 每个Server发出一个投票。由于是初始情况,ZK1和ZK2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时ZK1的投票为(1, 0),ZK2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

    2. 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。

    3. 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行比较,规则如下

      先看zxid,大的为leader,相同的话看myid,大的为leader

      对于ZK1而言,它的投票是(1, 0),接收ZK2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时ZK2的myid最大,于是ZK2胜。ZK1更新自己的投票为(2, 0),并将投票重新发送给ZK2。

    4. 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于ZK1、ZK2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出ZK2作为Leader。

    5. 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。当新的Zookeeper节点ZK3启动时,发现已经有Leader了,不再选举,直接将直接的状态从LOOKING改为FOLLOWING

  4. Zookeeper集群运行期间Leader重新选

    在Zookeeper运行期间,如果Leader节点挂了,那么整个Zookeeper集群将暂停对外服务,进入新一轮Leader选举。
    假设正在运行的有ZK1、ZK2、ZK3三台服务器,当前Leader是ZK2,若某一时刻Leader挂了,此时便开始Leader选举。选举过程如下图所示。

    1. 变更状态。Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
    2. 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定ZK1的ZXID为124,ZK3的ZXID为123;在第一轮投票中,ZK1和ZK3都会投自己,产生投票(1, 124),(3, 123),然后各自将投票发送给集群中所有机器。
    3. 接收来自各个服务器的投票。与启动时过程相同。
    4. 处理投票。与启动时过程相同,由于ZK1事务ID大,ZK1将会成为Leader。
    5. 统计投票。与启动时过程相同。
    6. 改变服务器的状态。与启动时过程相同。
Zoolnspector管理

下载Zoolnspector,解压,命令:java -jar jar包

  • controller节点

  • brokerid:为控制器的brokerid
  • timestrap:竞选为控制器时的时间戳

任意时刻,集群中只有一个控制器。每个broker启动是,会读取controller上的brokerid,如果!=-1,表示已经有控制器,当前broker就放弃竞选。

如果不存在controller节点或节点数据异常,就会去创建controller节点,当前broker去创建controller时节点,其他broker也可能去创建controller节点,创建成功的会称为控制器,失败的表示竞选失败。每个broker会在内存中保存控制器的brokerid,标识为activeControllerId。

  • controller_epoch节点

    ​ 持久节点。记录控制器变更的次数

控制器职责

监听broker,topic,partition相关变化以及从zookeeper中读取当前所有与topic,partition和broker有关信息并进行管理

可靠性保证

概述
  1. 可靠性保证:确保系统在各种不同环境下能够发生一致的行为

  2. kafka的保证:

    • 保证分区消息的顺序

      同一生产者往同一个分区写入消息,可以保证后写入的消息偏移量比先写入的消息偏移量大,消费者会按顺序读取消息

    • 只有当消息被写入分区的所有同步副本时,才被认为是已提交

      可以通过生产这acks参数设置需要多少副本接收到数据才算写入消息成功

    • 只要有一个副本是活跃的,已提交的消息就不会丢失

    • 消费者只能读取已提交的消息

失效副本

replica.lag.time.max.ms,默认1000

当ISR中一个follower滞后leader时间超过此值,该follower就会被踢出ISR

原理:当follower副本将leader副本的LEO(Log End Offset,每个分区最后一条消息的位置)之前的日志全部同步时,则认为该follower副本已经追赶上leader副本,此时更新该副本的lastCaughtUpTimeMs标识。

Kafka的副本管理器(ReplicaManager)启动时会启动一个副本过期检测的定时任务,而这个定时任务会定时检查当前时间与副本的lastCaughtUpTimeMs差值是否大于参数replica.lag.time.max.ms指定的值。

注意:不是只要follower拉取leader数据就会更新lastCaughtUpTimeMs(leader消息流入速度远大于follower同步速度,此时也要从ISR中踢出)

副本复制

producer只往leader上写数据,follower只按顺序从leader上同步数据。

follower不同步leader:

  1. 慢副本。一定周期内follower不能追赶上leader,IO平静
  2. 卡住副本。一定周期内follower停止从leader上同步数据,GC暂停或follwer失效,死亡
  3. 新启动副本。主题增加副本因子,新的follower不在ISR中, 直到完全追赶上了leader

可以通过 replica.lag.time.max.ms设置改变followe复制leader数据的频率

一致性保证

相关概念
  • LEO:即日志末端位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。注意是下一条消息!也就是说,如果LEO=10,那么表示该副本保存了10条消息,位移值范围是[0, 9]。另外,leader LEO和follower LEO的更新是有区别的。我们后面会详细说
  • HW:即上面提到的水位值。对于同一个副本对象而言,其HW值不会大于LEO值。小于等于HW值的所有消息都被认为是“已备份”的(replicated)。同理,leader副本和follower副本的HW更新是有区别的,我们后面详谈。

leader宕机后,从ISR中选举新leader,新leader会知道HW(高水位)之前的数据,切换了leader,消费者可以继续看到HW之前的数据。

HW截断机制:选举了新leader,而新leader并不能保证已经完全同步了之前的leader的所有数据,只能保证HW之前的数据是同步的,此时所有的follower都要将数据截断到HW位置,再和新的leader同步数据,来保证数据一致。当宕机的leader恢复后,发现新的leader中的数据和自己的数据不一致,此时宕机的leader会将自己的数据截断到宕机之前的HW位置,然后再从新的leader同步数据。宕机的leader活过来之后也会像其他follower一样同步数据,来保证数据一致性。

数据丢失

说明: A和B的log中已经写入了消息0,1,A的HW已经更新为1,而B的HW尚未更新到1,还是0,此时B重启后,会保留HW=0之前的消息,消息1就会被删除,然后A宕机,B称为新leader, A回来作为follower,发现B(HW=0)的和自己的HW(HW=1)不一样,就会将数据截断到HW=的位置,消息1就会被删除,消息1在两个broker都被删除就丢失了

数据不一致

说明:A的log中写入了0,1,HW更新到了1,B写入了0,HW更新到了0,此时A和B都宕机,B先重启了,成为了新leader,HW还是1,然后producer发送了2给B,此时A还没重启,B的HW就直接更新到1,然后A重启了,成为follower,自己的HW=1和B的HW=1一致,就不会做任何调整。但显然A和B的offset=1的消息是不一样的。

leader epoch
概述

Kafka 0.11引入了leader epoch来取代HW值,一对值:<epoch,offet>,保存在内存中,文件/opt/kafka/kafka-01/logs/jpy-0/leader-epoch-checkpoint

  • epoch:leader版本号,从0开始,leader变更一次+1
  • offset:该版本下,leader写入第一条消息的offset
<0,0>
<0,120>

第一个leader版本号为0从offset=0写入消息0-119,第二个leader版本号为1从120开始

避免数据丢失

避免数据不一致

消息重复

生产端重复

生产发送的消息没有收到正确的broke响应,导致producer重试。

解决方案
  • 开启kfaka幂等性,acks="all"或-1,retries>1

    properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
    properties.put(ProducerConfig.ACKS_CONFIG, "all");
    props.put(ProducerConfig.RETRIES_CONFIG,3);//重试次数
    
  • acks=0,不重试,这样可能丢失数据

消费端重复

数据消费完没有及时提交offset到broker

解决方案
  • 手动提交

  • 下游做幂等

    一般的解决方案是让下游做幂等或者尽量每消费一条消息都记录offset,对于少数严格的场景可能需要把offset或唯一ID,例如订单ID和下游状态更新放在同一个数据库里面做事务来保证精确的一次更新或者在下游数据表里面同时记录消费offset,然后更新下游数据的时候用消费位点做乐观锁拒绝掉旧位点的数据更新。

_consumer_offsets

位移主题,内部topic,用来保存消费组元数据以及对应提交的offset信息。自动创建时,默认50个分区,3个副本

何时创建

一般情况下,当集群中第一有消费者消费消息时,会自动创建,分区数通过server.properties中offsets.topic.num.partitions配置,分区数通过offsets.topic.replication.factor配置

posted @ 2021-02-26 15:33  jpy  阅读(324)  评论(0编辑  收藏  举报