分布式-分布式消息笔记

消息队列应用场景

消息队列

消息队列是进程之间的一种很重要的通信机制。参与消息传递的双方称为生产者和消费者,生产者和消费者可以只有一个实例,也可以集群部署。
消息体是参与生产和消费两方传递的数据,消息格式既可以是简单的字符串,也可以是序列化后的复杂文档信息。队列是消息的载体,用于传输和保存消息,它和数据结构中的队列一样,可以支持先进先出、优先级队列等不同的特性。
在这里插入图片描述

消息队列应用

消息队列可以用于系统内部组件之间的通信,也可以用于系统跟其他服务之间的交互,消息队列的使用,增加了系统的可扩展性。

系统解耦

消息队列,可以认为是在系统中隐含地加入了一个对外的扩展接口,能够方便地对业务进行解耦,调用方只需要发送消息而不用关注下游逻辑如何执行。

  • 远程服务调用,需要在其中一个调用方进行显式地编码业务逻辑;
  • 消息队列,系统之间可以更好地实现依赖倒转,这也是设计模式中的一个重要原则。

异步处理

异步化是一个非常重要的机制,在处理高并发、高可用等系统设计时,如果不需要或者限制于系统承载能力,不能立即处理消息,此时就可以应用消息队列,将请求异步化。

典型场景是流量削峰
举例:秒杀抢购的流量峰值是很高的,很多时候服务并不能承载这么高的瞬间流量,于是可以引入消息队列,结合限流工具,对超过系统阈值的请求,在消息队列中暂存,等待流量高峰过去以后再进行处理。

请求缓冲

消息队列,可以作为一个缓冲层,平滑各个业务系统之间处理性能的不同等。

数据分发

消息队列有不同的订阅模式,支持一对多的广播机制,可以用来实现数据的分发。
典型的比如关系型数据库对 binlog 订阅的处理,由于主库的 binlog 只有一份,但是下游的消费方可能包括各种文件索引、离线数据库等,这时候就可以应用消息队列来实现数据的分发。

常见的消息队列

Apache Kafka

Kafka 是 LinkedIn 开源的一个分布式消息系统,主要使用 Scala 语言开发,已经加入 Apache 顶级项目。
Kafka 集群部署时依赖 ZooKeeper 环境,相比其他的消息队列,运维成本要高很多,ZooKeeper 的引入,使得 Kafka 可以非常方便地进行水平扩展,支持海量数据的传输。
Kafka 的高吞吐率,在消息持久化写入磁盘的过程中,使用了多种技术来实现读写的高性能,包括磁盘的顺序读写、零拷贝技术等。

Apache RocketMQ

RocketMQ 使用Java语言开发,在阿里内部应用非常广泛,很多高并发的业务场景下都有 RocketMQ 的应用。

RocketMQ 经过了双十一的检验,消息传递的稳定性和可靠性都比较有保障。以消息持久化为例,Linux 文件在写入磁盘时,也就是常说的刷盘操作,因为存在缓存,可能会出现数据丢失的情况,RocketMQ 为了保证数据一致性,在写入磁盘时支持同步刷盘方式,即消息存储磁盘成功,才会返回消息发送成功的响应。

RocketMQ 在实现上有很多这种细节的设计,尽可能地保证了消息投递中的顺序一致性及可靠性,并且优化了响应时间,特别适合电商等相对复杂的业务中应用。

Apache RabbitMQ

RabbitMQ 是使用 Erlang 语言编写的一个开源消息队列,功能比较全面,支持多种消息传输的协议。

AMQP 是一个异步消息传输的网络协议,RabbitMQ 是典型实现代表,除了 AMQP,RabbitMQ 同时支持 MQTT、STOMP 等协议,Kafka 和 RocketMQ 实现的是自定义协议,实现起来灵活度更高。

除了顺序传输,RabbitMQ 还可以支持优先级队列等特性,不过,它不适合处理大数据量的消息,一旦出现消息堆积,性能下降比较快, RabbitMQ 比较适合企业级应用。

