Kafka-消息积压、消息过期、读写分离、一致性保障

目录

一、Kafka、消息队列、MQ高频面试问题

1.大量消息在mq里积压了几个小时了还没解决

2.消息设置了过期时间,过期就丢了怎么办

3.积压消息长时间没有处理,mq放不下了怎么办

4.Kafka为什么不支持读写分离?

5.MQ框架如何做到高可用性?

6.MQ框架 如何实现高吞吐量?

7.MQ中如何实现事务消息

8.如何保证数据⼀致性问题?

二、重点问题

2.1 数据一致性问题详解

2.2 Kafka 如何解决消息不丢失?

1、生产端

2、MQ服务端

3、消费端

4.如何解决重复消费,避免引发数据不一致


一、Kafka、消息队列、MQ高频面试问题

1.大量消息在mq里积压了几个小时了还没解决

场景:几千万条数据在MQ里积压了七八个小时,从下午4点多,积压到了晚上很晚,10点多,11点多。线上故障了,这个时候要不然就是修复consumer的问题,让他恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不行。一个消费者一秒是1000条,一秒3个消费者是3000条,一分钟是18万条,1000多万条。

所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概1小时的时间才能恢复过来。

解决方案: 这种时候只能操作临时扩容,以更快的速度去消费数据了。

具体操作步骤和思路如下:

  1. 先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉。
  2. 临时建立好原先10倍或者20倍的queue数量(新建一个topic,partition是原来的10倍)。
  3. 然后写一个临时分发消息的consumer程序,这个程序部署上去消费积压的消息,消费之后不做耗时处理,直接均匀轮询写入临时建好分10数量的queue里面。
  4. 紧接着征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的消息。
  5. 这种做法相当于临时将queue资源和consumer资源扩大10倍,以正常速度的10倍来消费消息。
  6. 等快速消费完了之后,恢复原来的部署架构,重新用原来的consumer机器来消费消息。

kafka的示意图

2.消息设置了过期时间,过期就丢了怎么办

假设你用的是rabbitmq,rabbitmq是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定的时间就会被rabbitmq给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。 解决方案: 这种情况下,实际上没有什么消息挤压,而是丢了大量的消息。所以第一种增加consumer肯定不适用。 这种情况可以采取 “批量重导” 的方案来进行解决。 在流量低峰期(比如夜深人静时),写一个程序,手动去查询丢失的那部分数据,然后将消息重新发送到mq里面,把丢失的数据重新补回来。

3.积压消息长时间没有处理,mq放不下了怎么办

如果走的方式是消息积压在mq里,那么如果你很长时间都没处理掉,此时导致mq都快写满了,咋办?这个还有别的办法吗? 解决方案: 这个就没有办法了,肯定是第一方案执行太慢,这种时候只好采用 “丢弃+批量重导” 的方式来解决了。

首先,临时写个程序,连接到mq里面消费数据,收到消息之后直接将其丢弃,快速消费掉积压的消息,降低MQ的压力,然后走第二种方案,在晚上夜深人静时去手动查询重导丢失的这部分数据。

4.如果让你设计一个MQ,你怎么设计

其实回答这类问题,说白了,起码不求你看过那技术的源码,起码你大概知道那个技术的基本原理,核心组成部分,基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好

比如说这个消息队列系统,我们来从以下几个角度来考虑一下

  1. 首先这个mq得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下kafka的设计理念,broker -> topic -> partition,每个partition放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
  2. 其次你得考虑一下这个mq的数据要不要落地磁盘吧?那肯定要了,落磁盘,才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是kafka的思路。
  3. 其次你考虑一下你的mq的可用性啊?这个事儿,具体参考我们之前可用性那个环节讲解的kafka的高可用保障机制。多副本 -> leader & follower -> broker挂了重新选举leader即可对外服务。
  4. 能不能支持数据0丢失啊?可以的,参考我们之前说的那个kafka数据零丢失方案
  5. 其实一个mq肯定是很复杂的,其实这是个开放题,就是看看你有没有从架构角度整体构思和设计的思维以及能力。

4.Kafka为什么不支持读写分离?

我们知道,生产端写入消息、消费端拉取消息都是与leader 副本交互的,并没有像mysql数据库那样,master负责写,slave负责读。

