0%

消息队列中间件简介

简介

消息队列中间件 (简称消息中间件) 是指利用高效可靠的消息传递机制进行与平台无关的数据交流, 并基于数据通信来进行分布式系统的集成. 通过提供消息传递和消息排队模型, 它可以在分布式环境下提供应用解耦, 弹性伸缩, 冗余存储, 流量削峰, 异步通信, 数据同步等等功能, 其作为分布式系统架构中的一个重要组件, 有着举足轻重的地位.

  • ActiveMQ 是 Apache 出品的, 采用 Java 语言编写的完全基于 JMS1.1 规范的面向消息的中间件, 为应用程序提供高效的, 可扩展的, 稳定的和安全的企业级消息通信. 不过由于历史原因包袱太重, 目前市场份额没有后面三种消息中间件多, 其最新架构被命名为 Apollo, 号称下一代 ActiveMQ, 有兴趣的同学可行了解.
  • RabbitMQ 是采用 Erlang 语言实现的 AMQP 协议的消息中间件, 最初起源于金融系统, 用于在分布式系统中存储转发消息. RabbitMQ 发展到今天, 被越来越多的人认可, 这和它在可靠性, 可用性, 扩展性, 功能丰富等方面的卓越表现是分不开的.
  • Kafka 起初是由 LinkedIn 公司采用 Scala 语言开发的一个分布式, 多分区, 多副本且基于 zookeeper 协调的分布式消息系统, 现已捐献给 Apache 基金会. 它是一种高吞吐量的分布式发布订阅消息系统, 以可水平扩展和高吞吐率而被广泛使用. 目前越来越多的开源分布式处理系统如 Cloudera, Apache Storm, Spark, Flink 等都支持与 Kafka 集成.
  • RocketMQ 是阿里开源的消息中间件, 目前已经捐献个 Apache 基金会, 它是由 Java 语言开发的, 具备高吞吐量, 高可用性, 适合大规模分布式系统应用等特点, 经历过双 11 的洗礼, 实力不容小觑.
  • ZeroMQ 号称史上最快的消息队列, 基于 C 语言开发. ZeroMQ 是一个消息处理队列库, 可在多线程, 多内核和主机之间弹性伸缩, 虽然大多数时候我们习惯将其归入消息队列家族之中, 但是其和前面的几款有着本质的区别, ZeroMQ 本身就不是一个消息队列服务器, 更像是一组底层网络通讯库, 对原有的 Socket API 上加上一层封装而已.

功能维度

优先级队列

优先级队列不同于先进先出队列, 优先级高的消息具备优先被消费的特权, 这样可以为下游提供不同消息级别的保证. 不过这个优先级也是需要有一个前提的: 如果消费者的消费速度大于生产者的速度, 并且消息中间件服务器 (一般简单的称之为 Broker) 中没有消息堆积, 那么对于发送的消息设置优先级也就没有什么实质性的意义了, 因为生产者刚发送完一条消息就被消费者消费了, 那么就相当于 Broker 中至多只有一条消息, 对于单条消息来说优先级是没有什么意义的.

延迟队列

当你在网上购物的时候是否会遇到这样的提示: “三十分钟之内未付款, 订单自动取消”?这个是延迟队列的一种典型应用场景. 延迟队列存储的是对应的延迟消息, 所谓“延迟消息”是指当消息被发送以后, 并不想让消费者立刻拿到消息, 而是等待特定时间后, 消费者才能拿到这个消息进行消费. 延迟队列一般分为两种: 基于消息的延迟和基于队列的延迟. 基于消息的延迟是指为每条消息设置不同的延迟时间, 那么每当队列中有新消息进入的时候就会重新根据延迟时间排序, 当然这也会对性能造成极大的影响. 实际应用中大多采用基于队列的延迟, 设置不同延迟级别的队列, 比如 5s, 10s, 30s, 1min, 5mins, 10mins 等, 每个队列中消息的延迟时间都是相同的, 这样免去了延迟排序所要承受的性能之苦, 通过一定的扫描策略 (比如定时) 即可投递超时的消息.

死信队列

由于某些原因消息无法被正确的投递, 为了确保消息不会被无故的丢弃, 一般将其置于一个特殊角色的队列, 这个队列一般称之为死信队列. 与此对应的还有一个“回退队列”的概念, 试想如果消费者在消费时发生了异常, 那么就不会对这一次消费进行确认 (Ack) , 进而发生回滚消息的操作之后消息始终会放在队列的顶部, 然后不断被处理和回滚, 导致队列陷入死循环. 为了解决这个问题, 可以为每个队列设置一个回退队列, 它和死信队列都是为异常的处理提供的一种机制保障. 实际情况下, 回退队列的角色可以由死信队列和重试队列来扮演.

重试队列