Kafka 可以在各类数据埋点中使用,比如电商营销的转化率日志收集和计算,另外,Kafka 的高性能适合应用在各类监控、大数据分析等场景。

RocketMQ 对一致性的良好保证,可以应用在电商各级业务调用的拆分中,比如在订单完成后通知用户,物流信息更新以后对订单状态的更新等。

RabbitMQ 则可以在数据迁移、系统内部的业务调用中应用,比如一些后台数据的同步、各种客服和 CRM 系统。

集群消费和广播消费

消息队列的消费模型

点对点发布订阅方式来源于消息队列的 JMS 实现标准,消息队列有不同的实现标准,比如 AMQP 和 JMS,其中 JMS(Java Message Service)是 Java 语言平台的一个消息队列规范。
AMQP 和 JMS 的区别是,AMQP 额外引入了 Exchange 的 Binding 的角色,生产者首先将消息发送给 Exchange,经过 Binding 分发给不同的队列。
AMQP 也定义了几种不同的消息模型,包括 direct exchange、topic change、headers exchange、system exchange 等。其中 direct exchange 可以类比点对点,其他的模型可以类比发布订阅。

点到点模型

生产者向一个特定的队列发布消息,消费者从该队列中读取消息,每条消息只会被一个消费者处理。

发布/订阅模型

在发布订阅模型中,消费者通过一个 Topic 来订阅消息,生产者将消息发布到指定的队列中。如果存在多个消费者,那么一条消息就会被多个消费者都消费一次。
点对点模型和发布订阅模型区别是消息能否被多次消费,发布订阅模型实现的是广播机制。如果只有一个消费者,则可以认为是点对点模型的一个特例。

Kafka 的消费模式

Kafka 集群的示意图,图中的 ZooKeeper 在 Kafka 中主要用于维护 Offset 偏移量,以及集群下的 Leader 选举,节点管理等。
在这里插入图片描述
角色:

  • Producer:消息生产者,负责发布消息到 broker。
  • Consumer:消息消费者,从 broker 中读取消息。
  • Broker:Broker 在 Kafka 中是消息处理的节点,可以对比服务器,一个节点就是一个 broker,Kafka 集群由一个或多个 broker 组成。
  • Topic:Topic 的语义和发布订阅模型中的主题是一致的,Kafka 通过 Topic 对消息进行归类,每一条消息都需要指定一个 Topic。
  • ConsumerGroup:消费组是对消费端的进一步拆分,每个消费者都属于一个特定的消费组,如果没有指定,则属于默认的消费组。

Kafka 的消费是基于 Topic 的,属于发布订阅机制,它会持久化消息,消息消费完后不会立即删除,会保留历史消息,可以比较好地支持多消费者订阅。

RocketMQ 的消费模式

RokcetMQ 是使用 Java 语言实现的,源码
在这里插入图片描述
NameServer 在 RocketMQ 集群中作为节点的路由中心,可以管理 Broker 集群,以及节点间的通信。

RocketMQ 额外支持 Tag 类型的划分。

  • Topic:在 RocketMQ 中,Topic 表示消息的第一级归属,每条消息都要有一个 Topic,一个 Group 可以订阅多个主题的消息。对于电商业务,根据业务不同,可以分为商品创建消息、订单消息、物流消息等。
  • Tag:RocetMQ 提供了二级消息分类,也就是 Tag,使用起来更加灵活。比如在电商业务中,一个订单消息可以分为订单完成消息、订单创建消息等,Tag 的添加,使得 RokcetMQ 中对消息的订阅更加方便。
  • ConsumerGroup:一个消费组可以订阅多个 Topic,这个是对订阅模式的扩展。

在 RocketMQ 中,一个 Topic 下可以有多个 Queue,正是因为 Queue 的引入,使得 RocketMQ 的集群具有了水平扩展能力。

RocketMQ 的消费模式分为集群消费广播消费,默认集群消费。

集群消费:实现了对点对点模型的扩展,任意一条消息只需要被集群内的任意一个消费者处理即可,同一个消费组下的各个消费端,会使用负载均衡的方式消费(实现消息的路由)。

  • 平均分配策略(默认)
  • 环形分配策略
  • 手动配置分配策略
  • 机房分配策略
  • 一致性哈希分配策略