这种设计主要是从两个方面考虑:
1、数据一致性。一主多从,leader副本的数据同步到follower副本有一定的延时,因此每个follower副本的消息位移也不一样,而消费端是通过消费位移来控制消息拉取进度,多个副本间要维护同一个消费位移的一致性。如果引入分布式锁,保证并发安全,非常耗费性能。
2、实时性。leader副本的数据同步到follower副本有一定的延时,如果网络较差,延迟会很严重,无法满足实时性业务需求。
综上考虑,读写操作都是针对 leader 副本进行的,而 follower 副本主要是⽤于数据的备份

5.MQ框架如何做到高可用性?

以Kafka框架为例,其他的MQ框架原理类似。
Kafka 由多个 broker 组成,每个 broker 是一个节点。你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 存放在不同的 broker 上,每个 partition 存放一部分数据,每个 partition 有多个 replica 副本。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。
如果某个 broker 宕机了,没事儿,那个 broker 上面的 partition 在其他机器上都有副本,此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就是所谓的⾼可⽤性。

6.MQ框架 如何实现高吞吐量?

  1. 消息的批量处理
  2. 消息压缩,节省传输带宽和存储空间
  3. 零拷⻉
  4. 磁盘的顺序写入
  5. page cache ⻚缓存,由操作系统异步将缓存中的数据刷到磁盘,以及高效的内存读取
  6. 分区设计,一个逻辑topic下面挂载N个分区,每个分区可以对应不同的机器消费消息,并发设计。

7.MQ中如何实现事务消息

  1. 生产者先发送一条半事务消息到MQ
  2. MQ收到消息后返回ack确认
  3. 生产者开始执行本地事务
  4. if 本地事务执行成功,发送commit到MQ;失败,发送rollback
  5. 如果MQ⻓时间未收到生产者的二次确认commit或rollback,MQ对生产者发起反向回查
  6. 生产者查询事务执行最终状态
  7. 根据查询事务状态,再次提交⼆次确认

8.如何保证数据⼀致性问题?

为了解耦,引⼊异步消息机制。先进⾏本地数据库操作,处理成功后,再发送MQ消息,由消费端进行后续操作。比如:电商订单下单成功后,要通知扣减库存。
这两者一定要保证事务操作,否则就会出现数据不一致问题。这时候,我们就需要引入事务消息 来解决这个问题。
另外,在消费环节,也可能出现数据不一致情况。我们可以采用 最终一致性 原则,增加重试机制。

如何保证MQ消息是有序的?

有些业务有上下⽂要求,⽐如:电商⾏业的下单、付款、发货、确认收货,每个环节都会发送消息。⽽消费
端拉取并消费消息时,也是希望按正常的状态机流程进行。所以对消息就有了顺序要求。解决思路:
1、该topic强制采用一个分区,所有消息放到一个队列里,这样能达到全局顺序性。但是会损失高并发特性。
2、局部有序,采用路由机制,将同一个订单的不同状态消息存储在一个分区partition ,单线程消费。比如 Kafka 就提供了一个接口扩展 org.apache.kafka.clients.Partitioner ,方便开发人员按照自己的业务场景来定制路由规则。

二、重点问题

2.1 数据一致性问题详解

为了系统间解耦,我们通常会引入MQ框架,大家各司其职共同完成上下游的业务流程。

图片

大致过程:

  • 生产端,创建一条消息,通过网络发送到MQ Server

  • MQ将 消息存储在topic 的一个分区

  • 消费端,从分区中拉取消息,消费处理

但现实往往不一样!MQ 架构设计要满足高并发、高性能、高可用等指标

图片

单分区,达不到我们的吞吐量要求,我们考虑采用多分区架构设计,正所谓 ”三个臭皮匠赛过一个诸葛亮“,多分区可以有效分摊全局压力,提升整体系统性能。

图片

两台 MQ机器,组成一个集群,原先一个分区存储6条消息,现在分摊到两个分区,每个分区各存储3条消息,性能比上面那个提升一倍。

貌似可以满足我们的需求,但任何事情都有两面性!

我们看看下面业务场景:

一个用户在电商网站上下订单到交易完成,中间会经历一系列动作,订单的状态也会随之变化,一个订单会产生多条MQ消息,下单付款发货买家确认收货,消费端需要严格按照业务状态机的顺序处理,否则,就会出现业务问题。

我们发现,消息带上了状态,不再是一个个独立的个体,有了上下文依赖关系!

