Loading

消息队列对比

关于消息队列的问题

1. 你在项目中是怎么用的消息队列?

当箱体有投放行为产生的时候,箱体大屏上会第一时间反馈投放完成,箱体后台会向kafka推送一条投放信息,等待业务服务的异步处理,在服务端进行消费,并根据业务处理,插入不同的业务表中.

2. 为什么使用消息队列?

首先说说,消息队列的优势

  • 异步化(提高系统响应速度):我们举个常见的电商下单例子:在没有异步化的步骤大致是用户提交订单->预扣库存->生成订单->消费成功消息推送->通知仓库发货->数据计入财务几个串行化步骤;在串行的模式下对于整条业务链路等待时间较长,而且浪费了接口的并发带来的性能提高。如果我们从[生成订单]后就直接返回主流程,使用消息中间件后续同时处理剩余流程。既不影响整个业务的体验,而且从性能跟响应速度上有了大幅提升。

  • 缓解大流量的冲击(填谷削峰):我们在提高了系统的并行速度后还得注意一件最重要的问题就是防止系统被大流量压垮。前面我们说到使用池化技术来削峰,引入消息中间件后就能使系统平稳的处理大流量,当然这里的处理技术没有这么简单,同样需要考虑例如消息堆积与消息丢弃等问题

  • 服务解耦(提高系统稳定性):拿上面说到的下单流程,随着业务逐渐发展,应用也变得复杂起来,原来一个下单流程可能就负责在一个人手里,现在可能分别在不同的人或者团队中。那么还是使用直接相互调用的话,每个子系统在开发升级的过程中为要为了上下游系统而做出妥协与让步,或者牵扯到一条业务线的多处改动适应某一个子系统的变动,每一次上线要拉着一大帮人上线,测试整条链路,这样导致系统到最后简直忍不惨睹。在引入消息队列后通过消息队列的各种特性可以让变化透明化,不在影响上下游系统,实现了系统间的解耦。

3. 说说用消息队列都有什么优点和缺点

  1. kafka

    • 架构介绍

      最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。

      <!-- topic对应多partition,partition分布在多broker上,多broker一起提供kafka服务. partition作为kafka存储的最小粒度,主要用于topic吞吐量的扩展,partition分主和副,均匀或hash到不同的broker上,partition数量过多,占用更多broker,同时副本要求也会增加partition主副本的leader选举成本 -->

      这是一个天然的分布式消息队列,一个topic的数据分散在多个机器上,每个机器放一部分数据.

      <!--Kafka0.8以前,没有HA 机制,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读-->

      Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。

      kafka_HA

      若某个broker宕机了,该broker上的partition在其他机器上有副本,若某个patrition的leader宕了,此时会从follower中重新选举一个新的leader.

    写数据时,生产者写leader,然后leader将数据落地写本地磁盘,接着follower主动从leader拉数据,当所有follower同步完数据,会向leader发送ack,leader收到所有follwer的ack后,返回写成功的消息给生产者.<!--当然这只是其中一种模式,还可以适当调整这个行为-->

    消费数据,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到.

     

    • 优点

      提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。天然适合大数据实时计算以及日志收集.

    • 缺点

      存在重复消费,对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,可以忽略该影响.

  2. activemq

    • 优点

  • 缺点

    • 几个月才发一次版本,据说研究重心在下一代产品Apollo.基本不用考虑了

  1. rabbitmq(轻量级、迅捷)

    • 架构介绍

      不是一个分布式消息队列(仅支持冗余方式的集群),就是一个传统的消息队列,提供了一些集群、HA(High Availablity,高可用)的机制而已.rabbitmq一个queue的数据都是放在一个节点里的,镜像集群下,也就是每个节点都放这个queue的完整数据.

    • 优点

      • 更新频率较快,erlang语言天生具备高并发的特性管理方便,社区开源并活跃,可以解决常见的问题,降低调研成本,对于流量不大与10万QPS的而且研发力量不是很充足的团队使用是非常方便的,易维护易使用,速度不比其他的慢

      • 支持非常灵活的路由配置,<!--区别于其他消息中间件的设计方式就是在生产者(Producer)和队列(Queue)之间增加了一个 Exchange 模块,你可以把Exchange模块理解成一个路由交换模块,负责将生产者产生的消息根据不同的路由规则分发给不同的队列,因为它的路由规则十分灵活,而且对于自己添加自定义路由也很容易,所有你要是有这方面的需求是个不错的选择-->

    • 缺点

      • 官方的测试数据说根据机器的配置,它每秒钟最大能够支持几万到十几万条消息,从这个数据我们就能知道,因为它的设计理念中并没有处理消息堆积的组件或者方法,那么所有的消息都会直接进入队列中,当消费端处理不及时就会把消息堆积在队列中,直到把队列装满,导致性能急剧下降。

  2. rocketmq(国产,阿里开源)

  • 优点

    • 专为解决大数据而生,采用了大量的批量跟异步的设计理念,异步收发消息处理消息中,性能每秒钟能处理几十万条

    • 缺点

      • 阿里出品,一旦阿里放弃维护,中小型公司很难进行定制化开发,不推荐.

      • 消息数量不到一定数量的时候,不会触发批量操作,反而会出现消息延迟,除非你对参数进行调整,所以基本上不太适合在线业务使用

        消息队列对比与选型

        image

