消息队列MQ带来的一些问题、及解决方案

消息队列MQ带来的一些问题、及解决方案

RocketMQ的架构

RocketMQ由NameServer、Broker、Consumer、Producer组成,NameServer之间互不通信,Broker会向所有的nameServer注册,通过心跳判断broker是否存活,producer和consumer 通过nameserver就知道broker上有哪些topic。

Rocket:火箭。阿里巴巴双十一官方指定消息产品,支撑阿里巴巴集团所有的消息服务,历经十余年高可用与高可靠的严苛考验,是阿里巴巴交易链路的核心产品。

  • Broker :消息中转角色,负责 存储消息,转发消息。 Broker 是具体提供业务的服务器,单个Broker节点与所有的NameServer节点保持长连接及心跳,并会定时将 Topic 信息注册到NameServer,顺带一提底层的通信和连接都是 基于Netty实现 的。Broker 负责消息存储,以Topic为纬度支持轻量级的队列,单机可以支撑上万队列规模,支持消息推拉模型。官网上有数据显示:具有 上亿级消息堆积能力 ,同时可 严格保证消息的有序性 。
  • Topic :主题!它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。 Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。一个 Topic 也可以被 0个、1个、多个消费者订阅。
  • Tag :标签!可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同Topic而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。标签有助于保持您的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。
  • MessageQueue :一个Topic下可以设置多个消息队列,发送消息时执行该消息的Topic,RocketMQ会轮询该Topic下的所有队列将消息发出去。消息的物理管理单位。一个Topic下可以有多个Queue,Queue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力。
  • NameServer :类似Kafka中的ZooKeeper,但NameServer集群之间是 没有通信 的,相对ZK来说更加 轻量 。它主要负责对于源数据的管理,包括了对于 Topic 和路由信息的管理。每个Broker在启动的时候会到NameServer注册,Producer在发送消息前会根据Topic去NameServer 获取对应Broker的路由信息 ,Consumer也会定时获取 Topic 的路由信息。
  • Producer : 生产者,支持三种方式发送消息: 同步、异步和单向 单向发送 :消息发出去后,可以继续发送下一条消息或执行业务代码,不等待服务器回应,且 没有回调函数 。异步发送 :消息发出去后,可以继续发送下一条消息或执行业务代码,不等待服务器回应, 有回调函数 。同步发送 :消息发出去后,等待服务器响应成功或失败,才能继续后面的操作。
  • Consumer :消费者,支持 PUSH 和 PULL 两种消费模式,支持 集群消费 和 广播消费 集群消费 :该模式下一个消费者集群共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。广播消费 :会发给消费者组中的每一个消费者进行消费。相当于 RabbitMQ 的发布订阅模式。
  • Group :分组,一个组可以订阅多个Topic。分为ProducerGroup,ConsumerGroup,代表某一类的生产者和消费者,一般来说同一个服务可以作为Group,同一个Group一般来说发送和消费的消息都是一样的
  • Offset :在RocketMQ中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用Offset来访问,Offset为Java Long类型,64位,理论上在 100年内不会溢出,所以认为是长度无限。也可以认为Message Queue是一个长度无限的数组, Offset 就是下标。

image

rabbitmq 的架构

  • Broker :一个RabbitMQ实例就是一个Broker
  • Virtual Host :虚拟主机。 相当于MySQL的DataBase ,一个Broker上可以存在多个vhost,vhost之间相互隔离。每个vhost都拥有自己的队列、交换机、绑定和权限机制。vhost必须在连接时指定,默认的vhost是/。
  • Exchange :交换机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
  • Queue :消息队列,用来保存消息直到发送给消费者。它是消息的容器。一个消息可投入一个或多个队列。
  • Banding :绑定关系,用于 消息队列和交换机之间的关联 。通过路由键( Routing Key )将交换机和消息队列关联起来。
  • Channel :管道,一条双向数据流通道。不管是发布消息、订阅队列还是接收消息,这些动作都是通过管道完成。因为对于操作系统来说,建立和销毁TCP都是非常昂贵的开销,所以引入了管道的概念,以复用一条TCP连接。
  • Connection :生产者/消费者 与broker之间的TCP连接。
  • Publisher :消息的生产者。
  • Consumer :消息的消费者。
  • Message :消息,它是由消息头和消息体组成。消息头则包括 Routing-Key 、 Priority (优先级)等。