广播消费:实现的是发布订阅模式,发送到消费组中的消息,会被多个消费者分别处理一次。

保证时序性

有序性可以分为业务上的有序和时间上的有序。

消息顺序消费困难

分布式的时钟问题

在分布式环境下,消息的生产者、消费者和队列存储,可能分布在不同的机器上,不同的机器使用各自的本地时钟,由于服务器存在时钟偏斜等问题,本地时间会出现不一致,所以不能用消息发送和到达的时间戳作为时序判断标准。

分布式系统下缺乏全局时钟,这就使得绝对的时间顺序实现起来更加困难。

消息发送端和消费端的集群

在目前大多数消息队列的应用中,生产者和消费者都是集群部署,通过 ProducerGroup 和 ConsumerGroup 的方式来运行。

  • 生产者如果存在多个发送实例,那么各个发送方的时间戳无法同步,所以消息发送端发送时的时序不能用来作为消息发送的有序判断。
  • 消费端可能存在多个实例,即使队列内部是有序的,由于存在消息的分发过程,不同消费实例的顺序难以全局统一,也无法实现绝对的有序消费。

消息重传等的影响

消息队列在传输消息时,可能会出现网络抖动导致的消息发送失败等,对这种场景的兼容,一般是通过进行合理地重传。消息的重传发生在什么时候是不可预知的,这也会导致消息传输出现乱序。

网络及内部并发

如果只有一个消费端或者只有一个生产端。

场景:如果单纯地依靠消息队列本身来保证,那么在跨实例的情况下,因为网络传输的不稳定会有先后顺序,以及内部消费的并发等,仍然无法实现绝对有序。

不同消息队列对顺序消费的保证

消息传输的有序性和不同的消息队列,不同业务场景,以及技术方案的实现细节等都有关系,解决消息传输的有序性,需要依赖消息队列提供对应的方式。

从消息队列角度,可以分为全局有序和局部有序。当前大部分消息队列的应用场景都是集群部署。

  • 全局有序的情况下,无法使用多分区进行性能的优化。
  • 局部有序,把业务消息分发到一个固定的分区,也就是单个队列内传输的方式,实现业务上对有序的要求。

Kafka 顺序消息

Kafka 保证消息在 Partition 内的顺序,对于需要确保顺序的消息,发送到同一个 Partition 中就可以。

  • 单分区的情况下可以天然满足消息有序性
  • 多分区,则可以通过制定的分发策略,将同一类消息分发到同一个 Partition 中。

例如,电商系统中的订单流转信息,写入 Kafka 时通过订单 ID 进行分发,保证同一个订单 ID 的消息都会被发送到同一个 Partition 中,这样消费端在消费的时候,可以保证取出数据时是有序的。

一个比较特殊的情况是消息失败重发的场景,比如同一个订单下的消息 1 和 2,如果 1 发送失败了,重发的时候可能会出现在 2 的后边,这种情况可以通过设置“max.in.flight.requests.per.connection”参数来解决,该参数可以限制客户端能够发送的未响应请求的个数,还可以在一定程度上避免这种消息乱序。

RocketMQ 顺序消息

RocketMQ 保证消息在同一个 Queue 中的顺序性,也就是可以满足队列的先进先出原则。

如果把对应一个业务主键的消息都路由到同一个 Queue 中就可以实现消息的有序传输,并且 RocketMQ 额外支持 Tag 的方式,可以对业务消息做进一步的拆分,在消费时相对更加灵活。

从业务角度保证顺序消费

消息传输的有序性是否有必要

考虑业务中,是否必须实现绝对的消息有序,或者是否必须要有消息队列这样的技术手段。

比如在一个订单状态消息流转的业务场景中,订单会有创建成功、待付款、已支付、已发货的状态,这几个状态之间是单调流动的,订单状态的更新需要保证有序性。

