MQ系列2:消息中间件的技术选型
1 背景
在高并发、高消息吞吐的互联网场景中,我们经常会使用消息队列(Message Queue)作为基础设施,在服务端架构中担当消息中转、消息削峰、事务异步处理 等职能。
对于那些不需要实时响应的的业务,我们都可以放在消息队列中进行传输。下面是用户在进行系统注册的时候场景,充分体现MQ的作用
可以看到用户注册的过程步骤1+步骤2,从请求到响应总共耗时 55 ms。消息消费+短信发送的时间比较长,从上面看花了5s多,一般让消息队列服务去处理,用户静静等待短信送达即可。
消息队列中间件(简称消息中间件)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦、
弹性伸缩、冗余存储、流量削峰、异步通信、数据同步等等功能,其作为分布式系统架构中的一个重要组件,有着举足轻重的地位。
2 消息中间件的组成
Broker:消息服务器,以服务的形式运行在server端,给各个业务系统提供核心消息数据的中转服务。
Producer:消息生产者,业务的发起方,负责生产消息传输给broker。
Consumer:消息消费者,业务的处理方,负责从broker获取消息并进行业务逻辑处理
Topic:主题模块,发布/订阅模式下的消息统一汇集地,不同生产者向topic发送消息,由MQ服务器分发到不同的订阅者,实现消息的广播
Queue:队列,PTP模式下,特定生产者向特定queue发送消息,消费者订阅特定的queue完成指定消息的接收。
Message:消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输。
这边以kafka为例子,这是典型的集群模式,Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。
- producer 负责生产消息
- consumer 负责消费消息
- broker 消息服务器,提供消息核心的处理工作
- zookeeper 用于生产者和消费者的注册与发现
3 消息中间件的模式分类
PTP点对点:使用queue作为通信载体
消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。
不可重复消费,消息被消费以后,queue中不再存储,所以消息消费者不可能消费到已经被消费的消息。 Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
Pub/Sub发布订阅(广播):使用topic作为通信载体
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费,所以从1到N个订阅者都能得到这个消息的拷贝。
4 消息中间件的优势
系统解耦:交互系统之间没有直接的调用关系,只是通过消息传输,故系统侵入性不强,耦合度低。
削峰、提高系统响应时间:例如原来的一套逻辑,可将紧急重要(需要立刻响应)的业务放到该调用方法中,响应要求不高的使用消息队列,放到MQ队列中,供消费者处理。
业务的有序性处理:先来先处理,比如一个系统处理某件事需要很长一段时间,但是在处理这件事情时候,有其他人也发出了请求,可以把请求放在消息队里,一个一个来处理
为大数据处理架构提供服务:通过消息作为整合,大数据的背景下,消息队列还与实时处理架构整合,为数据处理提供性能支持。
5 消息中间件常用协议
6 丰富强大的消息中间件生态
目前开源的消息中间还是很丰富的,大家用的比较多的比如 ActiveMQ、RabbitMQ、Kafka、RocketMQ、ZeroMQ 等。
但是每个人的业务场景不一样,受限于系统的规模,业务的取舍(如延迟容忍度,死信、重试的需求,可持久化需求),并不是每一款消息中间件都能满足你的需求。
除了个别大厂会进行自研(如 阿里的Rocket MQ、滴滴的DD MQ)之外,大部分同学还是要对选型有一些思考的。各自都有各自的侧重点,选择合适自己、扬长避短无疑是最好的方式。
6.1 主流MQ介绍
下面基于受众程度,对三款主流的MQ做介绍,通过各项指标上的对比,给出我们在实际应用场景中的建议。
RabbitMQ:
采用 Erlang 语言实现的 AMQP 协议的消息中间件,起源于金融系统,广泛应用在分布式系统中,承担消息转发的职责。RabbitMQ 发展历史比较久远,影响范围比较大,被很多开发者认可,在可靠性、可用性、可扩展性、功能性方面有着非凡表现。
RocketMQ:
阿里开源的消息中间件,目前已经捐献给 Apache 基金会,它是由 Java 语言开发的,具备高吞吐量、高可用性、适合大规模分布式系统应用等特点。并且在阿里的双11、618等重要活动中经受住了考验。
Kafka:
起初是由 LinkedIn 公司采用 Scala 语言开发的一个分布式、多分区、多副本且基于 zookeeper 协调的分布式消息系统,现已捐献给 Apache 基金会。它是一种高吞吐量的分布式发布订阅消息系统,以可水平扩展和高吞吐率而被广泛使用。
目前越来越多的开源分布式处理系统如 Cloudera、Apache Storm、Spark、Flink 等都支持与 Kafka 集成。
6.2 主流MQ对比
特性
|
RabbitMQ
|
RocketMQ
|
kafka
|
开发语言
|
erlang
|
java
|
scala
|
支持协议
|
AMQP
|
自定义
|
基于TCP 自定义
|
消息存储能力
|
内存、磁盘。支持少量堆积。
|
磁盘。支持大量堆积。
|
内存、磁盘、数据库。支持大量堆积。
|
消息事务性
|
支持(信道设置事务模式,性能有影响)
|
支持
|
支持
|
单机吞吐量
|
万级
|
10万级+
|
10万级+
|
时效性
|
us级
|
ms级
|
ms级以内
|
消息重复
|
支持at least once、at most once |
支持at least once
|
支持at least once、at most once |
消息回溯
|
不支持
|
支持指定时间点的回溯
|
支持指定分区offset位置的回溯
|
消息重试
|
不支持,但可以设置autoACK=false,未收到确认的会重入队列
|
支持
|
不支持,但可以通过消息回溯的方式来实现
|
可用性
|
高(主从架构)
|
非常高(分布式架构)
|
非常高(分布式架构)
|
功能特性说明
|
基于erlang开发,所以并发能力很强,性能极其好,延时很低;
管理界面较丰富
|
MQ功能比较完备,扩展性佳
|
只支持主要的MQ功能,像一些消息查询,消息回溯等功能支持的不是很强,在大数据领域应用广。
|
6.3 选型建议
6.3.1 系统建设规模角度
中小型系统建议选用RabbitMQ,数据量相对较小,选型应首选功能比较完备的,所以kafka排除。RocketMQ是阿里出品,如果阿里放弃维护,中小型公司一般很难投入人力进行RocketMQ的定制化开发,因此不推荐。
6.3.2 业务规模角度
6.3.3 功能性层面选型
功能项 | Kafka(1.1.0+) | RabbitMQ(3.6.10+) |
优先级队列 | 不支持 | 支持:具有优先被消费的特权,建议优先级大小设置在10以内,否则价值不大 |
延迟队列 | 不支持 | 支持 |
死信队列 | 不支持 | 支持:保存无法被正确投递的消息,避免消息被无端丢弃。 |
重试模式 | 不支持 |
不支持:RabbitMQ中可以参考延迟队列实现一个重试队列,需要再封装一下,也不是太难。 如果要在kafka中实现重试队列,首先要实现延迟队列的功能,相对比较复杂。 |
消费模式 | 拉 模式 | 推+拉 模式 |
广播消费(pub/sub) | 支持:kafka对广播消费的支持比较强大 | 支持:能力相比较kafka 弱一些 |
消息回溯 | 支持:kafka可以按照 offset(偏移量)和 timestamp(时间戳) 两种维度进行消息回溯。 | 不支持:RabbitMQ消息一旦被确认消费便丢弃 |
消息堆积 | 支持 |
支持:内存堆积过大会影响性能,如果仅考虑吞吐量因素,kafka的堆积效率比RabbitMQ总体高很多。 |
持久化 | 支持 | 支持 |
消息追踪 | 不支持:消息追踪可以通过外部系统来支持,但是支持粒度肯定没有内置的细腻 |
支持:RabbitMQ中可以采用Firehose 或者 rabbitmq_tracing插件实现。
|
消息过滤 | 客户端级别的支持 | 不支持,可以定制化封装 |
多租户 | 不支持 | 支持 |
多协议支持 | 只支持自定义协议,目前几个主流版本中存在兼容性问题。 | RabbitMQ本身就是AMQP协议的实现,同时支持MQTT、STOMP等协议 |
跨语言支持 | 采用Scala和Java编写,支持多种语言的客户端 | 采用Erlang编写,支持多种语言的客户端 |
流量控制 | 支持client和user级别,可将流控配置在生产者和消费者层面 | RabbitMQ的流控基于 Credit-Based 算法,是内部被动触发的保护机制,仅用于生产者层面。 |
消息顺序性 | 支持单分区(partition)级别的顺序性,在各自的分区中排序 |
顺序性消费的条件比较苛刻,需要单线程发送、单线程消费,这样吞吐量就下来了。 而且无法使用延迟队列、优先队列等一些高级功能,所以一般不使用。 |
安全机制 |
(TLS/SSL、SASL)身份认证和(读/写)权限控制 |
与kafka相似 |
幂等性 | 单个生产者+单partition + 单会话 场景下,支持幂等性 | 不支持 |
事务性消息 | 支持 | 支持 |
- 优先级队列:可配置优先级,优先级高的消息具备优先被消费的特权,这样可以为下游服务提供不同消息级别的保证。这种模式只是在生产效率高于消费效率的时候才有效果。如果消费者的消费速度大于生产者的速度,消息中间件服务器(Broker)中没有消息堆积,就不存在对待消费数据进行优先级排序的需求了。
- 延迟队列:延迟队列会存储对应的延迟消息,延迟消息是指消息被生产后,并不马上消费,而是等待一定时间后,消费者才拿到消息进行消费。延迟队列的模式分为两种,基于消息的延迟和基于队列的延迟。
- 基于消息的延迟是指为每条消息设置不同的延迟时间,那么每当队列中有新消息进入的时候就会重新根据延迟时间排序,但是这会对性能造成很大的影响。
- 基于队列的延迟,设置不同延迟级别的队列,如 15s、30s、1m、10m 等,每个队列中消息的延迟时间都是相同的,这样不需要消耗大量性能去做延迟时间排序,每个消息都有固定的投递时间。
- 延迟队列的常用的场景有以下几种:
- 1、购买火车票提示:30分钟之内未付款,将自动取消订单!
- 2、双11网购时,距离聚划算活动开始时间还有 17小时,到时全场5折优惠。
- 死信队列:由于某些原因消息无法被正确的投递,为了确保消息不会被无故的丢弃,一般会存储到一个特殊的队列中,我们称之为死信队列。与此对应的还有一个“回退队列”的概念,试想如果消费者在消费时发生了异常,那么就不会对这一次消费进行确认(Ack), 进而发生回滚消息的操作之后消息始终会放在队列的顶部,然后不断被处理和回滚,导致队列陷入死循环。为了解决这个问题,可以为每个队列设置一个回退队列,它和死信队列都是为异常的处理提供的一种机制保障。实际情况下,回退队列的角色可以由死信队列和重试队列来扮演。
- 重试队列:重试队列其实可以看成是一种回退队列,具体指消费端消费消息失败时,为防止消息无故丢失而重新将消息回滚到 Broker 中。与回退队列不同的是重试队列一般分成多个重试等级,每个重试等级一般也会设置重新投递延时,重试次数越多投递延时就越大。比如第一次重试延迟时间为5s,再次消费失败后延迟重试时间为10s,以此类推,重试越多次重新投递的时间就越久。为了避免延迟时间被无限放大,需要有个重试次数限制,超过就写入死信队列。这边需要注意:延迟队列动作由内部触发,重试队列动作由外部消费端触发。
- 消费模式:消费模式分为推(push)模式和拉(pull)模式。推模式是指由 Broker 主动推送消息至消费端,实时性较好,不过需要保证服务端推送的消息不会严重超过消费端消化能力。而拉模式是指消费端定时定量主动向 Broker 端请求拉取消息,虽然实时性较差,但是可以根据自身的消费能力来拉取。
- 广播消费:消息一般有两种发送模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。对于P2P模式而言,消息被消费以后,队列中不会再存储,即使有多个消费者,一条消息只会被一个消费者消费。而发布订阅(Pub/Sub)模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布 / 订阅模式在消息的一对多广播时采用。RabbitMQ 是一种典型的点对点模式,而 Kafka 是一种典型的发布订阅模式。
- 消息回溯:一般消息在消费完成之后就被处理了,之后再也不能消费到该条消息。消息回溯正好相反,是指消息在消费完成之后,还能追溯到之前被消费掉的消息。
- 消息堆积 + 持久化:进行流量的削峰填谷是消息中间件的一个核心功能,实现的能力主要体现在消息堆积能力上。消息堆积分内存式堆积和磁盘式堆积。RabbitMQ 是典型的内存式堆积,可以通过一些方式持久化到磁盘中,但是会降低一些性能。Kafka 是典型的磁盘式堆积,所有的消息都存储在磁盘中,存储容量是有了很大的提升,但是磁盘性能会比内存差很多。
- 消息追踪:在消息中间件中,消息的链路追踪非常重要,它可以对生产和消费过的消息进行trace追踪。这样,在出现故障的时候,就可以快速的定位问题。
- 消息过滤:消息过滤是指按照既定的过滤规则为下游用户提供指定类别的消息。就以 kafka 而言,完全可以将不同类别的消息发送至不同的 topic 中,由此可以实现某种意义的消息过滤,或者 Kafka 还可以根据分区对同一个 topic 中的消息进行分类。不过更加严格意义上的消息过滤应该是对既定的消息采取一定的方式按照一定的过滤规则进行过滤。同样以 Kafka 为例,可以通过客户端提供的 ConsumerInterceptor 接口或者 Kafka Stream 的 filter 功能进行消息过滤。
- 流量控制:flow control,当生产者和消费者 处理速度不均衡问题,通过对生产者和消费者的限流,来保障两者的均衡。通常的流控方法有 Stop-and-wait、滑动窗口以及令牌桶等。
- 消息顺序性:顺序性是指保证消息有序,特别是分布式场景下,有序的执行,是保证一致性 (Consistency)的前提。
- 消息幂等性:对于确保消息在生产者和消费者之间进行传输而言一般有三种传输保障(delivery guarantee):At most once,至多一次,消息可能丢失,但绝不会重复传输;At least once,至少一次,消息绝不会丢,但是可能会重复;Exactly once,精确一次,每条消息肯定会被传输一次且仅一次。对于大多数消息中间件而言,一般只提供 At most once 和 At least once 两种传输保障,对于第三种一般很难做到,由此消息幂等性也很难保证。
- 事务性消息:原子性事务中的操作为一个整体,要么都做,要么都不做。即一旦出错,就回滚事务,事务是由事务开始(Begin Transaction)和事务结束(End Transaction)之间执行的全体操作组成。Kafka 和 RabbitMQ 都支持,不过仅仅指的是生产者发送消息是一个事务性操作,要么发送成功,要么发送失败。
6.3.4 性能层面
功能维度是消息中间件选型中的一个重要的参考维度,但性能也是考虑的一个重要环节。
吞吐量角度:Kafka 在开启幂等、事务功能的时候会使其性能降低,RabbitMQ 在开启 rabbitmq_tracing 插件的时候也会极大的影响其性能。消息中间件的性能一般是指其吞吐量,虽然从功能维度上来说,RabbitMQ 的优势要大于 Kafka,但是 Kafka 的吞吐量要比 RabbitMQ 高出 1 至 2 个数量级,一般 RabbitMQ 的单机 QPS 在万级别之内,而 Kafka 的单机 QPS 可以维持在十万级别,甚至可以达到百万级。
时延角度:另外一个是时延,作为性能维度的一个重要指标,却往往在消息中间件领域所被忽视,因为一般使用消息中间件的场景对时效性的要求并不是很高,如果要求时效性完全可以采用 RPC 的方式实现。消息中间件具备消息堆积的能力。Kafka是ms以内,RabbitMQ是us级别的。
6.3.5 高可用角度
高可用角度是指系统的出错概率和无故障运行时长。
如消息丢失,是使用消息中间件时所不得不面对的一个同点,其背后消息可靠性也是衡量消息中间件好坏的一个关键因素。尤其是在金融支付领域,消息可靠性尤为重要。然而说到可靠性必然要说到可用性,注意这两者之间的区别,消息中间件的可靠性是指对消息不丢失的保障程度;
而消息中间件的可用性是指无故障运行的时间百分比,通常用几个 9 来衡量,如 99.99% 就是一个不错的指标。
对应的 RabbitMQ 是通过镜像环形队列实现多副本及强一致性语义的。多副本可以保证在 master 节点宕机异常之后可以提升 slave 作为新的 master 而继续提供服务来保障可用性。
6.3.6 运维管理层面
消息中间件一个很重要的考虑层面是运维管理,比如:申请、审核、监控、告警、管理、容灾、部署等。
对消息中间件的使用 从使用、接入规范、全方位的监控、流量统计和分析等方面,提供有效的基准数据,也可以在检测到异常的情况配合告警,以便运维、开发人员的迅速介入。除了一般的监控项(比如硬件、GC 等)之外,对于消息中间件还需要关注端到端时延、消息审计、消息堆积等方面。
对于 RabbitMQ 而言,最正统的监控管理工具莫过于 rabbitmq_management 插件了,另外还有 AppDynamics, Collectd, DataDog, Ganglia 等多种优秀的产品。
Kafka 丰富的管理工具,比如:Kafka Manager, Kafka Monitor, Kafka Offset Monitor 等产品,其中 Cruise 还可以提供自动化运维的功能。
6.3.7 社区力度及生态发展
Kafka 和 RabbitMQ 都有一系列开源的监控管理产品,社区活跃,产品生态都很不错。