对于这个问题,突然想到HTTP协议,其本身也是无状态的,也就是说前后两次请求没有关联,但有些业务功能有登录要求,那怎么解决?

引入Cookie机制,每次请求客户端额外传输一些数据,来达到上下文关联。

回到MQ的消息顺序问题,我们要如何解决?

图片

答案:各退一步,保证局部有序。

比如上面的电商例子,只要保证一个订单的多条状态消息在同一个分区,便可以满足业务需求,这个方案可以覆盖大部分的业务场景。

这里面只需要有一个路由策略组件,由它决定消息该放到哪个分区中!

考虑到市面MQ开源框架很多,常见的如:Kafka、Pulsar、RabbitMQ、RocketMQ 等,API方法略有区别,但设计思路是相通的。

接下来,我们以 RocketMQ 为例:

生产端提供了一个接口 MessageQueueSelector

public interface MessageQueueSelector {
   MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

接口内定义一个select方法,具体参数含义:

  • mqs:该Topic下所有的队列分片

  • msg:待发送的消息

  • arg:发送消息时传递的参数

关于MessageQueueSelector接口,RocketMQ 框架提供了三个默认实现类:

  • 1、SelectMessageQueueByHash:

arg参数的hashcode的绝对值,然后对mqs.size()取余,得到目标队列在mqs的下标

  • 2、SelectMessageQueueByRandom:

对mqs.size()值取随机数作为目标队列在mqs的下标

  • 3、SelectMessageQueueByMachineRoom

返回null

特别注意:

虽然保证了单个分片的消息有序,但每个分片的消费者只能是单线程处理,因为多线程无法控制消费顺序。这个可能会损失一些性能。

这里又引出另一个问题,如何保证一个队列只能有一个消费端呢?

1、org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance

图片

  • 遍历一个topic下所有的MessageQueue

  • isOrder && !this.lock(mq) 尝试对它加锁,确保一个MessageQueue只能被一个消费者处理

2、将PullRequest对象放入PullMessageServicepullRequestQueue队列中

public void dispatchPullRequest(List<PullRequest> pullRequestList) {
    for (PullRequest pullRequest : pullRequestList) {
        this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);
        log.info("doRebalance, {}, add a new pull request {}", consumerGroup, pullRequest);
    }
}

3、org.apache.rocketmq.client.impl.consumer.PullMessageService#run

图片

  • PullMessageService 是一个Runnable线程任务

  • 无限循环,从队列中拉取、处理消息

另一个问题,如何保证一个队列,只有一个线程在处理消息呢?

1、 DefaultMQPushConsumerImpl#pullMessage

图片

  • ConsumeMessageService 中有两个实现类,因为我们有消费顺序要求,会选择ConsumeMessageOrderlyService来处理业务

2、 ConsumeMessageOrderlyService.ConsumeRequest

图片

  • ConcurrentMap中获取messageQueue对应的锁对象

  • 通过 synchronized 关键字,线程来抢占锁,互斥关系,从而保证了一个MessageQueue只能有一个线程并发处理

继续往下看,如果扩容了怎么办?

原来有6个分区,order_id_1的消息在MessageQueue6 中,此时扩容一倍,现在12个分区,order_id_1订单后面产生的消息可能路由到了MessageQueue8 中,同一个订单的消息分布在两个分区中,无法保证顺序。

我们能做的是,先将存量消息处理完,再扩容。如果是在线业务,可以搞个临时topic,先将消息暂时堆积,待扩容后,按新的路由规则重新发送。

顺序消息,如果某条失败了怎么办?会不会一直阻塞?

1、如果失败,不会提交消费位移,系统会自动重试(有重试上限),此时会阻塞后面的消息消费,直到这条消息处理完

2、如果这个消息达到重试上限,依然失败,会进入死信队列,可以继续处理后面的消息

2.2 Kafka 如何解决消息不丢失?

Kafka 消息框架的核心思路,通过一个高性能的MQ服务来连接生产消费两个系统,达到系统间的解耦,有很强的扩展性。

图片

你可能会有疑问,如果中间某一个环节断掉了,那怎么办?

图片

这种情况,我们称之为消息丢失,会造成系统间的数据不一致。

那如何解决这个问题?需要从生产端MQ服务端消费端,三个维度来处理

1、生产端

生产端的职责就是,确保生产的消息能到达MQ服务端,这里我们需要有一个响应来判断本次的操作是否成功。

Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)

