1、Kafka如何防止数据丢失
1)消费端弄丢数据
消费者在消费完消息之后需要执行消费位移的提交,该消费位移表示下一条需要拉取的消息的位置。Kafka默认位移提交方式是自动提交,但它不是在你每消费一次数据之后就提交一次位移,而是每隔5秒将拉取到的每个分区中的最大的消费位移进行提交。自动位移提交在正常情况下不会发生消息丢失或重复消费的现象,唯一可能的情况,你拉取到消息后,消费者那边刚好进行了位移提交,Kafka那边以为你已经消费了这条消息,其实你刚开始准备对这条消息进行业务处理,但你还没处理完,然后因为某些原因,自己挂掉了,当你服务恢复后再去消费,那就是消费下一条消息了,那么这条未处理的消息就相当于丢失了。所以,很多时候并不是说拉取到消息就算消费完成,而是将消息写入数据库或缓存中,或者是更加复杂的业务处理,在这些情况下,所有的业务处理完成才能认为消息被成功消费。Kafka也提供了对位移提交进行手动提交的方式,开启手动提交的前提是消费者客户端参数enable.auto.commit配置为false,
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
消费者端手动提交方式提供了两种,commitSync()同步提交方式和commitAsync()异步提交方式。commitSync()同步提交方式在调用时Consumer程序会处于阻塞状态,直到远端的broker返回提交结果,这个状态才会结束,这样会对消费者的性能有一定的影响。commitAsync()异步提交方式在执行后会立刻返回,不会被阻塞,但是它也有相应的问题产生,如果异步提交失败后,它虽然也有重试,但是重试提交的位移值可能早已经“过期”或者不是最新的值了,因此异步提交的重试其实没有意义。这里我们可以把同步提交和异步提交相结合,以达到最理想的效果。
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
// 处理消息 record
}
consumer.commitAsync();
}
} catch (Exception e){
// 处理异常
} finally {
try {
consumer.commitSync();
} finally {
consumer.close();
}
}
2)Kafka端弄丢数据
如下图,副本A为leader副本,副本B为follower副本,它们的HW和LEO都为4。
image
此时,A中写入一条消息,它的LEO更新为5,B从A中同步了这条数据,自己的LEO也更新为5
image
之后B再向A发起请求以拉取数据,该FetchRequest请求中带上了B中的LEO信息,A在收到该请求后根据B的LEO值更新了自己的HW为5,A中虽然没有更多的消息,但还是在延时一段时间之后返回FetchRresponse,其中也包含了HW信息,最后B根据返回的HW信息更新自己的HW为5。
image
可以看到整个过程中两者之间的HW同步有一个间隙,B在同步A中的消息之后需要再一轮的FetchRequest/FetchResponse才能更新自身的HW为5。如果在更新HW之前,B宕机了,那么B在重启之后会根据之前HW位置进行日志截断,这样便会将4这条消息截断,然后再向A发送请求拉取消息。此时若A再宕机,那么B就会被选举为新的leader。B恢复之后会成为follower,由于follower副本的HW不能比leader副本的HW高,所以还会做一次日志截断,以此将HW调整为4。这样一来4这条数据就丢失了(就算A不能恢复,这条数据也同样丢失了)。
image
对于这种情况,一般要求起码设置如下4个参数:
1)给这个topic设置replication.factor参数:这个值必须大于1,要求每个partition必须有至少2个副本
2)在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower
3)在producer端设置acks=all或-1:这个是要求每条数据,必须是写入所有replica之后,才能认为是写成功了
4)在producer端设置retries为很大的一个值:这个是要求一旦写入失败,就无限重试,它默认为0,即在发生异常之后不进行任何重试。
当然,设置了acks等于all或-1之后,会影响一定的性能。Kafka从0.11.0.0(我们公司现在用的版本为0.10.0.0)开始引入了leader epoch的概念,在需要截断数据的时候使用leader epoch作为参考依据而不是原本的HW。leader epoch代表leader的纪元信息,初始值为0,每当leader变更一次,leader epoch的值就会加1,相当于为leader增设了一个版本号。引入leader epoch很好的解决了前面所说的数据丢失问题,也就不需要去设置acks=all了。
3)生产者端不会丢失数据
如果你配置了上面场景的参数,就是当数据写入leader副本和所有follower副本成功后才返回响应给生产者,如果写入不成功,生产者会不断重试。
2、Kafka 怎么防止重复消费
消费者的自动位移提交方式会带来重复消费的问题。假设刚刚提交完一次消费位移,然后拉取一批消息进行消费,在下一次自动位移提交之前,消费者崩了,那么等消费者恢复再来消费消息的时候又得从上一次位移提交的地方重新开始,这样便发生了重复消费的现象。
其实这里可以类似上面消费端丢失数据的情况,很多时候并不是说拉取到消息就算消费完成,而是将消息写入数据库或缓存中,或者是更加复杂的业务处理,重复消费也同样如此,重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。这里防止重复消费,你可以像上面一样把自动提交改为手动提交,或者是保证消息消费的幂等性。
保证消费消息幂等性
1)如果你是要插入mysql中,可以对其设置唯一键,插入重复的数据只会插入报错,不会有重复数据产生
2)如果你是要写入redis中,每次都是set操作,可以保证幂等性
如何保证消息消费是幂等性的,需要结合具体的业务来看。
3、Kafka为什么这么快?
1)消息压缩
Kafka在对消息进行压缩,Producer 端压缩、Broker 端保持、Consum进行解压缩。它秉承了用时间去换空间的思想,具体来说就是用CPU时间去换磁盘空间或网络I/O传输量,希望以较小的CPU开销带来更少的磁盘占用或更少的网络I/O传输。Kafka支持多种压缩算法,如GZIP、Snappy 和 LZ4。
2)数据读写
Kafka会把收到的消息都写入到磁盘中,它绝对不会丢失数据。因为磁盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以磁盘最讨厌随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
image
如上图,每个partition在存储层面可以看作一个可追加的日志文件,收到消息后Kafka会把数据顺序写入文件末尾。
即便是顺序写入磁盘,磁盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入磁盘,它充分利用了现代操作系统的页缓存,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问,来利用内存提高I/O效率。
除了消息顺序追加、页缓存等技术,Kafka还使用了零拷贝(Zero-Copy)技术来进一步提升性能。所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手,这样大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。
4、消息队列时间开销最大的在哪儿?
根据上面对Kafka的分析,可以类推作为一个消息中间件所需的时间开销主要在以下两个方面:1)消息读写 2)网络传输
5、Kafka跟其他消息队列的差异与适应的场景是哪些?
简单介绍下比较常用的消息中间件:
RabbitMQ是采用Erlang语言实现的AMQP协议的消息中间件,最初起源于金融系统,用于在分布式系统中存储和转发消息。RabbitMQ发展到今天,被越来越多的人认可,这和它在可靠性、可用性、扩展性、功能丰富等方面的卓越表现是分不开的。
RocketMQ是阿里开源的消息中间件,目前已捐献给Apache基金会,它是由Java语言开发的,具备高吞吐量、高可用性、适合大规模分布式系统应用等特点,经历过“双十一”的洗礼,实力不容小觑。
从以下几个方面来分析Kafka与其它常用的消息中间件的差异:
可靠性:Kafka的ISR机制保证其高可用,一主多从,leader副本挂掉后,可以自动选举新的leader;RocketMQ也支持主从机制保证其高可用,通过设定brokerId=0来设置master,不支持主从切换,master失效以后,从slave中进行消费;RabbitMQ也是支持主从机制保证高可用,master挂掉以后,最早加入集群的slave成为master,支持主从自动切换。
单机吞吐量:RabbitMQ单机吞吐量在万级别之内,吞吐量比RocketMQ和Kafka要低了一个数量级;RocketMQ和Kafka单机吞吐量可以维持在十万级别。
应用场景:RabbitMQ在金融支付领域使用较多,而在日志处理、大数据等方面Kafka使用居多,而RocketMQ目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景,支撑了阿里多次双十一活动。
6、Kafka在我们系统中的应用,生产者、消费者分别是什么?groupid是什么,topic是什么?
image
我们的BinaryBinlogKafkaProducer
while (true) {
try {
if(BinlogUtil.isCache(binlog.getHeader().getTableName())){
if(!TopicUtil.isExit(binlog.getHeader().getSchemaName()+"."+binlog.getHeader().getTableName())){
//创建topic
TopicUtil.createTopic(binlog.getHeader().getSchemaName()+"."+binlog.getHeader().getTableName());
}
send(binlog.getHeader().getSchemaName()+"."+binlog.getHeader().getTableName(),binlog.toByteArray());
}else {
send(topic, binlog.toByteArray());
}
break;
} catch (FailedToSendMessageException e) {
// 处理异常
}
}
topic设置有个判断,如果环境变量中有设置cacheTable这个参数,则设置topic为“库名.表名",若没有,则使用“Binlog”作为topic
1571622783461.png
消费者
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MessageConsumer {
String groupId() default "";
String zkHost() default "127.0.0.1";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MessageConsumerAction {
String topic() default "Binlog";
String eventType();
}
消费者的groupId设置在MessageConsumerServiceImpl中,设置的是className
String className = context.getIface() instanceof Proxy ? ((Class)
ifaceClass.getMethod("getTargetClass").invoke(context.getIface())).getName() : ifaceClass.getName();
groupId = "".equals(groupId) ? className : ifaceClass.getName();
1571630301269.png
消费者的消费是通过注解使用的
@MessageConsumer
@Transactional(value ="iplm_reportdb", rollbackFor = Array(classOf[Exception]))
class ReportBinlogServiceImplextends ReportBinlogService{
def init(): Unit ={
// 初始化加载缓存
}
@MessageConsumerAction(topic = "Binlog")
override def onReportModuleBinlogMessage(message: ByteBuffer): Unit = {
// 处理binlog
}
}