4. 如何保证消息队列的高可用?

 

5.消息队列丢数据了怎么处理?

一、rabbitmq丢数据

  1. 生产者丢数据

    生产者推送到kafka时,因网络或其他原因,丢数据.

    此时可以选择用rabbitmq提供的事务功能,就是生产者发送数据之前开启rabbitmq事务(channel.txSelect),然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)。但是rabbitmq事务机会降低吞吐量,因为太耗性能.

     

    所以一般是开启confirm模式

    消费者丢数据在生产者那里设置开启confirm模式后,每次写消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发

     

    事务机制和cnofirm机制最大的不同 在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息rabbitmq接收了之后会异步回调你一个接口通知你这个消息接收到了。

  2. 消费端丢数据

    消费的时候,刚消费到,还没处理,结果进程挂了,比如重启,此时rabbitmq认为消费过了,数据就丢了。

    用rabbitmq提供的ack机制,简单来说,就是关闭rabbitmq自动ack,可以通过一个api来调用,然后每次代码里确保处理完的时候,再程序里ack一把。这样的话,如果还没处理完,就没有ack,那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。

二、kafka

  1. 生产者丢数据

    生产者会不会丢数据

    如果按照上述的思路设置了ack=all,一定不会丢,要求是,你的leader接收到消息,所有的follower都同步到了消息之后,才认为本次写成功了。<!--如果没满足这个条件,生产者会自动不断的重试,重试无限次。-->

    #同步模式: ack机制能够保证数据的不丢失,如果ack设置为0,风险很大,一般不建议设置为0
    producer.type=sync
    request.required.acks=1
    #异步模式: 通过buffer来进行控制数据的发送,有两个值来进行控制,时间阈值与消息的数量阈值,如果buffer满了数据还没有发送出去,如果设置的是立即清理模式,风险很大,一定要设置为阻塞模式.结论:producer有丢数据的可能,但是可以通过配置保证消息的不丢失
    producer.type=async
    request.required.acks=1
    queue.buffering.max.ms=5000
    queue.buffering.max.messages=10000
    queue.enqueue.timeout.ms = -1
    batch.num.messages=200

    kafka某个broker宕机,然后重新选举partiton的leader时。大家想想,要是此时其他的follower刚好还有些数据没有同步,结果此时leader挂了,然后选举某个follower成leader之后,中间这部分数据就丢失了。

    所以此时一般是要求起码设置如下4个参数:

     #设置参数,保证生产者数据不丢失
    (1)给这个topic设置replication.factor参数:这个值必须大于1,要求每个partition必须有至少2个副本

    (2)在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower

    (3)在producer端设置acks=all:这个是要求每条数据,必须是写入所有replica之后,才能认为是写成功了

    (4)在producer端设置retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了
    (5)在producer端设置retry.backoff.ms(重试间隔时间)

     

  2. 消费端丢数据

    唯一可能就是消费到了这个消息,然后消费者那边自动提交了offset,让kafka以为你已经消费好了这个消息,其实你刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢了。 只要关闭自动提交offset,在处理完之后自己手动提交offset,就可以保证数据不会丢但是此时确实还是会重复消费,比如刚处理完,还没提交offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性(通俗点讲,一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错)就好了。

    <!--图解-->

    幂等性图解

     

     

6.如何保证消息的顺序性

  1. rabbitmq

    拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

  2. kafka

    写入一个partition中的数据一定是有序的,生产者在写的时候 ,可以指定一个key,比如指定订单id作为key,这个订单相关数据一定会被分发到一个partition中去。消费者从partition中取出数据的时候也一定是有序的,把每个数据放入对应的一个内存队列,一个partition中有几条相关数据就用几个内存队列,消费者开启多个线程,每个线程处理一个内存队列。

 

7. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?

面试官心理分析

你看这问法,其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办?

所以就这事儿,其实线上挺常见的,一般不出,一出就是大 case。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。

面试题剖析

关于这个事儿,我们一个一个来梳理吧,先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。

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

几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。

一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

  • 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。

  • 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。

  • 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。

  • 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。

  • 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

mq 中的消息过期失效了

假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。

这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

mq 都快写满了

如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

 

8.如果让你写一个消息队列,该如何进行架构设计

  • 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?

  • 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。

  • 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。

  • 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。

  •  

posted @ 2021-02-05 14:21  落地窗&阳光  阅读(194)  评论(0)    收藏  举报