如果要实现的功能是根据发货的状态,进行物流通知用户的功能,实际上因为这个状态是单调不可逆向的,可以忽略订单状态的顺序,只关注最后是否已发货的状态。在这个场景下,订单状态流转虽然是要考虑顺序,但是在具体的这个功能下,实际上不需要关注订单状态消息消费的时序。

业务中如何实现有序消费

除了消息队列自身的顺序消费机制,我们可以合理地对消息进行改造,从业务上实现有序的目的。具体的方式有以下几种。

  • 根据不同的业务场景,以发送端或者消费端时间戳为准

比如在电商大促的秒杀场景中,如果要对秒杀的请求进行排队,就可以使用秒杀提交时服务端的时间戳,虽然服务端不一定保证时钟一致,但是在这个场景下,我们不需要保证绝对的有序。

  • 每次消息发送时生成唯一递增的 ID

在每次写入消息时,可以考虑添加一个单调递增的序列 ID,在消费端进行消费时,缓存最大的序列 ID,只消费超过当前最大的序列 ID 的消息。这个方案和分布式算法中的 Paxos 很像,虽然无法实现绝对的有序,但是可以保证每次只处理最新的数据,避免一些业务上的不一致问题。

  • 通过缓存时间戳的方式

这种方式的机制和递增 ID 是一致的,即当生产者在发送消息时,添加一个时间戳,消费端在处理消息时,通过缓存时间戳的方式,判断消息产生的时间是否最新,如果不是则丢弃,否则执行下一步。

消息幂等:如何保证消息不被重复消费

对业务幂等的理解

首先明确一下,幂等并不是问题,而是业务的一个特性。幂等问题体现在对于不满足幂等性的业务,在消息重复消费,或者远程服务调用失败重试时,出现的数据不一致,业务数据错乱等现象。

以 HTTP 协议为例, HTTP 协议中定义了交互的不同方法,比如 GET 和 POST,以及 PUT、DELETE 等,其中 GET、DELETE 等方法都是幂等的,而 POST 方法不是。

业务上的幂等指的是操作不影响资源本身,并不是每次读取的结果都保证一致。比如通过 GET 接口查询一条订单记录,在多次查询的时间段内,订单状态可能会有新的更新而发生变化,查询到的数据可能不同,但是读接口本身仍然是一个幂等的操作。

在业务开发中对数据的操作主要是 CRUD,即在做数据处理时的 Create、Read、Update、Delete 这几种操作。很明显,这里的 Create 操作不是幂等的,Update 操作可能幂等也可能不幂等。例如,现在有一个订单表,下面的操作就是幂等的:

UPDATE order SET status=1 WHERE id=100

下面的这个操作,就不符合幂等性的要求:

UPDATE order SET price=price+1 WHERE id=100

对应的,Read 和 Delete 操作则是幂等的。

各类中间件对幂等性的处理

幂等处理不好,可能会出现很多问题,比如使用 binlog 分发进行数据同步,如果数据库更新消息被多次消费,可能会导致数据的不一致。

  • 远程服务调用的幂等问题

因为存在网络抖动等,远程服务调用出现失败,一般是通过配置重试,保证请求调用成功率,提高整体服务的可用性。

以 Apache Dubbo 为例,它支持多种集群容错的方式,并且可以针对业务特性,配置不同的失败重试机制,包括 Failover 失败自动切换、Failsafe 失败安全、Failfast 快速失败等。比如在 Failover 下,失败会重试两次;在 Failfast 下,失败则不会重试,直接抛出异常。

Dubbo 的容错机制考虑了多种业务场景的需求,根据不同的业务场景,可以选择不同的容错机制,进而有不同的重试策略,保证业务正确性。

Dubbo 文档

  • 消息消费中的重试问题

从本质上来讲,消息队列的消息发送重试,和微服务中的失败调用重试是一样的,都是通过重试的方式,解决网络抖动、传输不稳定等导致的偶发调用失败。

在分布式系统中,需从中间件和业务的不同层面,来保证服务调用的幂等性。

消息投递的几种语义

  • At most once