image

Kafka的架构

Kafka的元数据信息都是保存在Zookeeper,新版本部分已经存放到了Kafka内部了,由Broker、Zookeeper、Producer、Consumer组成。
Kafka是一个分布式、支持分区的、多副本的, 基于ZooKeeper 协调的分布式消息系统。

它最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于Hadoop的批处理系统、低延迟的实时系统、Storm/Spark流式处理引擎,Web/Nginx日志、访问日志,消息服务等等,用 Scala语言编写 。属于Apache基金会的顶级开源项目。

  • Broker :消息中间件处理节点,一个Kafka节点就是一个Broker,一个或者多个Broker可以组成一个Kafka集群

  • Topic :Kafka根据topic对消息进行归类,发布到Kafka集群的每条消息都需要指定一个topic

  • Producer :消息生产者,向Broker发送消息的客户端

  • Consumer :消息消费者,从Broker读取消息的客户端

  • ConsumerGroup :每个Consumer属于一个特定的ConsumerGroup,一条消息可以被多个不同的ConsumerGroup消费,但是一个ConsumerGroup中只能有一个Consumer能够消费该消息

  • Partition :物理上的概念,一个topic可以分为多个partition,每个partition内部消息是有序的

  • Leader :每个Partition有多个副本,其中有且仅有一个作为Leader,Leader是负责数据读写的Partition。

  • Follower :Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,Leader会把这个Follower从 ISR列表 中删除,重新创建一个Follower。

  • Offset :偏移量。Kafka的存储文件都是按照offset.kafka来命名,用Offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。

  • 可以这么来理解Topic,Partition和Broker:

一个Topic,代表逻辑上的一个业务数据集,比如订单相关操作消息放入订单Topic,用户相关操作消息放入用户Topic,对于大型网站来说,后端数据都是海量的,订单消息很可能是非常巨量的,比如有几百个G甚至达到TB级别,如果把这么多数据都放在一台机器上可定会有容量限制问题,那么就可以在Topic内部划分多个Partition来分片存储数据,不同的Partition可以位于不同的机器上,相当于 分布式存储 。每台机器上都运行一个Kafka的进程Broker。

image

总结

  • RocketMQ定位于非日志的可靠消息传输(日志场景也OK),目前RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。
  • RocketMQ的同步刷盘在单机可靠性上比Kafka更高,不会因为操作系统Crash,导致数据丢失。
  • 同时同步Replication也比Kafka异步Replication更可靠,数据完全无单点。
  • 另外Kafka的Replication以topic为单位,支持主机宕机,备机自动切换,但是这里有个问题,由于是异步Replication,那么切换后会有数据丢失,同时Leader如果重启后,会与已经存在的Leader产生数据冲突。
  • 例如充值类应用,当前时刻调用运营商网关,充值失败,可能是对方压力过多,稍后在调用就会成功,如支付宝到银行扣款也是类似需求。这里的重试需要可靠的重试,即失败重试的消息不因为Consumer宕机导致丢失。

如何保证顺序消费?

  • RabbitMQ :一个Queue对应一个Consumer即可解决。
  • RocketMQ hash(key)%队列数
  • Kafka: hash(key)%分区数

如何实现延迟消费?

  • RabbitMQ :两种方案
    死信队列 + TTL引入RabbitMQ的延迟插件
  • RocketMQ :天生支持延时消息。
  • Kafka :步骤如下
    专门为要延迟的消息创建一个Topic新建一个消费者去消费这个Topic消息持久化再开一个线程定时去拉取持久化的消息,放入实际要消费的Topic实际消费的消费者从实际要消费的Topic拉取消息。

