消息中间件 - 4.消息中间件Kafka服务端运行机制

kafka消息传输保障

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

kafka客户端的消息传输机制

  • 生产者[最少一次(at least once)]

生产者发送消息后,若消息被成功提交到日志文件,因为多副本机制,那么消息就不会丢失。若消息没有被成功提交到日志文件,如遭遇到了网络波动等问题,生产者无法判断消息是否已经提交成功,此时生产者可以进行重试,这个过程可能造成消息的重复写入,此时生产的消息的传输保障机制为最少一次(at least once)。

  • 消费者[最少一次(at least once)和至多一次(at most once)]

消费者处理消息和提交消费位移的顺序很大程度上决定了消费者提供哪种消息传输保障。

  1. 先处理消息再提交位移: 若消费者拉取完消息后,应用逻辑先处理消息后提交消费位移,那么在消息处理之后且位移提交之前消费者宕机了,等它重新上线后,会从上一次位移提交的位置拉取,造成重复消费,此时对应最少一次(at least once)。
  2. 先提交位移再处理消息: 若消费者拉取完消息后,应用逻辑先提交消费位移后处理消息,那么在位移提交之后且消息处理完成之前消费者宕机了,等它重新上线后,会从已经提交的位移处拉取,造成消息丢失,此时对应最多一次(at most once)。

由此可见,仅从生产者或者消费者端是无法实现消息的精准一次的特性。所以Kafka的Broker端引入了幂等性的相关概念与客户端的消息传输机制相互配合,进而实现消息精准一次传输的特性。

生产者幂等性(单个分区)

所谓的幂等,简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能之后就可以避免这种情况。开启幂等性的配置参数为enable.idempotence=true,默认未开启。当开启幂等性时,需要与生产者客户端相关配置配套使用,如:retries=0acks=-1/allmax.in.flight.requests.per.connection<=5等配置。实际上当开启幂等性后,生产者完全可以不进行配置该参数。

kafka为了实现生产者的幂等性,引入了生产者ID(pid)和序列号(seq)。每个新的生产者实例在初始化的时候都会被分配一个PID,这个PID对用户而言是完全透明的。对于每个PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将<PID,分区>对应的序列号的值加1。
在这里插入图片描述
broker端会在内存中为每一对<PID,分区>维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(seq_new)比broker端中维护的对应的序列号的值(seq_old)大1(seq_new=seq_old+1)时,消息才会被接收。如果seq_new<=seq_old则说明消息重复写入,broker可以直接丢弃。如果seq_new>seq_old+1时,说明有数据尚未写入,存在消息丢失生产者可能会抛出OutOfOrderSequenceException异常。

由于引入序列号来实现幂等也只是针对每一对<PID,分区>而言,因此Kafka的幂等只能保证单个生产者会话(session)中单分区的幂等。

Kafka事务实现机制(跨分区)

幂等性并不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对生产者多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。Kafka的事务实现主要组件包括:

  1. 事务协调器(TransactionCoordinator): 负责生成事务ID、启动事务和事务提交,同时还可以检查和回滚事务。每个Kafka Broker都有自己的事务管理器,这些事务管理器之间可以相互通信,以便协调分布式事务操作。
  2. 事务性生产者(TxProducer): Kafka的事务性生产者允许应用程序在事务范围内发送消息。事务性生产者产生的消息会被添加到一个本地事务中。当事务性生产者提交事务时,它将发送 Prepare Commit 请求到事务管理器,然后等待事务管理器的响应。如果响应成功,事务性生产者会将消息发送到Kafka集群中。如果响应失败,事务性生产者会撤销操作并回滚本地事务。
  3. 事务性消费者(TxComsumer): Kafka的事务性消费者可以读取事务性主题中的消息,并使用事务管理器来协调消费过程。当事务性消费者开始一个事务时,它会读取主题中的消息,并将其添加到事务中。如果事务成功提交,消息将被消费。如果事务回滚,则消息再被消费时自动忽略。
  4. 事务日志(ControlBatch): Kafka使用事务日志来记录事务操作的状态信息。每个事务包含一组 Prepare Commit 记录和一个 Commit 记录。Prepare Commit 记录用于添加事务记录,并在提交事务之前向事务管理器发送 Prepare Commit 请求。Commit 记录用于提交事务,并通知事务管理器事务已经成功完成。
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

transactionalId与PID一一对应,两者之间所不同的是transactionalId由用户显式设置,而PID是由Kafka内部分配的。并且当新的生产者使用已有的transactionalId启动时,会通过事务协调器获取当前该事务的状态,而使用transactionalId的旧生产者则会立刻失效(通过producer epoch实现)。

生产者端