消息在传递时,最多会被送达一次,在这种场景下,消息可能会丢,但绝不会重复传输,一般用于对消息可靠性没有太高要求的场景,比如一些允许数据丢失的日志报表、监控信息等。

  • At least once

消息在传递时,至少会被送达一次,在这种情况下,消息绝不会丢,但可能会出现重复传输。

绝大多数应用中,都是使用至少投递一次这种方式,同时,大部分消息队列都支持到这个级别,应用最广泛。

  • Exactly once

每条消息肯定会被传输一次且仅传输一次,并且保证送达,因为涉及发送端和生产端的各种协同机制,绝对的 Exactly once 级别是很难实现的,通用的 Exactly once 方案几乎不可能存在。

不同消息队列支持的投递方式

以 RocketMQ 为例。RocketMQ 支持 At least once 的投递语义,也就是保证每个消息至少被投递一次。是通过消费端消费的 ACK 机制来实现的:

在消息消费过程中,消费端在消息消费完成后,才返回 ACK,如果消息已经 pull 到本地,但还没有消费,则不会返回 ack 响应。

业务上如何处理幂等

消息队列是为了实现系统目标而引入的手段之一,并且分布式消息队列天然存在消费时序、消息失败重发等问题。所以要保证消息队列的消费幂等,还是要回到业务中,结合具体的设计方案解决。

天然幂等不需要额外设计

有部分业务是天然幂等的,这部分业务,允许重复调用,即允许重试,在配置消息队列时,还可以通过合理的重试,来提高请求的成功率。

利用数据库进行去重

业务上的幂等操作可以添加一个过滤的数据库,比如设置一个去重表,也可以在数据库中通过唯一索引来去重。

举一个例子,现在要根据订单流转的消息在数据库中写一张订单 Log 表,我们可以把订单 ID 和修改时间戳做一个唯一索引进行约束。

当消费端消费消息出现重复投递时,会多次去订单 Log 表中进行写入,由于我们添加了唯一索引,除了第一条之外,后面的都会失败,这就从业务上保证了幂等,即使消费多次,也不会影响最终的数据结果。

设置全局唯一消息 ID 或者任务 ID

在消息投递时,给每条业务消息附加一个唯一的消息 ID,然后就可以在消费端利用类似分布式锁的机制,实现唯一性的消费。

还是用上面记录订单状态流转消息的例子,我们在每条消息中添加一个唯一 ID,消息被消费后,在缓存中设置一个 Key 为对应的唯一 ID,代表数据已经被消费,当其他的消费端去消费时,就可以根据这条记录,来判断是否已经处理过。

高可用:如何实现消息队列的 HA?

分布式系统的可用性取决于系统中最容易出现故障,或者性能最低的组件。系统中的各个组件都要进行高可用设计,防止单点故障,消息队列也不例外。

消息队列高可用手段

分布式系统的高可用依赖副本技术,副本的引入,使得分布式系统可以更好地进行扩展,当出现某个节点宕机时,由于副本的存在,也能够快速地进行替换,提升系统整体可靠性,防止数据丢失。

Kafka 的副本机制

Kafka 的高可用实现主要依赖副本机制。

Broker 和 Partition 的关系

Broker 在是一个 Kafka 服务器节点,Kafka 集群由多个 Broker 组成,对应多个 Kafka 节点。
Kafka 是典型的发布订阅模式,存在 Topic 的概念,一个 Broker 可以容纳多个 Topic,也就是一台服务器可以传输多个 Topic 数据。

Topic 是一个逻辑概念,和物理上如何存储无关,Kafka 将一个 Topic 分散到多个 Partition 中, Partition 就是一个物理概念,对应的是具体某个 Broker 上的磁盘文件。

从 Partition 的角度,Kafka 保证消息在 Partition 内部有序,所以 Partition 是一段连续的存储,不能跨多个 Broker 存在,如果是在同一个 Broker 上,也不能挂载到多个磁盘。从 Broker 的角度,一个 Broker 可以有多个 Topic,对应多个 Partition。

Partition 可以细分为一个或者多个 Segment,也就是数据块,每个 Segment 都对应一个 index 索引文件,以及一个 log 数据文件。对 Partition 的进一步拆分,使得 Kafka 对 分区的管理更加灵活。

