作为一个消息中间件,如果在运行过程中丢失消息,往往是很难排查和追溯的,Kafka也会存在丢失消息的情况,今天我想分析一下Kafka丢失消息的几个原因,以及如何尽可能的防止消息不丢失。在分析Kafka丢失消息的时候,我准备从broker、producer、consumer三端进行分析。
1.broker
producer发送消息到broker端,如果broker接收并保存该消息,返回ack标记到producer,那么认为该消息已经生产并发送完成。
kafka中ack有三种模式:
ack=0,即生产者发送完消息就认为该消息已经发送成功,并不会等到broker的响应,这种是效率比较高的一种方式,但极有可能丢失消息,所以一般不建议使用。
ack=1,即只要写入leader副本成功就认为消息发送成功,就会返回ack到producer,并不会等到该消息同步到其他follower才ack。一般情况下这种是没有问题的,但还是会存在丢失消息的情况,比如leader副本接收到了消息并返回ack到producer,但是此时ISR中的follower副本还没有同步完这条消息,此时leader副本挂了,ISR中的follower会被重新选举为leader,由于之前没有同步完该消息,导致该follower没有这条消息,之前producer也已经收到ack,认为已经发送成功,无法重发消息,那么这条消息就丢失了。(leader的HW是ISR中所有副本中LEO最靠前的那个位置,即消费者只能消费HW位置之前的消息),后面由于新成为leader的follower没有该消息,所以后面也消费不到了。
ack=-1/all,即不只leader副本要成功接收到消息,还要等到ISR中所有的follower都同步完该消息后,才返回ack给producer。这种方式是效率最差的,但可靠性是最高的。这样即使follower还没有同步完leader中的消息,leader挂掉了,由于没有返回ack给producer,那么producer会进行重试,除非一直重试都发送失败,超过了重试次数,这样肯定是服务器或系统出现了问题,不过这样我们可以通过其他方式将该条消息记录下来。如果follower已经同步完leader中的信息,leader挂了,那么follower中已经有了该消息,那么后面重选举变为leader后也会存在该消息,就不会丢失消息了。
通常和ack搭配使用的broker配置min.insync.replicas,即ISR中最小的副本数量,一般会配置>=2,如果ISR中的replica数量小于该配置的数量,则会抛出异常。如果不配置该参数的话,那么ISR中的follower的数量就可能为0,这样就变为了ack=1了。
2.Producer
producer也存在丢失消息的情况,为了提升效率,producer在发送数据时可以将多个请求缓存起来,然后批量发送。这些消息就会放在本地的buffer中,算是消息缓存区。producer可以按时间间隔将这些buffer数据发出。
但是buffer中的数据是危险的,他们保存在内存中,一旦producer停止了,或者producer客户端内存不够时,采取的策略时丢弃消息,那么这些消息也就丢失了。
这里有几个解决办法:
·将异步发送改为同步发送,或者在产生消息的时候,使用阻塞的线程池,并且线程数有一定上限。整体思路是控制消息产生速度。
·扩大buffer的容量,可以缓解这个情况的出现,但无法杜绝。
·消息不直接发送到buffer内存中,而且先将消息写道本地磁盘或者数据库中,再由另一个生产线程进行消息发送。
3.Consumer
kafka消费端提交offset的方式有自动提交和手动提交两种方式。
自动提交:enable.auto.commit=true,即消费者在调用poll方法时会拉取到要消费的数据,并且在指定的时间后将该消息进行提交,这个时间是可以配置的,如果我们在拿到消息后在指定的时间内没有操作完消息,甚至程序执行异常了,这时候消费者位移已经提交成功,而我们的程序没有处理成功,这样该消息就丢失了。
手动提交:enable.auto.commit=false,即由我们手动的控制位移。
同步提交:consumer.commitSync()
异步提交:consumer.commitAsync()
手动提交保证了消息至少会被消费一次,但是会造成重复消费的问题。
举例说明一种场景,假设你花钱从网上租了一本共有10章内容的电子书,该电子书的有效阅读时间是 1 天,过期后该电子书就无法打开,但如果在 1 天之内你完成阅读就退还租金。
为了加快阅读速度,你把书中的 10 个章节分别委托给你的 10 个朋友,请他们帮你阅读,并拜托他们告诉你主旨大意。当电子书临近过期时,这 10 个人告诉你说他们读完了自己所负责的那个章节的内容,于是你放心地把该书还了回去。不料,在这 10 个人向你描述主旨大意时,你突然发现有一个人对你撒了谎,他并没有看完他负责的那个章节。那么很显然,你无法知道那一章的内容了。
对于Kafka而言,就相当于consumer从kafka获取消息后开启了多个线程异步处理消息,而Consumer自动的向前更新位移,假如其中某个线程运行失败了,他负责的消息没有被处理成功,但是位移已经被更新了,那么这条消息对于consumer已经丢失了。
这是因为consumer自动提交位移,与你没有确认书籍内容被全部读完就将书归还类似,你没有真正地确认消息是否被消费就盲目的更新了位移。
Kafka无消息丢失配置
1.不要使用producer.send(msg),而要使用producer.send(msg, callback)。
2.设置acks=all。acks是producer的一个参数,代表了你对“已提交”消息的定义。如果设置成all,则表明所有副本broker都要接收到消息,该消息才算是“已提交”。
3.设置retries为一个较大的值。retries同样是producer的参数,对应前面提到的producer自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了retries > 0的producer能够自动重试消息发送,避免消息丢失。
4.设置unclean.leader.election.enable=false。这是broker端的参数,他控制的是哪些broker有资格竞选分区的leader。如果一个broker落后原先的leader太多,那么它一旦成为新的leader,必然会造成消息的丢失。故一般都要将该参数设置成false,即不允许这种情况发生。
5.设置replication.factor >= 3。这也是broker端的参数。就是将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
6.设置min.insync.replicas > 1。这也是broker端的参数,控制的消息至少被写入多少副本才算“已提交”。设置成大于1可以提升消息持久性。在生产环境中不要使用默认值1。
7.确保replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要再不降低可用性的基础上完成。推荐设置成replication.factor = min.insync.replicas + 1。
8.确保消息消费完成再提交。Consumer端有个参数enable.auto.commit,最好把它设置成false,并采用手动提交位移的方式。就像前面说的,这对于单Consumer多线程处理的场景而言是至关重要的。