比如,上面的代码就是通过一个Callback函数,来判断消息是否发送成功,如果失败,我们需要补偿处理。

另外,为了提升发送时的灵活性,kafka提供了多种参数,供不同业务自己选择

1.1 参数 acks

该参数表示有多少个分区副本收到消息,才认为本次发送是成功的。

  • acks=0,只要发送消息就认为成功,生产端不等待服务器节点的响应

  • acks=1,表示生产者收到 leader 分区的响应就认为发送成功

  • acks=-1,只有当 ISR 中的副本全部收到消息时,生产端才会认为是成功的。这种配置是最安全的,但由于同步的节点较多,吞吐量会降低。

1.2 参数 retries

表示生产端的重试次数,如果重试次数用完后,还是失败,会将消息临时存储在本地磁盘,待服务恢复后再重新发送。建议值 retries=3

1.3 参数 retry.backoff.m

消息发送超时或失败后,间隔的重试时间。一般推荐的设置时间是 300 毫秒。

这里要特别注意一种特殊情况,如果MQ服务没有正常响应,不一定代表消息发送失败,也有可能是响应时正好赶上网络抖动,响应超时。

图片

当生产端做完这些,一定能保证消息发送成功了,但可能发送多次,这样就会导致消息重复,这个我们后面再讲解决方案

2、MQ服务端

MQ服务端作为消息的存储介质,也有可能会丢失消息。比如:一个分区突然挂掉,那么怎么保证这个分区的数据不丢失,我们会引入副本概念,通过备份来解决这个问题。

具体可设置哪些参数?

2.1 参数 replication.factor

表示分区副本的个数,replication.factor >1 当leader 副本挂了,follower副本会被选举为leader继续提供服务。

2.2 参数 min.insync.replicas

表示 ISR 最少的副本数量,通常设置 min.insync.replicas >1,这样才有可用的follower副本执行替换,保证消息不丢失

2.3 参数 unclean.leader.election.enable

是否可以把非 ISR 集合中的副本选举为 leader 副本。

如果设置为true,而follower副本的同步消息进度落后较多,此时被选举为leader,会导致消息丢失,慎用。

图片

3、消费端

消费端要做的是把消息完整的消费处理掉。但是这里面有个提交位移的步骤。

图片

有的同学,考虑到业务处理消耗时间较长,会单独启动线程拉取消息存储到本地内存队列,然后再搞个线程池并行处理业务逻辑。这样设计有个风险,本地消息如果没有处理完,服务器宕机了,会造成消息丢失。

正确的做法:拉取消息 ---  业务处理  ---- 提交消费位移

关于提交位移,kafka提供了集中参数配置

参数  enable.auto.commit

表示消费位移是否自动提交。

如果拉取了消息,业务逻辑还没处理完,提交了消费位移但是消费端却挂了,消费端恢复或其他消费端接管该分片再也拉取不到这条消息,会造成消息丢失。所以,我们通常设置 enable.auto.commit=false,手动提交消费位移。

List<String> messages = consumer.poll();
processMsg(messages);
consumer.commitOffset();

这个方案,会产生另外一个问题,我们来看下这个图

图片

拉取了消息4~消息8,业务处理后,在提交消费位移时,不凑巧系统宕机了,最后的提交位移并没有保存到MQ 服务端,下次拉取消息时,依然是从消息4开始拉取,但是这部分消息已经处理过了,这样便会导致重复消费。

4.如何解决重复消费,避免引发数据不一致

首先,要解决MQ 服务端的重复消息。kafka 在  0.11.0 版本后,每条消息都有唯一的message id, MQ服务采用空间换时间方式,自动对重复消息过滤处理,保证接口的幂等性。

图片

但这个不能根本上解决消息重复问题,即使MQ服务中存储的消息没有重复,但消费端是采用拉取方式,如果重复拉取,也会导致重复消费,如何解决这种场景问题?

方案一:只拉取一次(消费者拉取消息后,先提交 offset 后再处理消息),但是如果系统宕机,业务处理没有正常结束,后面再也拉取不到这些消息,会导致数据不一致,该方案很少采用。

方案二:允许拉取重复消息,但是消费端自己做幂等性控制。保证只成功消费一次

关于幂等技术方案很多,我们可以采用数据表Redis缓存存储处理标识,每次拉取到消息,处理前先校验处理状态,再决定是处理还是丢弃消息。

  • 5
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码者人生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值