重试队列其实可以看成是一种回退队列, 具体指消费端消费消息失败时, 为防止消息无故丢失而重新将消息回滚到 Broker 中. 与回退队列不同的是重试队列一般分成多个重试等级, 每个重试等级一般也会设置重新投递延时, 重试次数越多投递延时就越大. 举个例子: 消息第一次消费失败入重试队列 Q1, Q1 的重新投递延迟为 5s, 在 5s 过后重新投递该消息;如果消息再次消费失败则入重试队列 Q2, Q2 的重新投递延迟为 10s, 在 10s 过后再次投递该消息. 以此类推, 重试越多次重新投递的时间就越久, 为此需要设置一个上限, 超过投递次数就入死信队列. 重试队列与延迟队列有相同的地方, 都是需要设置延迟级别, 它们彼此的区别是: 延迟队列动作由内部触发, 重试队列动作由外部消费端触发;延迟队列作用一次, 而重试队列的作用范围会向后传递.

消费模式

消费模式分为推 (push) 模式和拉 (pull) 模式. 推模式是指由 Broker 主动推送消息至消费端, 实时性较好, 不过需要一定的流制机制来确保服务端推送过来的消息不会压垮消费端. 而拉模式是指消费端主动向 Broker 端请求拉取 (一般是定时或者定量) 消息, 实时性较推模式差, 但是可以根据自身的处理能力而控制拉取的消息量.

广播消费

消息一般有两种传递模式: 点对点 (P2P, Point-to-Point) 模式和发布 / 订阅 (Pub/Sub) 模式. 对于点对点的模式而言, 消息被消费以后, 队列中不会再存储, 所以消息消费者不可能消费到已经被消费的消息. 虽然队列可以支持多个消费者, 但是一条消息只会被一个消费者消费. 发布订阅模式定义了如何向一个内容节点发布和订阅消息, 这个内容节点称为主题 (topic) , 主题可以认为是消息传递的中介, 消息发布者将消息发布到某个主题, 而消息订阅者则从主题中订阅消息. 主题使得消息的订阅者与消息的发布者互相保持独立, 不需要进行接触即可保证消息的传递, 发布 / 订阅模式在消息的一对多广播时采用. RabbitMQ 是一种典型的点对点模式, 而 Kafka 是一种典型的发布订阅模式. 但是 RabbitMQ 中可以通过设置交换器类型来实现发布订阅模式而达到广播消费的效果, Kafka 中也能以点对点的形式消费, 你完全可以把其消费组 (consumer group) 的概念看成是队列的概念. 不过对比来说, Kafka 中因为有了消息回溯功能的存在, 对于广播消费的力度支持比 RabbitMQ 的要强.

消息回溯

一般消息在消费完成之后就被处理了, 之后再也不能消费到该条消息. 消息回溯正好相反, 是指消息在消费完成之后, 还能消费到之前被消费掉的消息. 对于消息而言, 经常面临的问题是“消息丢失”, 至于是真正由于消息中间件的缺陷丢失还是由于使用方的误用而丢失一般很难追查, 如果消息中间件本身具备消息回溯功能的话, 可以通过回溯消费复现“丢失的”消息进而查出问题的源头之所在. 消息回溯的作用远不止与此, 比如还有索引恢复, 本地缓存重建, 有些业务补偿方案也可以采用回溯的方式来实现.

消息堆积 + 持久化

流量削峰是消息中间件的一个非常重要的功能, 而这个功能其实得益于其消息堆积能力. 从某种意义上来讲, 如果一个消息中间件不具备消息堆积的能力, 那么就不能把它看做是一个合格的消息中间件. 消息堆积分内存式堆积和磁盘式堆积. RabbitMQ 是典型的内存式堆积, 但这并非绝对, 在某些条件触发后会有换页动作来将内存中的消息换页到磁盘 (换页动作会影响吞吐) , 或者直接使用惰性队列来将消息直接持久化至磁盘中. Kafka 是一种典型的磁盘式堆积, 所有的消息都存储在磁盘中. 一般来说, 磁盘的容量会比内存的容量要大得多, 对于磁盘式的堆积其堆积能力就是整个磁盘的大小. 从另外一个角度讲, 消息堆积也为消息中间件提供了冗余存储的功能. 援引 纽约时报的案例, 其直接将 Kafka 用作存储系统.

消息追踪

对于分布式架构系统中的链路追踪 (trace) 而言, 大家一定不会陌生. 对于消息中间件而言, 消息的链路追踪 (以下简称消息追踪) 同样重要. 对于消息追踪最通俗的理解就是要知道消息从哪来, 存在哪里以及发往哪里去. 基于此功能下, 我们可以对发送或者消费完的消息进行链路追踪服务, 进而可以进行问题的快速定位与排查.

消息过滤

消息过滤是指按照既定的过滤规则为下游用户提供指定类别的消息. 就以 kafka 而言, 完全可以将不同类别的消息发送至不同的 topic 中, 由此可以实现某种意义的消息过滤, 或者 Kafka 还可以根据分区对同一个 topic 中的消息进行分类. 不过更加严格意义上的消息过滤应该是对既定的消息采取一定的方式按照一定的过滤规则进行过滤. 同样以 Kafka 为例, 可以通过客户端提供的 ConsumerInterceptor 接口或者 Kafka Stream 的 filter 功能进行消息过滤.

多租户

