4.3 数据不丢失机制

一般我们在用到这种消息中间件的时候,肯定会考虑要怎样才能保证数据不丢失,在面试中也会问到相关的问题。但凡遇到这种问题,是指3个方面的数据不丢失,即:produce r端数据不丢失、 consumer 端数据不丢失、 broker端数据不丢失。下面我们分别从这三个方面来学习,kafka是如何保证数据不丢失的

4.3.1 broker端数据不丢失(leader竞选导致消息丢失)

生产者通过分区的leader写入数据后,所有再ISR中的follower都会从leader中复制数据,这样,可以确保即使leader奔溃了,其他的follower的数据依然可以使用。

broker相关保证数据不丢失的配置如下

  • replication-factor 3
    在创建topic时会通过replication-factor来创建副本的个数,它提高了kafka的高可用性,同时,它允许n-1台broker挂掉,设置好合理的副本因子对kafka整体性能是非常有帮助的,通常是3个极限是5个,如果多了也会影响开销。
  • min.insync.replicas = 2
    分区ISR队列集合中最少有多少个副本,默认值是1,ISR集合是可参与竞选leader的集合
  • unclean.leander.election.enable = false
    是否允许从ISR队列中选举leader副本,默认值是false,如果设置成true,则可能会造成数据丢失

详细解释:

min.insync.replica    在一个topic中,1个分区 有3个副本,在创建时设置了min.insync.replica=2,假如此时在ISR中只有leader副本(1个)存在,在producer端生产数据时,此时的acks=all,这也就意味着在producer向broker端写数据时,必须保证ISR中指定数量的副本(包含leader、fllow副本)全部同步完成才算写成功,这个数量就是由min.insync.replica来控制的,这样producer端向broker端写数据是不成功,因为ISR中只有leader副本,min.insync.replica要求2个副本,此时的producer生产数据失败(异常),当然consumer端是可以消费数据的,只不过是没有新数据产生而已.这样保证了数据的一致性,但这样会导致高可用性降低了。

unclean.leader.election.enable    假如现在有leader 0 、fllow 1、 fllow 2、 三个副本,存储的数据量分别是10 9 8,此时的broker的配置是:min.insync.replica=2 ,且acks=all,leader的数据更新到了15,在没有同步到fllow 1 fllow 2时挂掉了,此时的ISR队列中是有fllow 1 和fllow 2的,如果unclean.leader.election.enable设置的是true,表示在ISR中的副本是可以竞选leader这样就会造成9-15或8-15之间的数据丢失,所以unclean.leader.election.enable必须设置成成false,这样整个kafka cluster都不读写了,这样就保证了数据的高度一致性.

4.3.2 生产者数据不丢失

  • 生产者连接leader写入数据时,可以通过ACK机制保证自己的数据时候已经写入成功。ACK机制有三个可选的配置
  • **acks=-1/all,**高可用型
  • retries > 0 ,retry.backoff.ms=100(毫秒) (并根据实际情况设置retry可能恢复的间隔时间)
  • 优点:这样保证了producer端每发送一条消息都要成功,如果不成功并将消息缓存起来,等异常恢复后再次发送。
  • 缺点:这样保证了高可用,但是这会导致集群的吞吐量不是很高,因为数据发送到broker之后,leader要将数据同步到fllower上,如果网络带宽、不稳定等情况时,ack响应时间会更长
  • acks=1,折中型
  • acks = 1 retries > 0 retries 时间间隔设置 (并根据实际情况设置retries可能恢复的间隔时间)
  • 优点:保证了消息的可靠性和吞吐量,是个折中的方案
  • 缺点:性能处于2者中间
  • acks=0,高吞吐型
  • 优点:可以相对容忍一些数据的丢失,吞吐量大,可以接收大量请求
  • 缺点:不知道发送的消息是 否成功
  • 在kafka中错误分为2种,一种是可恢复的,另一种是不可恢复的。
  • 可恢复性的错误:如:遇到在leader的选举网络的抖动等这些异常时,如果我们在这个时候配置的retries大于0的,也就是可以进行重试操作,那么等到leader选举完成后、网络稳定后,这些异常就会消失,错误也就可以恢复,数据再次重发时就会正常发送到broker端**。需要注意retries(重试)之间的时间间隔,以确保在重试时可恢复性错误都已恢复**。
  • 不可恢复性的错误:如:超过了发送消息的最大值(max.request.size)时,这种错误是不可恢复的,如果不做处理,那么数据就会丢失,因此我们需要注意在发生异常时把这些消息写入到DB、缓存本地文件中等等,把这些不成功的数据记录下来,等错误修复后,再把这些数据发送到broker端
  • 除了ACK机制的保证,用户还可以采用同步和异步的方式发送数据。
  • 同步:发送一批数据给Kafka后,等待Kafka返回结果
  • 异步:发送一批数据给Kafka后,只是提供一个回调函数。

