网上有很多Kafka的文章,但大多写得千篇一律,要么偏理论化,无实战数据参考。要么写了发现的某个问题的解决方案,对于想在实际环境上搭建真实的Kafka环境,参考意义并不大。
这篇文章基于大量的实战经验,在大规模,海量数据,以及实时处理的环境下,这些经验也是在解决Kafka很多真实问题得出的。试图在一开始就协助大家在大家在搭建真实Kafka环境的时候,提前做好最优的解决方案,避免后续不断的出各类问题,然后投入大量人力进行整改。
这篇文章不是给完全的初学者准备的,文章并不会针对索引的细节给出一步步的解决,所以,读者如果对一下基本概念不熟悉的话,建议提前阅读相关文档。
先来解答一下到底是要选择Kafka还是像RabbitMQ、RocketMQ、ActiveMQ等其他消息队列?
首先,在大数据环境下,首选一定是Kafka,批量的日志采集,大量的数据同步等,Kafka的性能极其高。
但是如果企业已经有了Kafka,还需要用RabbitMQ、RocketMQ、ActiveMQ等其他消息队列,还是直接就采用Kafka进行业务消息的处理呢?
那就要看业务的,Kafka经过最近几年发展,可靠性方面已经大幅提升,其可靠性可能并不逊色于RabbitMQ。但是我们也知道,Kafka原先是为了日志而生,而为了提高可靠性,需要进行多种配置,比如说,acks配置为-1,增加副本数,写入采用同步等,这个为了提高可靠性而配置,则降低了吞吐率,导致性能并不比RabbitMQ高。
而且在我们的使用中,Kafka在极端情况下,还是出现了极少量的数据丢失,这类问题极难跟踪追查。而RabbitMQ、RocketMQ、ActiveMQ等其他消息队列则一直比较稳定,几乎不用担心数据丢失问题。而且Kafka稳定性要稍弱一点,维护也相对复杂一点。
所以我建议在正常的业务场景下,对性能要求不是特别高的情况下,但是首选RabbitMQ、RocketMQ、ActiveMQ等消息队列。而假设企业已经有Kafka,而且业务并非像银行等对安全要求极其高的业务,允许极端情况下的极少量数据丢失,且企业对Kafka应用比较熟悉,那也可以选择Kafka作为业务的消息队列。
解决了选择问题,那么现在就来谈谈配置问题。
生产者的发送方式应该设置为同步还是异步?我相信这是很多人纠结的地方,而且选择一种方式之后,后续如果想改动就没有那么容易了,往往涉及到代码的改动,如果现网项目是维护很久了,以前的人写了,新的开发人员往往不敢改动这里的配置,生怕发生异常。
默认的发送方式是同步的,但如果改成异步,将极大的提高性能。但异步方式却不能确保消息的可靠性,增加了丢数据的风险。
先来看看异步方式配置,有下面几个参数:
异步方式,可以批量发送消息,并且有设置超时和重试机制,但系统重试超过限制,消息会丢失,不过Kafka0.11版本有回调机制,如果消息发送失败,可以回调,然后由业务觉得下一步处理。
我曾经遇到过因为Kafka的Bug,导致kafka的其中一个broker有一段时间不可用,导致消息发送失败。理论上,对于这部分失败,通过把失败信息记录下来,然后通过重刷数据修复。
但是这个回调机制其实是不能100%保证绝对有回调的,正常情况下是没有对应的,但是在某种极其特殊的异常情况下,Kafka本身出现问题了,有可能是无法回调的。
由于异步方式性能极高,所以业界使用异步方式还是占多数的,所以建议对消息可靠性要求不是极其严格的话,采用异步方式,重试30秒如果失败就回调,这样性能上基本也是可以满足的。
接下来再来讲一讲acks,这边是用来表示可靠性级别的配置,一般有0,1和-1三种配置,默认配置是1。0表示消息发送不等待Leader的确认就继续发送下一条,1表示等待Leader确认后再发下一条,-1表示等待Leader和Follower确认后再发下一条。配置为0极其不可靠,我觉得几乎不用考虑。主要是考虑配置为1和-1的情况。
我们假设就是3副本,也就是说有1个Leader和2个follower,配置为1,也就是说消息写入Leader成功后就返回,这时候,Leader的数据会同步给2个follower,但假设这个时候Leader突然间宕机了,这时就会发生消息丢失。
当配置为-1时,则还有另外一个参数,用来标记多少个follower同步成功后,继续下一条消息的发送。假设消息写入Leader成功后,但follower还在同步,此时Leader挂掉,由于消息还没有得到确认,所以该消息会重新发送,并不会丢失。
设置为-1并不消息消息就一定不会丢失,如果Leader挂掉,重新选举的Leader也挂掉,也就是连续两个Leader都挂掉,是有可能出现消息丢失的。而且,设置为-1可能会产生另外一个问题,就是消息重复,比如说,消息同步Leader成功,同步Follower1成功,但还没来得及同步Follower2,这是Leader宕机,Producter重新发送消息,这是Follower1就重复接受到同一条消息了。
讲了那么多,那配置为1是不是表示就不安全呢?其实大部分情况下是安全的,比如说集群出现故障,其中一个broker挂掉了,consumer端会产生一系列INFO和WARN级别的日志输出,但若干秒之后自动恢复,消息还是连续的,并没有出现断点。
这边遇到过两次消息丢失的情况,一次是因为Kafka的Bug,导致ISR收缩为自己,并且不可用,导致消息丢失。另外一次是因为网络短时间不可用,导致消息丢失。
但是如果acks设置为-1,对性能影响挺大,因为写完Leader之后,要有同步follower,所以相比acks设置为1,可能性能会下降一半。
所以在设置acks配置的时候,如果只是采集日志,可以接受极少量的数据丢失,那么建议把acks配置为1。
如果是业务数据,那么要根据情况看,如果能够接受该增加一倍的硬件成本,那么建议配置为-1,如果能接受极少量的数据丢失,那么可以配置为1。实际使用中,出现消息丢失的情况极难出现,大部分都是业务自身代码的问题导致的。
接下来讲一下副本数吧,建议正常还是采用三副本(就是1个Leader,2个Follower),三副本是比较常用的做法,三副本才是认为可靠的。当然,副本超过3个,那更好,只不过存储成本就高利了,而且提高的可靠性并没有那么明显。而把副本从2个改成三副本,则可靠性提高很多,不推荐设置为2副本。
接下来将讲讲consumer这边。
commit方式有自动提交和手动提交两种方式,默认是自动提交,提交间隔5秒。
自动提交有数据丢失和数据重复的风险,比如说,消息自动提交了,但consumer还没有处理完就挂掉了,那么数据就没有处理完成而丢失了。如果消息处理完成了,但还没来得及提交,系统就挂掉了,那么就会出现数据重复。
但是手动提交也有问题,正常来说,consumer和partition的个数并不一致,而consumer的提交是针对partition的,那么当多个任务对应同一个partition的时候,一个提交就会把别的还在处理的任务也commit了。
现在的consumer通常有两种写法:
第一种方式,consumer的数量不能超过partition的数量,否则多出来的consumer永远不会被用到,而且因为每个consumer都需要一个TCP连接,会造成大量的性能消耗。第二种方式,实现每个partition的消息顺序处理更困难,比如说两个消息被不同的线程处理,那么处理上就可以突破时间顺序,导致后到的数据被提前处理。
通常来说,更多推荐的是采用方式2,它更加容易扩展,性能不够了,扩大线程数量就可以了。
但是,手动提交上面也说了,当多个任务对应同一个partition的时候,一个提交就会把别的线程还没有提交的任务commit了。
综上所述,如果对于消息丢失和重复的要求不是特别高的话,建议还是采用自动提交方式把,它实现简单,逻辑比较健壮。如果对可靠性要求非常高,那么还是采用手动提交的方式,但是就要在处理是就要很注意。