1 初识Kafka
1.1 Kafka三大角色
消息系统
具备消息系统的解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,Kafka 还提供了大多数消息系统难以实现的消息顺序性保障及回溯消费的功能。
存储系统
Kafka把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效地降低了数据丢失的风险。
流式处理平台
Kafka不仅为每个流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理类库,比如窗口、连接、变换和聚合等各类操作
1.2 基本概念
1.2.1 体系架构
kafka集群包括若干Producer、若干 Broker、若干Consumer,以及一个ZooKeeper集群 ZooKeeper是Kafka用来负责集群元数据的管理、控制器的选举等操作的。Producer将消息发送到Broker,Broker负责将收到的消息存储到磁盘中,而Consumer负责从Broker订阅并消费消息。
1.2.2 主题和分区
消息以主题为单位进行归类,主题是一个逻辑上的概念,它还可以细分为多个分区。分区的划分不仅为Kafka提供了可伸缩性、水平扩展的功能,还通过多副本机制来为Kafka提供数据冗余以提高数据可靠性。
分区在存储层面可以看作一个可追加的日志(Log)文件,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量(offset)。 offset是消息在分区中的唯一标识,Kafka通过它来保证消息在分区内的顺序性,不过offset并不跨越分区,也就是说,Kafka保证的是分区有序而不是主题有序。
1.2.3 多副本(Replica)机制
副本之间是“一主多从”的关系,其中leader副本负责处理读写请求,follower副本只负责与leader副本的消息同步。
AR 分区中的所有副本统称为AR(Assigned Replicas)
ISR 所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成ISR(In-SyncReplicas),ISR集合是AR集合中的一个子集。
OSR 与leader副本同步滞后过多的副本(不包括leader副本)组成OSR(Out-of-Sync Replicas)
AR=ISR+OSR
leader副本负责维护和跟踪ISR集合中所有follower副本的滞后状态,当follower副本落后太多或失效时,leader副本会把它从ISR集合中剔除。如果OSR集合中有follower副本“追上”了leader副本,那么leader副本会把它从OSR集合转移至ISR集合
1.2.4 高水位HW
HW 是High Watermark的缩写,俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个offset之前的消息
LEO 是Log End Offset的缩写,它标识当前日志文件中下一条待写入消息的offset。
分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,对消费者而言只能消费HW之前的消息
1.3 文件目录布局
不考虑多副本的情况,一个分区对应一个日志(Log)。为了防止 Log 过大,Kafka又引入了日志分段(LogSegment)的概念,将Log切分为多个LogSegment,相当于一个巨型文件被平均分配为多个相对较小的文件,这样也便于消息的维护和清理
Log 和LogSegment 也不是纯粹物理意义上的概念,Log 在物理上只以文件夹的形式存储,而每个LogSegment 对应于磁盘上的一个日志文件和两个索引文件,以及可能的其他文件(比如以“.txnindex”为后缀的事务索引文件)
1.3.1 日志索引
每个日志分段文件对应了两个索引文件,主要用来提高查找消息的效率
偏移量索引 偏移量索引项的格式如图5-8所示。每个索引项占用8个字节,分为两个部分
(1)relativeOffset:相对偏移量,表示消息相对于baseOffset 的偏移量,占用4 个字节,当前索引文件的文件名即为baseOffset的值。
(2)position:物理地址,也就是消息在日志分段文件中对应的物理位置,占用4个字节。
1.3.2 时间戳索引
每个索引项占用12个字节,分为两个部分。
(1)timestamp:当前日志分段最大的时间戳。 (2)relativeOffset:时间戳所对应的消息的相对偏移量。
1.3.2 日志清理
Kafka提供了两种日志清理策略 日志删除(Log Retention):按照一定的保留策略直接删除不符合条件的日志分段 日志压缩(Log Compaction):针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本
log.cleanup.policy来设置日志清理策略,此参数的默认值为“delete”,即采用日志删除的清理策略 如果要采用日志压缩的清理策略,就需要将log.cleanup.policy设置为“compact”,并且还需要将log.cleaner.enable(默认值为true)设定为true log.cleanup.policy参数设置为“delete,compact”,还可以同时支持日志删除和日志压缩两种策略
日志删除 在Kafka的日志管理器中会有一个专门的日志删除任务来周期性地检测和删除不符合保留条件的日志分段文件,这个周期可以通过broker端参数log.retention.check.interval.ms来配置,默认值为300000,即5分钟 当前日志分段的保留策略有3种:基于时间的保留策略、基于日志大小的保留策略和基于日志起始偏移量的保留策略
磁盘存储 Kafka 在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且也不允许修改已写入的消息,这种方式属于典型的顺序写盘的操作,所以就算 Kafka使用磁盘作为存储介质,它所能承载的吞吐量也不容小觑
页缓存 Kafka 中大量使用了页缓存,这是 Kafka 实现高吞吐的重要因素之一。
虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务的,但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync)的功能,这些功能可以通过 log.flush.interval.messages、log.flush.interval.ms 等参数来控制
磁盘I/O流程 除了消息顺序追加、页缓存等技术,Kafka还使用零拷贝(Zero-Copy)技术来进一步提升性能 所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。
对 Linux操作系统而言,零拷贝技术依赖于底层的 sendfile()方法实现。对应于 Java 语言,FileChannal.transferTo()方法的底层实现就是sendfile()方法
message.max.bytes
消息体的最大大小,单位是字节,默认100012B,约976.6KB。如果Producer发送的消息大于这个值,报出RecordTooLargeException。该参数还受max.request.size(客户端参数),max.message.bytes(topic端参数)影响。
replica.lag.time.max.ms 4000 如果一个replica没有备份的条数超过这个数值,则leader将移除这个follower,并认为这个follower已经挂了
2 生产者
2.1 整体架构
生产者客户端由两个线程协调运行,即主线程和
2.1.1 主线程
在主线程中由KafkaProducer创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到RecordAccumulator(消息累加器)中
2.1.2 累加器RecordAccumulator
主要用来缓存消息以便 Sender 线程可以批量发送,进而减少网络传输的资源消耗以提升性能。
RecordAccumulator缓存的大小由buffer.memory决定。如果数据产生速度大于向broker发送的速度,producer的send方法会阻塞或者抛出异常。阻塞时间 由max.block.ms决定。
buffer.memory
默认33554432,即32MB。RecordAccumulator用来缓存数据的内存大小。如果数据产生速度大于向broker发送的速度,producer的send方法会阻塞或者抛出异常。
max.block.ms
阻塞时间 ,默认60000,即60s
原理
在RecordAccumulator 的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,即 Deque<ProducerBatch>。消息写入缓存时,追加到双端队列的尾部;Sender读取消息时,从双端队列的头部读取。
在RecordAccumulator的内部还有一个BufferPool,它主要用来实现ByteBuffer的复用,以实现缓存的高效利用。BufferPool只针对特定大小的ByteBuffer进行管理,而其他大小的ByteBuffer不会缓存进BufferPool中,
这个特定的大小由batch.size参数来指定,默认值为16384B,即16KB。batch.size与ProducerBatch大小紧密相关, ProducerBatch中有多个ProducerRecord,如果不超过batch.size,则以batch.size大小来创建ProducerBatch,否则以评估大小创建。
batch.size
默认值为16384B,即16KB
2.1.3 Sender线程
Sender 线程负责从RecordAccumulator中获取消息并将其发送到Kafka中
Sender 从 RecordAccumulator 中获取缓存的消息之后,会进一步将原本<分区,Deque<ProducerBatch>>的保存形式转变成<Node,List< ProducerBatch>的形式。接着,Sender 还会进一步封装成<Node,Request>的形式,这样就可以将Request请求发往各个Node了。
Deque<ProducerBatch>=》 <Node,List< ProducerBatch> => <Node,Request>
Node表示Kafka集群的broker节点 Request是指Kafka的各种协议请求
2.2 重要的生产者参数
acks 用来指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。
acks=1:默认值即为1。生产者发送消息之后,只要分区的leader副本成功写入消息,那么它就会收到来自服务端的成功响应。acks=0:生产者发送消息之后不需要等待任何服务端的响应。acks=-1或acks=all:生产者在消息发送之后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。
max.request.size
用来限制生产者客户端能发送的消息的最大值,默认值为 1048576B,即 1MB。注意该参数不能超过brokre端的参数message.max.bytes,否则会报错RecordTooLargeException
batch.size
该参数对于调优producer至关重要。producer采用分批发送机制,该参数即控制一个ProducerBatch的大小。默认是16KB。
max.in.flight.requests.per.connection 它指定了Sender线程在单个Socket连接上能够发送未应答PRODUCE请求的最大请求数。适当增加此值通常会增大吞吐量,从而整体上提升producer的性能。
如果开启了重试机制,配置该参数大于1可能造成消息发送的乱序(先发送A,然后发送B,但B却先行被broker接收)
retries
retries 配置生产者重试的次数,默认值为0,即在发生异常的时候不进行任何重试动作。 producer中一般会发生两种类型的异常,可重试的异常和不可重试的异常。对于可重试的异常,只要在重试次数内自行恢复了,就不会抛出异常。
retry.backoff.ms
设定两次重试之间的时间间隔,避免无效的频繁重试。默认值为100。
connections.max.idle.ms 指定在多久之后关闭限制的连接,默认值是540000(ms),即9分钟
linger.ms
指定生产者发送 ProducerBatch 之前等待更多消息(ProducerRecord)加入ProducerBatch 的时间,默认值为 0。(类似TCP中的Nagle算法)
边界:一旦获得某个partition的batch.size,将会立即发送而不顾这项设置
然而如果获得消息字节数比这项设置要小的多,需要“linger”特定的时间以获取更多的消息。
receive.buffer.bytes
这个参数用来设置Socket接收消息缓冲区(SO_RECBUF)的大小,默认值为32768(B),即32KB。如果设置为-1,则使用操作系统的默认值。
send.buffer.bytes
设置Socket发送消息缓冲区(SO_SNDBUF)的大小,默认值为131072(B),即128KB。如果设置为-1,则使用操作系统的默认值。
request.timeout.ms
配置Producer等待请求响应的最长时间,默认值为30000(ms)。请求超时后可以重试。
注意要比broker端参数replica.lag.time.max.ms要大,这样可以减少客户端因为重试而引起消息重复的概率。
2.3 同步异步和ack的联系和区别
同步和异步指client(producer)是否收到leader给的ack后才发
当用户调用send时,就完成数据发送了(对于用户来说),后台线程负责实际发送数据,因此,我们说数据发送总是异步的 用户可以通过send().get() ,把用户主线程改为同步方式(用户线程有同步和异步之分;发送线程只有异步)
send()方法每次只能发送一条数据至InFlightRequest队列max.in.flight.requests.per.connection控制只能发送一次请求,发送次数有个窗口,控制该窗口的值,但是每次可发送一批数据; batch.size是控制一批数据的上限,当batch.size=1时,每次最多发送一条。 组合在一起就是只能连续发送一次请求,每次最多发送一条
3 消费者
3.1 消费者和消费组
3.1.1 基本概念
消费者负责订阅 Kafka 中的主题(Topic),并且从订阅的主题上拉取消息。与其他一些消息中间件不同的是:在 Kafka 的消费理念中还有一层消费组的概念,每个消费者都有一个对应的消费组。当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者。
- 每个消费组有一个或者多个消费者
- 每个消费组拥有一个唯一性的标识id(group.id)
- 消费组在消费topic的时候,topic的每个partition只能分配给一个消费组中的消费者
3.1.2 消息投递模式
对于消息中间件而言,一般有两种消息投递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。 点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息。 发布订阅模式定义了如何向一个内容节点(主题Topic)发布和订阅消息,消息发布者将消息发布到某个主题,而消息订阅者从主题中订阅消息. 发布/订阅模式在消息的一对多广播时采用。
- 如果所有的消费者都隶属于同一个消费组,那么所有的消息都会被均衡地投递给每一个消费者,即每条消息只会被一个消费者处理,这就相当于点对点模式的应用。
- 如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用。
3.2 消费消息
3.2.1 位移的提交
消费者使用offset来表示消费到分区中某个消息所在的位置
消费位移存储在Kafka内部的主题__consumer_offsets中。将消费位移存储起来(持久化)的动作称为“提交”,消费者在消费完消息之后需要执行消费位移的提交。
注意,当前消费者需要提交的位移并不是x,而是x+1
3.2.2 自动提交和异步提交
消费者客户端参数enable.auto.commit表示提交方式是自动提交还是手动提交。
3.2.2.1 自动提交
默认值为 true,表示自动提交。消费者会在poll方法调用后每隔5秒(由auto.commit.interval.ms指定)提交一次位移。
在调用poll()时,消费者判断是否到达提交时间,如果是则提交上一次poll()返回的最大位移。
自动提交问题:
- 消息重复:某个消费者poll消息后,应用正在处理消息,在3秒后kafka进行了重平衡,那么由于没有更新位移导致重平衡后这部分消息重复消费
- 消息丢失: 当offset被自动定时提交时,数据还在内存中未处理,此时刚好把线程kill掉,那么offset已经提交,但是数据未处理,导致这部分内存中的数据丢失
3.2.2.1 自动提交
enable.auto.commit = false表示手动提交,自动位移提交的方式在正常情况下不会发生消息丢失或重复消费的现象,但是在编程的世界里异常无可避免,自动位移提交也无法做到精确的位移管理。
手动提交分为同步提交和异步提交两种方式:
同步提交
KafkaConsumer使用commitSync()同步提交。根据poll()方法拉取的最新位移来提交,只要没有发生不可恢复的错误,就会阻塞消费者线程直到提交完成。
带参数的commitSync方法可以提交指定分区的位移
commitSync(Map<TopicPartition, OffsetAndMetadata> offsets)
异步提交
KafkaConsumer使用commitAsync()进行异步提交。可以使消费者性能增强。异步提交没有实现重试。
带参数的commitAsync方法可以提交指定分区的位移
commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)
3.1.2 控制或关闭消费
KafkaConsumer使用 pause()和resume()方法来分别实现暂停和恢复分区的拉取。
如何优雅地退出消费消息的poll()循环:
方式一
while(isRunning.get())的方式,这样可以通过在其他地方设定isRunning.set(false)来退出while循环
方式二
调用KafkaConsumer的wakeup()方法,wakeup()方法是 KafkaConsumer 中唯一可以从其他线程里安全调用的方法
KafkaConsumer 是非线程安全的
调用wakeup()方法后可以退出poll()的逻辑,并抛出 WakeupException 的异常
3.1.3 指定位移消费
auto.offset.reset值详解
auto.offset.reset的配置决定从何处开始进行消费:
- latest: 表示从分区末尾开始消费消息,默认值
- earliest: 从起始处,也就是0开始消费
- none: topic 各分区都存在已提交的offset 时,从 offset 开始消费;出现查不到消费位移的时候,则抛出异常
seek()方法
KafkaConsumer 中的 seek() 方法可以从指定的位移处开始拉取消息
public void seek(TopicPartition partition, long offset)
参数 partition 表示分区,offset用来指定从分区的哪个位置开始消费。
seek() 方法只能重置消费者分配到的分区的消费位置,而分区的分配是在 poll() 方法的调用过程中实现的,也就是说,在执行 seek() 方法之前需要先执行一次 poll() 方法,等到分配到分区之后才可以重置消费位置。
如果对未分配的分区执行 seek()方法,那么会报出 IllegalStateException 的异常.
3.2 再均衡
再均衡是指分区的所属权从一个消费者转移到另一消费者的行为,它为消费组具备高可用性和伸缩性提供保障,使我们可以既方便又安全地删除消费组内的消费者或往消费组内添加消费者。 在再均衡发生期间,消费组内的消费者是无法读取消息的
3.2.1 触发条件
ConsumerGroup(消费组)里的Consumer(消费者)数量发生变化(主动加入、主动离开、崩溃):崩溃不一定就是指 consumer进程"挂掉"、 consumer进程所在的机器宕机、长时间GC、网络延迟,当 consumer无法在指定的时间内完成消息的处理,那么coordinator就认为该 consumer已经崩溃,从而引发新一轮 rebalance。- 订阅
topic(主题)的数量发生变更(比如使用正则表达式的方式订阅),当匹配正则表达式的新topic被创建时则会触发 rebalance。 - 订阅
topic(主题)的partition(分区)数量发生变更(kafka目前只支持增加分区)
3.2.2 重平衡的缺点
消费组不可用: 重平衡过程中,消费者无法从kafka消费消息,这对kafka的TPS影响极大,而如果kafka集内节点较多,比如数百个,那重平衡可能会耗时极多。数分钟到数小时都有可能,而这段时间kafka基本处于不可用状态。
消费者当前状态丢失: 比如消费者消费完某个分区的部门消息还来不及提交,分区被分配给了另一个消费者,会发生重复消费。
3.2.3 避免
那首先要知道哪些情况会出现错误判断挂掉的情况。 在分布式系统中,通常是通过心跳来维持分布式系统的,kafka也不例外。你无法完全保证消费者不会故障。而消费者故障其实也是最常见的引发重平衡的地方。 而其他几种触发重平衡的方式,增加分区,或是增加订阅的主题,或是增加消费者,更多的是主动控制
如果消费者真正挂掉了,那我们是没有什么办法的,但实际中,会有一些情况,会让kafka错误地认为一个正常的消费者已经挂掉了,我们要的就是避免这样的情况出现
影响消费者数量减少的参数
session.timout.ms
Broker端参数,消费者的存活时间,默认10秒,如果在这段时间内,协调者没收到任何心跳,则认为该消费者已崩溃离组
heartbeat.interval.ms
消费者端参数,发送心跳的频率,默认3秒
max.poll.interval.ms
消费者端参数,两次调用poll的最大时间间隔,默认5分钟,如果5分钟内无法消费完,则会主动离组。
建议参考值
- session.timeout.ms ≥ 3 * heartbeat.interval.ms
- session.timout.ms 控制心跳超时时间,推荐值设置为6s
- heartbeat.interval.ms 控制心跳发送频率,推荐值设置为2s
- max.poll.interval.ms 控制poll的间隔,推荐为消费者处理消息最长耗时再加1分钟
3.2.4 再均衡监听器 ConsumerRebalanceListener
再均衡监听器用来设定发生再均衡前后的一些准备或清理工作,如提交偏移量,或关闭数据库连接等。
void onPartitionsRevoked(Collection<TopicPartition>partitions)
// 在均衡开始之前和消费者停止读取消息之后调用,一般用来提交偏移量
void onPartitionsAssigned(Collection<TopicPartition>partitions)
//在重新分配分区之后和消费者开始读取消息之前调用,一般用来指定消费偏移量
3.2.5 分区分配策略
分区分配策略决定订阅topic的每个分区会被分配给哪个consumer。默认提供了 3 种分配策略
range策略: 将单个 topic 的所有分区按照顺序排列,然后把这些分区划分成固定大小的分区段并依次分配给每个 consumer。假设有ConsumerA和ConsumerB分别处理三个分区的数据,当ConsumerC加入时,触发rebalance后,ConsumerA、ConsumerB、ConsumerC每个都处理2个分区的数据。
round-robin策略: 把所有 topic 的所有分区顺序摆开,然后轮询式地分配给各个 consumer
sticky策略: 有效地避免了上述两种策略完全无视历史分配方案的缺陷。采用了"有黏性"的策略对所有 consumer 实例进行分配,可以规避极端情况下的数据倾斜并且在两次 rebalance间最大限度地维持了之前的分配方案
自定义策略:
PartitionAssignor接口用于用户定义实现分区分配算法,以实现Consumer之间的分区分配。
3.3 多线程实现
KafkaProducer是线程安全的,然而KafkaConsumer却是非线程安全的。
KafkaConsumer中定义了一个 acquire()方法,用来检测当前是否只有一个线程在操作,若有其他线程正在操作则会抛出ConcurrentModifcationException异常
KafkaConsumer中的每个公用方法在执行所要执行的动作之前都会调用这个acquire()方法,只有wakeup()方法是个例外 acquire()方法和我们通常所说的锁(synchronized、Lock等)不同,它不会造成阻塞等待,我们可以将其看作一个轻量级锁,它仅通过线程操作计数标记的方式来检测线程是否发生了并发操作,以此保证只有一个线程在操作。
第一种: 线程封闭,即为每个线程实例化一个KafkaConsumer对象。一个消费线程可以消费一个或多个分区中的消息。并发度受限于分区的个数。也是最常见的方式。
第二种: 多个线程同时消费一个分区,通过assign(),seek()等方式实现。对于位移提交和顺序控制非常复杂,实际运用不推荐。 一般而言,分区是消费线程的最小划分单位。
第三种: 线程池。思路是一个消费线程poll到消息后,将消息提交到线程池处理。相对于第一种方式,还可以减少TCP连接。缺点是对于消息的处理比较困难。
3.4 重要的消费者参数
fetch.min.bytes 一次拉取消息最小返回的字节数量,默认为1B。若是不满足这个数值则会等待直到满足指定大小。默认为1B。
fetch.max.wait.ms
如果没有足够的数据能够满足fetch.min.bytes,则此项配置是指在应答fetch请求之前,server会阻塞的最大时间,默认500ms。
fetch.max.bytes
一次拉取消息最大返回的字节数量,默认为50M。
注意:如果一个分区的第一批消息大小大于该值也会返回。最大接收消息的大小通过服务端参数message.max.bytes(对应主题端参数max.message.bytes)来设置。
max.partition.fetch.bytes
该属性指定了服务器从每个分区里返回给消费者的最大字节数。默认值lMB。
与fetch.max.bytes类似,如果值比消息的大小要小,依然能消费。
max.partition.fetch.bytes限制一次拉取总分区的大小,fetch.max.bytes限制整体消息大小。
max.poll.records Consumer一次拉取的最大消息数,默认为500(条)
session.timeout.ms
当消费者被认为已经挂掉之前可以与服务器断开连接的时间。默认是10000。
消费者在session.timeout.ms之内没有发送心跳,将会被认为已经死亡。此时,协调器将会发生再均衡,该属性与心跳频率heartbeat.interval.ms(默认3000)紧密相关,heartbeat.interval.ms参数值必须要小于session.timeout.ms(一般是1/3)。
connections.max.idle.ms 指定在多久之后关闭限制的连接,默认值是540000(ms),即9分钟
enable.auto.commit
指定了消费者是否自动提交偏移量,默认值是true。如果设置为true, 可以通过设置 auto.commit.interval.ms(默认5000)属性来控制提交的频率。
auto.offset.reset
重置位点策略,在kafka提交位点时,对应的消息已被删除时采取的恢复策略,默认为latest,可选:earliest、none(会抛出异常)。
send.buffer.bytes
网络通道(TCP)的发送缓存区大小,默认为128K。
receive.buffer.bytes 网络通道(TCP)的接收缓存区大小,默认为64KB。
request.timeout.ms 请求的超时时间,与Broker端的网络通讯的请求超时时间,默认为3000ms。
reconnect.backoff.ms 重新建立链接的等待时长,默认为50ms。
retry.backoff.ms 尝试重新发送失败的请求到指定分区前的等待时间,即重试间隔时间,默认为100ms。
4 事务
消息传输保障
- 最多一次(<=1): 消息不会被重复发送,最多被传输一次,但也有可能一次不传输
- 最少一次(>=1):消息不会被漏发送,最少被传输一次,但也有可能被重复传输
- 精确的一次(Exactly once)(=1): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次
4.1 幂等性
幂等简单理解就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能之后就可以避免这种情况
实现
Kafka引入了producer id(PID)和序列号(sequence number)这两个概念。
-
PID: 每个新的生产者实例在初始化的时候都会被分配一个PID,这个PID对用户而言是透明的。 -
sequence number: 对于每个PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将对应的序列号的值加1。
broker会在内存中维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_new)比broker端中维护的对应的序列号的值(SN_old)大1(即SN_new = SN_old + 1)时,broker才会接收它。
如果SN_new < SN_old + 1,那么说明消息被重复写入,broker可以直接将其丢弃。
如果SN_new > SN_old + 1,那么说明中间有数据尚未写入,出现了乱序,暗示可能有消息丢失,这个异常是一个严重的异常。
幂等性只能保证单个生产者会话中(session)中单分区的幂等。
配置
enable.idempotence=true
4.2 多会话幂等性
-
事务型 Producer 可以将一组消息要么全部写入成功或者全部失败,实现多分区以及多会话上的消息无重复的呢,主要的机制是两阶段提交(2PC),引入了事务协调器的组件帮助完成分布式事务
-
Kafka引入了一个新的组件Transaction Coordinator,它管理了一个全局唯一的事务ID(Transaction ID),并将生产者的PID和事务ID进行绑定,当生产者重启时虽然PID会变,但仍然可以和Transaction Coordinator交互,通过事务ID可以找回原来的PID,这样就保证了重启后的生产者也能保证Exactly Once 了。
-
同时,Transaction Coordinator 将事务信息写入 Kafka 的一个内部 Topic,即使整个kafka服务重启,由于事务状态已持久化到topic,进行中的事务状态也可以得到恢复,然后继续进行。
Kafka事务通过隔离机制来实现多会话幂等性。 使用事务,事务要求生产者开启幂等特性,应用程序必须提供唯一的transactionalId。
配置
transactional.id = ?
enable.idempotence = true (如果未显式设置,则KafkaProducer默认会将它的值设置为true,设置为false,则会报出ConfigException的异常)
4.2.1 生产者端
KafkaProducer提供了5个与事务相关的方法
void initTransactions();
void beginTransaction() throws ProducerFencedException;
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
String consumerGroupId) throws ProducerFencedException;
void commitTransaction() throws ProducerFencedException;
void abortTransaction() throws ProducerFencedException;
- initTransactions() 初始化事务
- beginTransaction() 开启事务
- sendOffsetsToTransaction() 为消费者提供在事务内的位移提交的操作
- commitTransaction() 提交事务
- abortTransaction() 中止事务,类似于事务回滚
4.2.2 消费端
消费端参数isolation.level,与事务关联
-
read_uncommitted:表明消费端可以消费未提交的事务,默认值 -
read_committed:表示消费端应用看不到尚未提交的事务内的消息