4.3.3 消费者数据不丢失

高可用模型及幂等

在分布式系统中一般有三种处理语义:

  • At-most once(最多一次):只管把数据消费到,不管有没有成功,可能会有数据的丢失。 如果在 Ack 超时或返回错误时 Producer 不重试,也就是我们讲 request.required.acks = -1,则该消息可能最终没有写入 Kafka,所以 Consumer 不会接收消息。
  • at-least-once(至少一次,有可能会有多次):producer在收到ack时,证明消息已经写入kafka;但是ack超时或者返回错误,producer会尝试重新发送消息;
  • **at-exactly-once(仅有一次)**无论producer向kafka发送多少相同的消息,kafka知会持久化一条。

在consumer消费阶段,对offset的处理,关系到是否丢失数据,是否重复消费数据,因此,我们把处理好offset就可以做到exactly-once && at-least-once(只消费一次)数据。

enable.auto.commit=true时,表示由kafka的consumer端自动提交offset,当你在pull(拉取)30条数据,在处理到第20条时自动提交了offset,但是在处理21条的时候出现了异常,当你再次pull数据时,由于之前是自动提交的offset,所以是从30条之后开始拉取数据,这也就意味着21-30条的数据发生了丢失

由于上面的情况可知自动提交offset时,如果处理数据失败就会发生数据丢失的情况。那我们设置成手动提交,即将enable.auto.commit=false时,由于是手动提交的,可以处理一条提交一条,也可以处理一批,提交一批,由于consumer在消费数据时是按一个batch来的,当pull了30条数据时,如果我们处理一条,提交一个offset,这样会严重影响消费的能力,那就需要我们来按一批来处理,或者设置一个累加器,处理一条加1,如果在处理数据时发生了异常,那就把当前处理失败的offset进行提交(放在finally代码块中)注意一定要确保offset的正确性,当下次再次消费的时候就可以从提交的offset处进行再次消费。

消息丢失

首先一个消费者要消费消息的时候,会根据自己所在的消费者组ID找到需要消费的topic,然后找到对应的partition(),再找到对应的offset。通过offset进行一整套机制查找到message。读取到数据之后,再Java程序中进行处理这条消息,并将处理后的结果写入到存储中,如MySQL等。在写入数据的时候,恰好出现了故障,导致写入数据失败。上述提交offset方式有两种,如果采用自动提交的方式时,即便存储消息失败了,还是会向Zookeeper中提交offset,进而导致消息丢失。提交到zookeeper后,offset就进行累加了,下次消费就会从新的offset开始消费,那么旧的offset数据就丢失了。

如何保证kafka数据不丢失 kafka怎么保证数据不丢失_分布式

消息重复消费

消费者获取offsset的步骤与上述的一致,当消费者获取到offset之后就可以开始消费数据了。根据offset向partition中拉取数据,消费者通过Java程序处理数据,并将结果写入到MySQL中。并通过自动提交的方式提交offset。数据写在MySQL的步骤是成功对的,但写入到Zookeeper中是失败的,那么将会出现数据重复消费的情况了。

如何保证kafka数据不丢失 kafka怎么保证数据不丢失_kafka_02

解决办法

另一种办法是将 offset 存储在数据库中,和消息处理放在同一事务中,真正实现 exactly once.

kafka0.9版本之前,offset存储在zookeeper,0.9版本以及之后,默认offset存储在kafka的一个内置的topic中。除此之外,kafka还可以选择自定义存储offset。

因为Kafka的消息是可以通过LowLevel API的方式进行对offset有选择性的拉取数据的,因此offset全都存储在MySQL中, 每次需要拉去数据是先通过MySQL获取offset,通过offset拉取到数据后,需要将数据保存在MySQL数据库中.当然这样还是会有可能导致存储到数据库时发生异常,导致数据的重复消费.那么我们的二次解决办法是,使用MySQL事务,将写入到MySQL的数据和保存offset放在一个MySQL事务之中,如果两个操作都不会出现异常,则提交事务.这样的话要么全部成功,要么全部失败,因此就实现了at-exactly-once了.