如何保证消息的可靠性投递

RabbitMQ:

Broker-->消费者:手动ACK
生产者-->Broker:两种方案
数据库持久化:

1.将业务订单数据和生成的Message进行持久化操作(一般情况下插入数据库,这里如果分库的话可能涉及到分布式事务)

2.将Message发送到Broker服务器中

3.通过RabbitMQ的Confirm机制,在producer端,监听服务器是否ACK。

4.如果ACK了,就将Message这条数据状态更新为已发送。如果失败,修改为失败状态。

5.分布式定时任务查询数据库3分钟(这个具体时间应该根据的时效性来定)之前的发送失败的消息

6.重新发送消息,记录发送次数

7.如果发送次数过多仍然失败,那么就需要人工排查之类的操作。

image

优点:能够保证消息百分百不丢失。

缺点:第一步会涉及到分布式事务问题。

消息的延迟投递:

流程图中,颜色不同的代表不同的message

1.将业务订单持久化

2.发送一条Message到broker(称之为主Message),再发送相同的一条到不同的队列或者交换机(这条称为确认Message)中。

3.主Message由实际业务处理端消费后,生成一条响应Message。之前的确认Message由Message Service应用处理入库。

4~6.实际业务处理端发送的确认Message由Message Service接收后,将原Message状态修改。

7.如果该条Message没有被确认,则通过rpc调用重新由producer进行全过程。

image

优点:相对于持久化方案来说响应速度有所提升

缺点:系统复杂性有点高,万一两条消息都失败了,消息存在丢失情况,仍需Confirm机制做补偿。

RocketMQ

生产者弄丢数据:

Producer在把Message发送Broker的过程中,因为网络问题等发生丢失,或者Message到了Broker,但是出了问题,没有保存下来。针对这个问题,RocketMQ对Producer发送消息设置了3种方式:

同步发送
异步发送
单向发送

Broker弄丢数据:

Broker接收到Message暂存到内存,Consumer还没来得及消费,Broker挂掉了。

可以通过 持久化 设置去解决:

创建Queue的时候设置持久化,保证Broker持久化Queue的元数据,但是不会持久化Queue里面的消息
将Message的deliveryMode设置为2,可以将消息持久化到磁盘,这样只有Message支持化到磁盘之后才会发送通知Producer ack
这两步过后,即使Broker挂了,Producer肯定收不到ack的,就可以进行重发。

消费者弄丢数据:

Consumer有消费到Message,但是内部出现问题,Message还没处理,Broker以为Consumer处理完了,只会把后续的消息发送。这时候,就要 关闭autoack,消息处理过后,进行手动ack , 多次消费失败的消息,会进入 死信队列 ,这时候需要人工干预。

Kafka

生产者弄丢数据

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

Broker弄丢数据

Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。

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

replication.factor
min.insync.replicas
acks=all
retries=MAX

我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。

消费者弄丢数据

你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。

这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要 关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。 但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。

如何保证消息的幂等?

以 RocketMQ 为例,下面列出了消息重复的场景:

发送时消息重复

当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

投递时消息重复

消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RocketMQ版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启)

当消息队列RocketMQ版的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息。

那么,有什么解决方案呢? 直接上图。

image

如何解决消息积压的问题?

关于这个问题,有几个点需要考虑:

如何快速让积压的消息被消费掉?

临时写一个消息分发的消费者,把积压队列里的消息均匀分发到N个队列中,同时一个队列对应一个消费者,相当于消费速度提高了N倍。

积压时间太久,导致部分消息过期,怎么处理?

批量重导。在业务不繁忙的时候,比如凌晨,提前准备好程序,把丢失的那批消息查出来,重新导入到MQ中。

消息大量积压,MQ磁盘被写满了,导致新消息进不来了,丢掉了大量消息,怎么处理?

这个没办法。谁让【消息分发的消费者】写的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

文:一只阿木木

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章