logo头像

生而无畏,战至终章

消息面经(持续更新)

1. 各种消息中间件之间的对比

  • ActiveMQ:老牌的消息中间件,用的较少,支撑互联网公司的高并发、高负载以及高吞吐的复杂场景
  • RabbitMQ:于可以支撑高并发、高吞吐、性能很高,同时有非常完善便捷的后台管理界面可以使用,支持集群化、高可用部署架构、消息高可靠支持,功能较为完善,但是他自身是基于 Erlang 语言开发的,所以导致较为难以分析里面的源码,也较难进行深层次的源码定制和改造,毕竟需要较为扎实的 Erlang 语言功底才可以。
  • RocketMQ:支持集群化、高可用部署架构、消息高可靠支持,功能较为完善,RocketMQ 是基于 Java 语言开发的,适合深入阅读源码
  • Kafaka:优势在于专为超高吞吐量的实时日志采集、实时数据同步、实时数据计算等场景来设计

2. 为什么要引入消息中间件,好处以及缺点

好处:

  • 系统解耦
  • 异步调用
  • 流量削峰

缺点:

  • 系统的可用性降低
  • 系统稳定性降低
  • 一致性问题

3. 如何解决重复消费

要求消息接收者的消息处理是幂等操作,如何保证幂等操作

  • MVCC
    多版本并发控制,乐观锁的一种实现,在生产者发送消息时进行数据更新时需要带上数据的版本号,消费者去更新时需要去比较持有数据的版本号,版本号不一致的操作无法成功。

  • 去重表
    利用数据库表单的特性来实现幂等

4. 大量消息积压怎么解决

如果积压了几百万到上千万的数据,即使消费者恢复了,也需要大概一小时的时间才能恢复过来
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:

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

5. 消息的堆积解决思路

  1. 设计分区数的时候,尽量设置的多点(当然也不要太大,太大影响延迟,具体可以参考我前面提到的文章),从而提升生产和消费的并行度,避免消费太慢导致消费堆积。
  2. 增大批次:瓶颈在消费吞吐量的时候,增加批次也可以改善性能
  3. 增加线程数:如果一些消费者组中的消费者线程还是有1个消费者线程消费多个分区的情况,建议增加消费者线程。尽量1个消费者线程对应1个分区,从而发挥现有分区数下的最大并行度。

6. 消息队列实现概要

RocketMQ的consumer使用一般是均衡消费的方式,比如有一个topic: hello-world,我们创建的时候分配了16个分区,而我们总共有4个consumer:A/B/C/D,那么就会每个consumer分配四个分区,比如consumer A就会分配: hello-world-1、hello-world-2、hello-world-3、hello-world-4这四个分区。
也就是说总共需要这么几个要素: topic的总分区数、consumer的总数。topic的总分区数比较简单,启动的时候直接从zk读取即可,关键是第二个consumer的总数,我们知道应用是会宕机的、扩容、缩容、网络问题等,都会影响consumer的总数,RocketMQ是采用的方式是从name server中获取,每个节点启动的时候会定时向name server发送心跳,如果一定时间内没收到心跳包就可以判定这个节点挂掉了。那么这个问题解决了,consumer启动的时候可以从name server 发送请求,获取consumer的列表。
RocketMQ的分配分区是在客户端执行的,也就是说consumer获取到全量的consumer列表后,根据一定的策略分配分区,然后开始消费。然后我们知道consumer会扩容、缩容等原因不停的在变化,那么这个时候就需要重新负载均衡,那么客户端如果知道consumer变化了呢,有两种方式:

  • broker主动通知
  • 客户端定时刷新
    那么这里就会有几个问题,由于网络延迟、每个consumer启动的时间不一样,那么consumer获取到变化的时机就会不一样,也就是说数据会不一致,导致在某段时间内 某个分区被多个客户端同时消费

7. 如何保证消息的顺序

简单且可行的办法就是:保证生产者 - MQServer - 消费者是一对一对一的关系
这样的设计虽然简单易行,但也会存在一些很严重的问题,比如:

  • 并行度就会成为消息系统的瓶颈(吞吐量不够)
  • 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。

8. Rocketmq 消息的解压缩

如果消息大小超过了 128k ,这条消息是不允许发送的。但如果消息大小超过了 4k,那么,这条消息将会被压缩。

9. RocketMQ介绍

RocketMQ 核心的四大组件:Name Server、Broker、Producer、Consumer ,每个组件都可以部署成集群模式进行水平扩展。

9.1. 生产者

生产者(Producer)负责产生消息,生产者向消息服务器发送由业务应用程序系统生成的消息。 RocketMQ 提供了三种方式发送消息:同步、异步和单向。
同步发送
同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信
异步发送
步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务
单向发送
单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集

