[TOC]
概述
kafka 的可靠性保证和 tcp 的可靠性保证有一致的地方,就是都是基于消息的确认和重传来实现的。当然 tcp 还有滑动窗口,拥塞避免等特性。
为保证 producer 发送的数据,能可靠的发送到指定的 topic,topic 的每个 partition 收到。producer 发送的数据后,broker 都需要向 producer 发送 ack(acknowledgement 确认收到),如果 producer 收到 ack,就会进行下一轮的发送,否则重新发送数据。
常见的两种保证
副本数据同步策略
方案 | 优点 | 缺点 |
---|---|---|
半数以上完成同步,broker 发送 ack | 延迟低 | 选举新的leader时,如果要容忍 n 个节点的故障,那么系统需要 2n + 1个节点 |
全部完成同步,broker 发送 ack | 选举新的leader节点时,容忍 n 台节点的故障,需要 n+1 个副本 | 延迟高 |
kafka 选择了第二种方案,原因如下
- 第二种做数据镜像时,同样的容错下,第二种方案需要的节点更少。而kafka的每一个节点都存储这大量的数据,第一种方案会造成大量的数据冗余
- 第二种的劣势是网络延迟高,但是网络延迟对kafka的影响较小
kakfa 的优化(ISR 同步副本列表)
第二种存在的单节点问题
采用第二种方案之后, 设想以下场景: leader 收到数据, 所有follower 都开始同步数据,但有一个follower ,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。那么一个节点的性能,就会影响整个数据镜像集群的性能了。
解决
Leader 维护了一个动态的in-sync replica set(ISR 同步副本列表) ,意为和leader保持同步的follower 集合. 当ISR 中的 follower 完成数据的同步之后, Leader就会给follower 发送ack如果 follower 长时间未向 leader 同步数据,则该follower 将被 提出ISR 。
ack 机制
对于某些不太重要的数据,对数据的可靠性要求不要很高,能够容忍数据的少量丢失.所以没必要等ISR 中的follower 全部接受成功
所以Kafka 为用户提高了三种可靠性级别,当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:
request.required.acks = 1(默认)
这意味着producer在ISR中的leader已成功收到的数据并得到确认后发送下一条message。如果leader宕机了(因为只有 ISR 中的leader 确认了消息),则会丢失数据。
producer发送数据到leader,leader写本地日志成功,返回客户端成功;此时ISR中的副本还没有来得及拉取该消息,leader就宕机了,那么此次发送的消息就会丢失。
request.required.acks = 0
这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
request.required.acks = -1
producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,ISR中的成员由于某些情况会增加也会减少,最少就只剩一个leader),这样就变成了acks=1的情况。
acks=-1的情况下,数据发送到leader后 ,部分ISR的副本同步,leader此时挂掉。比如follower1h和follower2都有可能变成新的leader, producer端会得到返回异常,producer端会重新发送数据,数据可能会重复。
复制原理和同步方式
Kafka中topic的每个partition有一个预写式的日志文件,虽然partition可以继续细分为若干个segment文件,但是对于上层应用来说可以将partition看成最小的存储单元(一个有多个segment文件拼接的“巨型”文件),每个partition都由一些列有序的、不可变的消息组成,这些消息被连续的追加到partition中。
Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个broker失效情况下仍然保证服务可用。在Kafka中发生复制时确保partition的日志能有序地写到其他节点上,N个replicas中,其中一个replica为leader,其他都为follower, leader处理partition的所有读写请求,与此同时,follower会被动定期地去复制leader上的数据。
LEO & HW
LEO: 指的是每个副本最大的offset ;
HW: 指的是消费者能见到的最大的offset .ISR 队列中最小的LEO;
follower 故障
follower 发生故障后会被临时提出ISR ,待该follower恢复后,follower会读取本地磁盘记录的上次的HW , 并将log文件高于HW的部分截掉,.从HW开始向leader进行同步,等该follower的LEO大于等于该Partition的HW,即follower 追上leader之后,就可以重新加入ISR 了。
leader 故障
leader 发生故障之后,会从ISR 中选出一个新的leader ,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
可靠性的保证
消息传输保障
接下来讨论的是Kafka如何确保消息在producer和consumer之间传输。有以下三种可能的传输保障(delivery guarantee):
- At most once: 消息可能会丢,但绝不会重复传输
- At least once:消息绝不会丢,但可能会重复传输
- Exactly once:每条消息肯定会被传输一次且仅传输一次
broker 的保证
Kafka的消息传输保障机制非常直观。当producer向broker发送消息时,一旦这条消息被commit,由于副本机制(replication)的存在,它就不会丢失。但是如果producer发送数据给broker后,遇到的网络问题而造成通信中断,那producer就无法判断该条消息是否已经提交(commit)。虽然Kafka无法确定网络故障期间发生了什么,但是producer可以retry多次,确保消息已经正确传输到broker中,所以目前Kafka实现的是at least once。
consumer
consumer 最好的方法就是,在引入去重机制后,实现 at least once 的消费方式,即先取数据,然后做业务逻辑,最后commit。
消息去重
如上一节所述,Kafka在producer端和consumer端都会出现消息的重复,这就需要去重处理。
Kafka文档中提及GUID(Globally Unique Identifier)的概念,通过客户端生成算法得到每个消息的unique id,同时可映射至broker上存储的地址,即通过GUID便可查询提取消息内容,也便于发送方的幂等性保证,需要在broker上提供此去重处理模块,目前版本尚不支持。
针对GUID, 如果从客户端的角度去重,那么需要引入集中式缓存,必然会增加依赖复杂度,另外缓存的大小难以界定。
不只是Kafka, 类似RabbitMQ以及RocketMQ这类商业级中间件也只保障at least once, 且也无法从自身去进行消息去重。所以我们建议业务方根据自身的业务特点进行去重,比如业务消息本身具备幂等性,或者借助Redis等其他产品进行去重处理。