Replication 之间如何同步数据

如果没有副本,当某个 Kafka Broker 挂掉,或者某台服务器宕机(可能部署了多个 Broker),存储在其上的消息就不能被正常消费,导致系统可用性降低,或者出现数据丢失。

Kafka 中有一个配置参数 replication-factor(副本因子),可以调整对应分区下副本的数量,注意副本因子数包含原来的 Partition,如果需要有 2 个副本,则要配置为 3。

假设现在有一个订单的 Topic,配置分区数为 3,如果配置 replication-factor 为 3,那么对应的有三个分区,每个分区都有 3 个副本。

每个分区下配置多个副本,多个副本之间为了协调,就必须有一定的同步机制。Kafka 中同一个分区下的不同副本,有不同的角色关系,分为 Leader Replication 和 Follower Replication。Leader 负责处理所有 Producer、Consumer 的请求,进行读写处理,Follower 作为数据备份,不处理来自客户端的请求。

Follower 不接受读写请求,通过 Fetch Request 方式,拉取 Leader 副本的数据进行同步。在 Kafka 中,会为数据同步开辟一个单独的线程,称为 ReplicaFetcherThread,该线程会主动从 Leader 批量拉取数据,这样可以高性能的实现数据同步。

Replication 分配有哪些约定

Kafka 中分区副本数的配置,既要考虑提高系统可用性,又要尽量减少机器资源浪费。

为了更好地做负载均衡,Kafka 会将所有的 Partition 均匀地分配到整个集群上;
为了提高 Kafka 的系统容错能力,一个 Partition 的副本,也要分散到不同的 Broker 上,否则就去了副本的意义。

为了尽可能地提升服务的可用性和容错率,Kafka 的分区和副本分配遵循如下的原则:

一个 Topic 的 Partition 数量大于 Broker 的数量,使 Partition 尽量均匀分配到整个集群上; 同一个分区,所有的副本要尽量均匀分配到集群中的多台 Broker 上,尽可能保证同一个 分区下的主从副本,分配到不同的 Broker 上。

Leader Replication 如何选举

Leader挂掉,需要在这些副本之间选出一个新的Leader。

Kafka 数据同步中有一个 ISR(In-Sync Replicas,副本同步队列)的概念,Leader 节点在返回 ACK 响应时,会关注 ISR 中节点的同步状态,所以这个队列里的所有副本,都和 Leader 保持一致。

Kafka 的 ISR 依赖 ZooKeeper 进行管理,ISR 副本同步队列中的节点,拥有优先选举的权利,因为 ISR 里的节点和 Leader 保持一致,如果必须满足一致性,只有 ISR 里的成员才能被选为 Leader。

如果某个 Broker 挂掉,Kafka 会从 ISR 列表中选择一个分区作为新的 Leader 副本。
如果 ISR 列表是空的

  • 直接抛出 NoReplicaOnlineException 异常,保证一致性;
  • 从其他副本中选择一个作为 Leader,则可能会丢失数据,具体需要根据业务场景进行配置。

所有的副本都挂了怎么办

如果一个分区下的所有副本都挂掉了。Kafka 需要等待某个副本恢复服务,具体可以有两种方案:

  • 等待 ISR 中的某个副本恢复正常,作为新的 Leader;会保证数据不丢失,但是如果全部的 ISR 节点都彻底宕机,系统就无法对外提供服务了,对应的分区会彻底不可用。优先保证数据一致性。
  • 等待任一个副本恢复正常,作为新的 Leader。由于选择的 Leader 节点可能不是来自 ISR,所以可能会存在数据丢失,不能保证已经包含全部 Commit 的信息。优先保证服务可用性。

消息队列选型:Kafka 如何实现高性能

Kafka 的高性能

Kafka 所实现的高性能不需要太高配置的机器,它使用普通服务器就能实现 TB 级别的传输性能。

磁盘顺序读写