9.2. 生产者组

生产者组(Producer Group)是一类 Producer 的集合,这类 Producer 通常发送一类消息并且发送逻辑一致,所以将这些 Producer 分组在一起。从部署结构上看生产者通过 Producer Group 的名字来标记自己是一个集群。

9.3. 消费者

消费者(Consumer)负责消费消息,消费者从消息服务器拉取信息并将其输入用户应用程序。站在用户应用的角度消费者有两种类型:拉取型消费者推送型消费者
拉取型消费者
拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
推送型消费者
推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息

9.4. 消费者组

消费者组(Consumer Group)一类 Consumer 的集合名称,这类 Consumer 通常消费同一类消息并且消费逻辑一致,所以将这些 Consumer 分组在一起。消费者组与生产者组类似,都是将相同角色的分组在一起并命名
分组是个很精妙的概念设计,RocketMQ 正是通过这种分组机制,实现了天然的消息负载均衡。消费消息时通过 Consumer Group 实现了将消息分发到多个消费者服务器实例,比如某个 Topic 有9条消息,其中一个 Consumer Group 有3个实例(3个进程或3台机器),那么每个实例将均摊3条消息,这也意味着我们可以很方便的通过加机器来实现水平扩展。

9.5. 消息服务器(Broker)

消息服务器(Broker)是消息存储中心,主要作用是接收来自 Producer 的消息并存储, Consumer 从这里取得消息。它还存储与消息相关的元数据,包括用户组、消费进度偏移量、队列信息等。从部署结构图中可以看出 Broker 有 Master 和 Slave 两种类型,Master 既可以写又可以读,Slave 不可以写只可以读。
从物理结构上看 Broker 的集群部署方式有四种:单 Master 、多 Master 、多 Master 多 Slave(同步刷盘)、多 Master多 Slave(异步刷盘)。
单 Master
这种方式一旦 Broker 重启或宕机会导致整个服务不可用,这种方式风险较大,所以显然不建议线上环境使用。
多 Master
所有消息服务器都是 Master ,没有 Slave 。这种方式优点是配置简单,单个 Master 宕机或重启维护对应用无影响。缺点是单台机器宕机期间,该机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受影响。
多 Master 多 Slave(异步复制)
每个 Master 配置一个 Slave,所以有多对 Master-Slave,消息采用异步复制方式,主备之间有毫秒级消息延迟。这种方式优点是消息丢失的非常少,且消息实时性不会受影响,Master 宕机后消费者可以继续从 Slave 消费,中间的过程对用户应用程序透明,不需要人工干预,性能同多 Master 方式几乎一样。缺点是 Master 宕机时在磁盘损坏情况下会丢失极少量消息
多 Master 多 Slave(同步双写)
每个 Master 配置一个 Slave,所以有多对 Master-Slave ,消息采用同步双写方式,主备都写成功才返回成功。这种方式优点是数据与服务都没有单点问题,Master 宕机时消息无延迟,服务与数据的可用性非常高。缺点是性能相对异步复制方式略低,发送消息的延迟会略高。

9.6. 名称服务器(NameServer)

名称服务器(NameServer)用来保存 Broker 相关元信息并给 Producer 和 Consumer 查找 Broker 信息。NameServer 被设计成几乎无状态的,可以横向扩展,节点之间相互之间无通信,通过部署多台机器来标记自己是一个伪集群。
每个 Broker 在启动的时候会到 NameServer 注册,Producer 在发送消息前会根据 Topic 到 NameServer 获取到 Broker 的路由信息,Consumer 也会定时获取 Topic 的路由信息。所以从功能上看应该是和 ZooKeeper 差不多

9.7. 消息队列

消息队列(Message Queue),主题被划分为一个或多个子主题,即消息队列。一个 Topic 下可以设置多个消息队列,发送消息时执行该消息的 Topic ,RocketMQ 会轮询该 Topic 下的所有队列将消息发出去

9.8. 消息消费模式

消息消费模式有两种:集群消费(Clustering)和广播消费(Broadcasting)
默认情况下就是集群消费,该模式下一个消费者集群共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。而广播消费消息会发给消费者组中的每一个消费者进行消费。

9.9. 消息顺序

消息顺序(Message Order)有两种:顺序消费(Orderly)和并行消费(Concurrently)
顺序消费表示消息消费的顺序同生产者为每个消息队列发送的顺序一致,所以如果正在处理全局顺序是强制性的场景,需要确保使用的主题只有一个消息队列。并行消费不再保证消息顺序,消费的最大并行数量受每个消费者客户端指定的线程池限制