也可以称为多重租赁技术, 是一种软件架构技术, 主要用来实现多用户的环境下公用相同的系统或程序组件, 并且仍可以确保各用户间数据的隔离性. RabbitMQ 就能够支持多租户技术, 每一个租户表示为一个 vhost, 其本质上是一个独立的小型 RabbitMQ 服务器, 又有自己独立的队列, 交换器及绑定关系等, 并且它拥有自己独立的权限. vhost 就像是物理机中的虚拟机一样, 它们在各个实例间提供逻辑上的分离, 为不同程序安全保密地允许数据, 它既能将同一个 RabbitMQ 中的众多客户区分开, 又可以避免队列和交换器等命名冲突.

多协议支持

消息是信息的载体, 为了让生产者和消费者都能理解所承载的信息 (生产者需要知道如何构造消息, 消费者需要知道如何解析消息) , 它们就需要按照一种统一的格式描述消息, 这种统一的格式称之为消息协议. 有效的消息一定具有某种格式, 而没有格式的消息是没有意义的. 一般消息层面的协议有 AMQP, MQTT, STOMP, XMPP 等 (消息领域中的 JMS 更多的是一个规范而不是一个协议) , 支持的协议越多其应用范围就会越广, 通用性越强, 比如 RabbitMQ 能够支持 MQTT 协议就让其在物联网应用中获得一席之地. 还有的消息中间件是基于其本身的私有协议运转的, 典型的如 Kafka.

跨语言支持

对很多公司而言, 其技术栈体系中会有多种编程语言, 如 C/C++, JAVA, Go, PHP 等, 消息中间件本身具备应用解耦的特性, 如果能够进一步的支持多客户端语言, 那么就可以将此特性的效能扩大. 跨语言的支持力度也可以从侧面反映出一个消息中间件的流行程度.

流量控制

流量控制 (flow control) 针对的是发送方和接收方速度不匹配的问题, 提供一种速度匹配服务抑制发送速率使接收方应用程序的读取速率与之相适应. 通常的流控方法有 Stop-and-wait, 滑动窗口以及令牌桶等.

消息顺序性

顾名思义, 消息顺序性是指保证消息有序. 这个功能有个很常见的应用场景就是 CDC (Change Data Chapture) , 以 MySQL 为例, 如果其传输的 binlog 的顺序出错, 比如原本是先对一条数据加 1, 然后再乘以 2, 发送错序之后就变成了先乘以 2 后加 1 了, 造成了数据不一致.

安全机制

在 Kafka 0.9 版本之后就开始增加了身份认证和权限控制两种安全机制. 身份认证是指客户端与服务端连接进行身份认证, 包括客户端与 Broker 之间, Broker 与 Broker 之间, Broker 与 ZooKeeper 之间的连接认证, 目前支持 SSL, SASL 等认证机制. 权限控制是指对客户端的读写操作进行权限控制, 包括对消息或 Kafka 集群操作权限控制. 权限控制是可插拔的, 并支持与外部的授权服务进行集成. 对于 RabbitMQ 而言, 其同样提供身份认证 (TLS/SSL, SASL) 和权限控制 (读写操作) 的安全机制.

消息幂等性

对于确保消息在生产者和消费者之间进行传输而言一般有三种传输保障 (delivery guarantee) : At most once, 至多一次, 消息可能丢失, 但绝不会重复传输;At least once, 至少一次, 消息绝不会丢, 但是可能会重复;Exactly once, 精确一次, 每条消息肯定会被传输一次且仅一次. 对于大多数消息中间件而言, 一般只提供 At most once 和 At least once 两种传输保障, 对于第三种一般很难做到, 由此消息幂等性也很难保证.

Kafka 自 0.11 版本开始引入了幂等性和事务, Kafka 的幂等性是指单个生产者对于单分区单会话的幂等, 而事务可以保证原子性地写入到多个分区, 即写入到多个分区的消息要么全部成功, 要么全部回滚, 这两个功能加起来可以让 Kafka 具备 EOS (Exactly Once Semantic) 的能力.

不过如果要考虑全局的幂等, 还需要与从上下游方面综合考虑, 即关联业务层面, 幂等处理本身也是业务层面所需要考虑的重要议题. 以下游消费者层面为例, 有可能消费者消费完一条消息之后没有来得及确认消息就发生异常, 等到恢复之后又得重新消费原来消费过的那条消息, 那么这种类型的消息幂等是无法有消息中间件层面来保证的. 如果要保证全局的幂等, 需要引入更多的外部资源来保证, 比如以订单号作为唯一性标识, 并且在下游设置一个去重表.

事务性消息

事务本身是一个并不陌生的词汇, 事务是由事务开始 (Begin Transaction) 和事务结束 (End Transaction) 之间执行的全体操作组成. 支持事务的消息中间件并不在少数, Kafka 和 RabbitMQ 都支持, 不过此两者的事务是指生产者发生消息的事务, 要么发送成功, 要么发送失败. 消息中间件可以作为用来实现分布式事务的一种手段, 但其本身并不提供全局分布式事务的功能.

引用