Kafka 对磁盘的应用,得益于消息队列的存储特性。消息队列对外提供的主要方法是生产和消费,不涉及数据的 CRUD。所以在写入磁盘时,可以使用顺序追加的方式来避免低效的磁盘寻址。

数据存储在硬盘上,而硬盘有机械硬盘和固态硬盘之分。机械硬盘成本低、容量大,但每次读写都会寻址,再写入数据(在机械硬盘上,寻址是一个物理动作,耗时最大);SSD 固态硬盘性能很高,有着非常低的寻道时间和存取时间,但成本也特别高。

为了提高在机械硬盘上读写的速度,Kafka 使用了顺序读写。在一个分区内,Kafka 采用 append 的方式进行顺序写入,这样即使是普通的机械磁盘,也可以有很高的性能。

何时进行刷盘?在 Linux 系统中,当我们把数据写入文件系统之后,其实数据是存放在操作系统的 page cache 里面,并没有刷到磁盘上,如果服务器宕机,数据就丢失了。

写到磁盘的过程叫作 Flush。刷盘一般有两种方式,

  • 依靠操作系统进行管理,定时刷盘,
  • 同步刷盘,比如调用 fsync 等系统函数。

同步刷盘保证了数据的可靠性,但是会降低整体性能。Kafka 可以配置异步刷盘,不开启同步刷盘,异步刷盘不需要等写入磁盘后返回消息投递的 ACK,所以它提高了消息发送的吞吐量,降低了请求的延时,这也是 Kafka 磁盘高性能的一个原因。

批量操作优化

Kafka 的批量包括批量写入、批量发布等。它在消息投递时会将消息缓存起来,然后批量发送;同样,消费端在消费消息时,也不是一条一条处理的,而是批量进行拉取,提高了消息的处理速度。

除了批量以外,Kafka 的数据传输还可以配置压缩协议,比如 Gzip 和 Snappy 压缩协议。虽然在进行数据压缩时会消耗少量的 CPU 资源,但可以减少网络传输的数据大小、优化网络 IO、提升传输速率。

Sendfile 零拷贝

零拷贝是操作系统文件读写的一种技术。

零拷贝不是不需要拷贝,而是减少不必要的拷贝次数,这里会涉及 Linux 用户态和内核态的区别。

用户进程是运行在用户空间的,不能直接操作内核缓冲区的数据。所以在用户进程进行系统调用的时候,会由用户态切换到内核态,待内核处理完之后再返回用户态。

传统的 IO 流程,需要先把数据拷贝到内核缓冲区,再从内核缓冲拷贝到用户空间,应用程序处理完成以后,再拷贝回内核缓冲区。这个过程中发生了多次数据拷贝。

为了减少不必要的拷贝,Kafka 依赖 Linux 内核提供的 Sendfile 系统调用。 在 Sendfile 方法中,数据在内核缓冲区完成输入和输出,不需要拷贝到用户空间处理,避免了重复的数据拷贝。在具体的操作中,Kafka 把所有的消息都存放在单独的文件里,在消息投递时直接通过 Sendfile 方法发送文件,减少了上下文切换,因此大大提高了性能。

MMAP 技术

Kafka 是使用 Scala 语言开发的。Scala 运行在 Java 虚拟机上,也就是说 Kafka 节点运行需要 JVM 的支持,但是 Kafka 并不直接依赖 JVM 堆内存。如果 Kafka 所有的数据操作都在堆内存中进行,则会对堆内存造成非常大的压力,影响垃圾回收处理,增加 JVM 的停顿时间和整体延迟。除了 Sendfile 之外,还有一种零拷贝的实现技术,即 Memory Mapped Files。

Kafka 使用 Memory Mapped Files 完成内存映射,Memory Mapped Files 对文件的操作不是 write/read,而是直接对内存地址的操作。如果是调用文件的 read 操作,则把数据先读取到内核空间中,然后再复制到用户空间。 但 MMAP 可以将文件直接映射到用户态的内存空间,省去了用户空间到内核空间复制的开销,所以说 MMAP 也是一种零拷贝技术。

MMAP 和 Sendfile 并没有本质上的区别,都是零拷贝的实现。