生产者代码相关
	public interface Producer<K, V> extends Closeable {
	    // 初始化事务,包括申请 producer id
	    void initTransactions();
	    // 开始事务,这里会更改事务的本地状态
	    void beginTransaction() throws ProducerFencedException;
	    // 提交消费位置,offsets表示每个分区的消费位置,consumerGroupId表示消费组的名称
	    void sendOffsetsToTransaction(Map<TopicPartition, ffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException;
	    // 发送事务提交请求
	    void commitTransaction() throws ProducerFencedException;
	    // 发送事务回滚请求
	    void abortTransaction() throws ProducerFencedException;
	}
生产者配置相关
  • transactional.id:事务ID,不能为空
  • enable.idempotence:幂等性,必须开启。设置为true。
  • transaction.timeout.ms:事务超时时间,默认为10秒,最长为15分钟。
  • acks:要求此配置项必须设置为all。[开启幂等性后可不用配置]
  • retries:重试次数,必须大于0。[开启幂等性后可不用配置]
  • max.in.flight.requests.per.connection:单个连接并行发送消息的请求数量,必须小于或等于5。[开启幂等性后可不用配置]

消费者端

消费事务级别(isolation.level)
  • read_uncommitted:消费端应用可以看到(消费到)未提交的事务,当然对于已提交的事务也是可见的。
  • read_committed:消费端应用不可以看到尚未提交的事务内的消息

当生产者开启事务时,KafkaConsumer在拉取到未提交事务的消息后会判断当前的事务级别,如果是read_uncommitted读未提交,则将该消息推送给消费者线程进行消费。如果是read_committed读已提交,则将该消息缓存在内部,直到生产者提交事务后才会将该消息推送给消费者线程。当然如果生产者放弃了事务,KafkaConsumer也会将这些缓存的消息丢弃而不推送给消费者线程。

LastStableOffset

在这里插入图片描述

由此可见,Kafka中的事务与我们所理解的事务其实并不是一回事。Kafka中的事务本质上还是为了解决一组消息在不同分区下的幂等性保障问题,从而实现消息传输中精准一次的特性。再次强调消息的精准一次语义是需要开启事务生产者、事务消费者还有Broker端一同协作从而最终实现该特性。

Kafka 事务的实现原理
Kafka事务「原理剖析」

消息的精准一次传输特性

从上述几个点描述来看,要实现精准一次的语义就需要开启 Kafka 事务的功能并且使用合理的配置。此外,个人认为在实际开发的过程中仍然需要有我们自己的机制去完成消息的幂等消费问题,比如消息的ID。

  1. 使用事务:Kafka提供了事务功能,通过将消息的消费和位移提交操作放在同一个事务中,可以实现精确一次消费。消费者可以使用手动位移提交,在处理完消息后再提交位移,确保只有当消息处理成功后才会更新位移。

  2. 消费者组与消费位移:Kafka中的消费者组(Consumer Group)可以协同工作来实现精确一次消费。消费者在处理消息后,提交位移,以确保其他消费者组中的消费者不会重复消费相同的消息。

  3. 幂等性和去重:消费者处理消息的时候实现幂等,即无论消息处理多少次,结果都是一致的。通过在消费者端实现幂等逻辑,可以避免重复处理消息。另外,可以利用唯一标识符或消息ID来进行消息去重,不处理已经处理过的消息。

控制器

在 Kafka 集群中会有一个或多个 broker,其中有一个 broker 会被选举为控制器(Kafka Controller)。在任意时刻,集群中有且仅有一个控制器。它负责管理整个集群中所有分区和副本的状态。当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。当增加分区的数量时,同样还是由控制器负责分区的重新分配。

控制器的选举

kafka控制器发生选举的时机:1.集群启动;2.控制器节点故障;3.控制器节点失联;4.控制器故障恢复。

  1. 每个Broker启动时会在zookeeper中注册自己的信息到/brokers/ids/${broker_id},表明自己是集群中的一员。
  2. broker会抢先在zookeeper中创建/controller一个临时节点。谁先创建谁就成为了Controller。只有/controller中broker的值正常时才允许创建,异常时可以被修改。
  3. 当broker成为了Controller之后,会在zookeeper中的永久节点/controller_epoch修改当前controller的变更版本(自增1)。非Controller节点在收到Controller节点后首先会判断该请求携带的版本是否是最新的,避免旧Controller的无效请求。
  4. 当控制器成功选举,并且已经完成初始化后,它会将自己的元数据信息(包括分区和副本的状态信息等)保存到Zookeeper中,供其他Broker使用,同时还会定期发送心跳消息,以告知其他Broker自己的状态信息。

在这里插入图片描述

需要注意的是,Kafka控制器选举过程中,必须保证Zookeeper的正常运行,否则可能会导致选举失败或者数据不一致等问题。该kafka3.0以上的版本中,kafka已丢弃zk,而是将该部分信息注册到了内部。

集群脑裂问题的处理

当控制器所在的节点因为某些原因(网络不可达、GC等)产生故障不可对外响应达到一定的时间后,集群中的其他节点就会认为该节点可能已经存在故障,从而触发控制器的选举。当新的控制器被选举之后,原故障的节点已经恢复,此时集群中存在两个控制器,一个是新选举出来的控制器,一个是从故障中恢复的控制器,这就是所谓的脑裂。

Kafka是通过使用controller_epoch来处理,controller_epoch只是一个单调递增的数。第一次选择控制器时,controller_epoch值为1。如果再次选择新控制器,controller_epoch为2,依次单调递增。每个新的Controller被当选后都会向zookeeper的永久节点/controller_epoch进行自增运算。当其他Broker知道当前的controller_epoch时,如果他们从Controller收到包含旧(较小)controller_epoch的消息,则它们将被忽略。即Broker根据最大的controller_epoch来区分最新的Controller。

Kafka Controller选举过程

ZK节点数据

在这里插入图片描述

消息的可靠性

虽然Kafka提供了很多特性,但实际上如果对某些配置不熟悉或不能很好的理解,就可能会破坏已有的功能特性。尤其是在可靠性这一方面是需要去认真研究的点。在配置这一方面而言,RocketMQ 确实要比 Kafka 友好太多。

消息可靠性的机制

其实研究Kafka中消息可靠性传输无非就是研究消息传输都有哪些参与者,并且各个参与者之间是如何协调的。消息传输的参与者无非就是:1.生产者发送消息、2.服务器端同步消息并完成持久化、3.消费者完成消息的消费。而相关的协调无非就是依靠:1.ack机制、2.消息的发送或接收方式、3.位移的管理,包括Broker端位移的机制以及消费者端位移的提交方式。

  • 生产者端:通过消息发送方以同步或异步的方式获取发送的消息的响应结果,失败做重试或其他处理策略。
  • 服务端ACK:指定分区中有N个副本收到这条消息,生产者才认为这条消息是写入成功的。
    • acks = 1,默认为1。生产者发送消息,只要 leader 副本成功写入消息,就代表成功。这种方案的问题在于,当返回成功后,如果 leader 副本和 follower 副本还没有来得及同步,leader 就崩溃了,那么在选举后新的 leader 就没有这条消息,也就丢失了。
    • acks = 0。生产者发送消息后直接算写入成功,不需要等待响应。这个方案的问题很明显,只要服务端写消息时出现任何问题,都会导致消息丢失。
    • acks = -1/all。生产者发送消息后,需要等待 ISR 中的所有副本都成功写入消息后才能收到服务端的响应。毫无疑问这种方案的可靠性是最高的,但是如果 ISR 中只有leader 副本,那么就和 acks = 1 毫无差别了。
  • 服务端分区副本可以通过HW位移表示消息是否可以被消费。
  • 消费端手动提交位移:默认情况下,当消费者消费到消息后,就会自动提交位移。但是如果消费者消费出错,没有进入真正的业务处理,那么就可能会导致这条消息消费失败,从而丢失。开启手动提交位移后,等待业务正常处理完成后,再提交offset,从而保证消息已成功处理。

从这个机制上看,如果要保证消息的可靠性就需要保障每个传输环节都是可靠的。但实际上并不是每个环境都能得到保证。比如:

  1. 生产者发送消息到Broker:由服务端的ACK机制和发送端获取响应结果并进行处理来共同保证。当 acks 为 0,只要服务端写消息时出现任何问题,都会导致消息丢失;当 acks 配置为 1 时,生产者发送消息,只要 leader 副本成功写入消息,就代表成功。这种方案的问题在于,当返回成功后,如果 leader 副本和 follower 副本还没有来得及同步,leader 就崩溃了,那么在选举后新的 leader 就没有这条消息,也就丢失了。
  2. Broker 存储数据:broker存储消息首先是先写入到操作系统的Page Cache中。什么时候将缓存的数据写入文件中是由操作系统自行决定。所以如果此时机器突然挂了,而且消息尚未被同步到follower副本中,消息也是会丢失的。
  3. 消费者消费数据:在开启自动提交 offset 时,只要消费者消费到消息,那么就会自动提交偏移量,如果业务还没有来得及处理,那么消息就会丢失。

单个Partition内的消息顺序性

Kafka 可以保证同一个分区中的消息是有序的。如果生产者按照一定的顺序发送消息,那么这些消息也会顺序地写入分区,进而消费者也可以按照同样的顺序消费它们。

对于某些应用来说,顺序性非常重要,比如 MySQL 的 binlog 传输,如果出现错误就会造成非常严重的后果。如果将 acks 参数配置为非零值,并且max.in.flight.requests.per.connection参数配置为大于1的值,那么就会出现错序的现象:如果第一批次消息写入失败,而第二批次消息写入成功,那么生产者会重试发送第一批次的消息,此时如果第一批次的消息写入成功,那么这两个批次的消息就出现了错序。

一般而言,在需要保证消息顺序的场合建议把参数max.in.flight.requests.per.connection配置为1,而不是把acks配置为0,不过这样也会影响整体的吞吐。

所以单个Partition内的消息的顺序确实是可以保证的,但是这个顺序并不是生产者发送消息的顺序,而是Kafka将消息写入到分区内的顺序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值