消息队列选型:RocketMQ 适用哪些场景?

RocketMQ 应用

RocketMQ 基于高可用分布式集群技术,提供低延时、高可靠的消息发布与订阅服务。

NameServer : RocketMQ 整体设计和其他的 MQ 类似,除了 Producer、Consumer,还有 NameServer 和 Broker。
NameServer 存储了 Topic 和 Broker 的信息,主要功能是管理 Broker,以及进行消费的路由信息管理。

服务器启动,节点会注册到 NameServer 上,通过心跳保持连接,并记录各个节点的存活状态;NameServer 还记录了生产者和消费者的请求信息,结合消息队列的节点信息,实现消息投递的负载均衡等功能。

Broker 和是消息存储的承载,作为客户端请求的入口,可以管理生产者和消费者的消费情况。

Broker 集群还承担了消息队列高可用的责任,它可以扩展副本机制,通过主从节点间的数据同步保证高可用。队列均匀分散在各个 Broker 上。在消息投递时,消息生产者通过不同的分发策略,对投递的消息进行分发,保证消息发布的均匀。

Broker 可以进行横向扩展——如果消息队列集群不能满足目前的业务场景,那么可以增加新的机器,扩展 Broker 集群。新的 Broker 节点启动以后,会注册到 NameServer 上,集群中的生产者和消费者通过 NameServer 感知到新的节点,接下来就可以进行消息的发布和消费。

Tag 是对 Topic 的进一步扩展,可以理解为一个子主题。有了 Tag,在进行消息队列的主题划分时,可以把一个业务模块的消息进一步拆分,使其更加灵活。

比如在电商业务场景中,通常我们会按照订单、商品、交易、物流等大的模块进行划分,但是在实际应用中,订单消息仍有订单创建、订单支付、订单配送等不同的消息,商品消息也会有商品价格更新、库存更新等不同的分类。使用一级主题,对消息的拆分也许不能满足业务的要求。但通过 Tag,我们可以把订单消息统一为 Order-Topic。下面继续创建 Order-Create-Message、Order-Pay-Message 等子主题,对各项信息进行细化,使其在应用中变得更加方便,在业务开发中会更加灵活。

RocketMQ 特性

实现 Binlog 分发

很多业务场景都对消息消费的顺序有很高的要求。以电商业务中的 Binlog 消息分发为例,在大多数业务场景下,除了数据库作为持久层存储以外,还会有文件索引、各类缓存的存在。

比如电商中的订单信息,订单信息在用户端的展示是通过 ElasticSearch 等文件索引实现的。在订单状态修改后,需要实时同步修改。

业务数据被分散在不同的存储中,就一定要考虑数据一致性,一个典型的解决方案是基于 Binlog 的数据同步。

使用 RocketMQ 实现 Binlog 数据同步,有一个成熟的方案,那就是 RocketMQ 结合阿里的 Canal。Canal 是阿里巴巴开源的数据库组件,可以基于 MySQL 数据库进行增量日志解析,实现增量数据订阅和消费。
Canal 的实现原理特别巧妙模拟 MySQL Slave 的交互协议,把自己作为 MySQL 主从同步中的一个从节点,拉取 Binlog 日志信息,然后进行分发。

Canal 的解决方案还包括一个可视化界面,该界面可以进行动态管理,配置 RocketMQ 集群。 Canal 的代码仓库

实现分布式一致性

在 RocketMQ 中,事务消息就是支持类似 XA 规范的分布式事务功能,通过 RocketMQ 达到分布式事务的最终一致。

以电商中的下单后扣款为例,用户在完成商品购买后,点击确认支付,这时候会调用交易模块的服务,更新账户金额,或者从第三方支付扣款。使用 TCC 进行改造,也可以使用基于消息队列的本地消息表。

RocketMQ 实现的事务消息和本地消息表类似。RokcetMQ 在事务消息的实现中添加了一个 Half Message 的概念。Half Message 表示事务消息处于未完成状态,利用这个中间状态,可以实现一个类似于两阶段提交的流程,实现最终的一致性。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值