读构建可扩展分布式系统:方法与实践06异步消息传递
1. 异步消息传递
1.1. 通信是分布式系统的基础,也是架构师需要纳入其系统设计的主要问题
1.2. 客户端发送请求并等待服务器响应
-
1.2.1. 这就是大多数分布式通信的设计方式,因为客户端需要得到即时响应后才能继续
-
1.2.2. 并非所有系统都有这个要求
1.3. 使用异步通信的方式,客户端(称为生产者)将其请求发送到中间消息传递服务
1.4. 生产者对他们发送的请求“发后即忘”(fire and forget)
-
1.4.1. 一旦请求被传递到消息传递服务,生产者就会进入其逻辑中的下一步,并确信它发送的请求最终得到处理
-
1.4.2. 消息机制提高了系统的响应能力,因为生产者不必等到请求处理完成
1.5. 异步消息传递是可扩展系统架构的一个组成部分
1.6. 消息传递机制在经历请求高峰和低谷的系统中特别有吸引力
- 1.6.1. 在高峰时段,生产者可以将请求添加到队列中并快速响应客户端,而无须等待请求被处理
1.7. 消息队列可以分布在多个代理之间以扩展消息吞吐量,也可以复制队列来提高可用性
1.8. 消息机制并非不存在风险
- 1.8.1. 将消息副本放在队列中,如果队列保留在内存中,则消息可能会丢失
1.9. 将消息副本放在队列中,如果队列保留在内存中,则消息可能会丢失
2. 消息传递简介
2.1. 异步消息传递平台是一个成熟的技术领域
-
2.1.1. 久负盛名的IBM MQ系列出现于1993年,至今仍是企业系统的中流砥柱
-
2.1.2. Java消息传递服务(JMS)是一种API级别的规范,由多个JEE供应商实现和支持
-
2.1.3. RabbitMQ,可以说是部署最广泛的开源消息传递系统
2.2. 消息传递原语
-
2.2.1. 消息队列
-
2.2.1.1. 存储一系列消息的队列
-
2.2.2. 生产者
-
2.2.2.1. 将消息发送到队列
-
2.2.2.2. 生产者将消息发送到代理上的命名队列
-
2.2.3. 消费者
-
2.2.3.1. 从队列中取出消息
-
2.2.3.2. 多个消费者可以从同一个队列中获取消息
-
2.2.3.3. 消费者获取消息有两种行为模式,即拉取或推送
> 2.2.3.3.1. 在拉取(也称为轮询)模式中,消费者向代理发送请求,代理用下一条可供处理的消息进行响应
> 2.2.3.3.2. 在推送模式下,消费者告知代理自己希望从队列中接收消息
> 2.2.3.3.2.1. 消费者提供了一个回调函数,当消息可用时应调用该函数
> 2.2.3.3.2.2. 然后消费者就会阻塞(或执行其他工作),消息代理会在有消息可用时将消息传递给回调函数进行处理
> 2.2.3.3.2.3. 使用推送模式更加高效
2.2.3.3.2.3.1. 避免了代理可能被来自多个消费者的请求压垮,并使代理能更高效地实现消息传递
2.2.3.3.2.3.2. 消费者确认后,代理就可以将消息标记为已传递,并将其从队列中删除
2.2.3.3.2.3.3. 如果使用自动确认,消息传递给消费者之后,在消息处理之前代理就会收到确认
-
2.2.4. 消息代理
-
2.2.4.1. 消息代理是一个服务,管理着一个或多个队列
-
2.2.4.2. 消息代理可以在同一硬件上管理多个队列
2.3. 通常会有消费者希望确保消息在确认之前得到完全处理
-
2.3.1. 它将使用手动确认的方式
-
2.3.2. 可以防止出现消息已经被传递给消费者,但由于消费者崩溃而导致消息未被处理的问题
-
2.3.3. 确实会增加消息确认的延迟
2.4. 无论选择何种确认模式,未确认的消息都有效地保留在队列中,并将在稍后的某个时间传递给另一个消费者处理
2.5. 消息持久化
-
2.5.1. 默认情况下,消息队列通常保存在内存中,以便为生产者和消费者提供尽可能快的服务
-
2.5.2. 只要内存充足,在内存中管理队列的开销就是最小的
-
2.5.2.1. 如果服务器崩溃,那么它确实有丢失消息的风险
-
2.5.3. 为了防止消息丢失,队列可以设置成可持久化的
-
2.5.3.1. 当生产者将消息放入队列时,只有消息写入磁盘后操作才会完成
-
2.5.3.2. 如果消息代理发生故障,在重新启动时它可以将队列内容恢复到失败前的状态,并且不会丢失任何消息
-
2.5.4. 持久队列会固有地增加发送操作的响应时间,但数据安全性却得到了提高
-
2.5.5. 代理通常会在内存和磁盘上维护队列内容,这样就能在正常操作时以最小的开销将消息发送给消费者
2.6. 发布-订阅
-
2.6.1. 在发布-订阅系统中,消息队列被称为主题
-
2.6.2. 一个主题一般都是一个消息队列,它会将每个发布的消息传递给多个订阅者之一
-
2.6.3. 发布者与订阅者分离,订阅者的数量可以动态变化
-
2.6.3.1. 须对现有系统进行任何更改即可添加新的订阅者,架构具有高度的可扩展性
-
2.6.4. 发布-订阅模式给消息代理带来了额外的性能负担
-
2.6.4.1. 利用推送的消息消费模型为发布-订阅架构提供了最有效的解决方案
-
2.6.5. 发布-订阅消息传递机制是构建分布式事件驱动架构的关键组件
-
2.6.5.1. 在事件驱动的架构中,多个服务可以使用消息代理主题发布与某些状态变更相关的事件
-
2.6.5.2. 服务可以通过订阅主题来注册感兴趣的各种事件类型
-
2.6.5.3. 该主题发布的每个事件都会发送给所有感兴趣的消费者服务
2.7. 消息复制
-
2.7.1. 在异步系统中,消息代理可能会是一个潜在的故障点
-
2.7.2. 系统或网络故障可能导致代理不可用,从而使系统无法正常运行
-
2.7.3. 大多数消息代理都允许在多个代理之间以物理方式复制逻辑队列和主题,每个代理都在自己的节点上运行
-
2.7.4. 消息队列复制的最常见方法是领导者-追随者(leader-follower)架构
-
2.7.4.1. 一个代理被指定为领导者,生产者和消费者分别通过该领导者发送和接收消息
-
2.7.4.2. 追随者被称为热备用,是领导者的副本,如果领导者发生故障,则追随者顶上
-
2.7.5. 在故障场景下,生产者和消费者可以通过切换访问追随者来继续操作,称之为故障转移
-
2.7.5.1. 故障转移在消息代理的客户端库中实现,因此对生产者和消费者来说是透明的
-
2.7.6. 实现一个可以执行队列复制的代理是一件复杂的事情
-
2.7.6.1. 不要考虑使用自己的复制方案或任何其他复杂的分布式算法
-
2.7.6.2. 你的解决方案将不如现有解决方案有效,开发成本将超出你的预期
3. RabbitMQ
3.1. 是分布式系统中使用最广泛的消息代理之一
3.2. RabbitMQ代理采用Erlang语言构建,主要为AMQP(Advanced Message Queuing Protocol,高级消息队列协议)开放标准提供支持
-
3.2.1. AMQP诞生于金融行业,致力于定义合作协议
-
3.2.1.1. 它是一种二进制协议,为执行该协议的不同产品提供互操作性
-
3.2.2. RabbitMQ支持开箱即用,支持AMQP v0-9-1,并通过插件支持v1.0
3.3. 消息、交换机和队列
-
3.3.1. 代理基于一个被称为交换机(exchange)的概念实现了一个消息传递模型,它为创建消息传递拓扑提供了一种灵活的机制
-
3.3.2. 交换是对接收生产者消息并传递给代理队列的过程的一种抽象
-
3.3.3. 直连交换机通常用于根据匹配路由键将每条消息传递到一个目标队列
3.4. 分发和并发
-
3.4.1. 通道不是线程安全的,这意味着每个线程都需要对通道进行独占访问
-
3.4.2. 线程的生命周期和调用由服务器平台控制,而不是由你的代码控制
-
3.4.3. 轮询效率很低,因为它涉及繁忙的等待,即使没有消息可用,也要求消费者不断询问消息
-
3.4.3.1. 高性能系统中不会使用这种方法
-
3.4.4. 推送模型
3.5. 与大多数消息代理一样,RabbitMQ在消费速率跟生产速率相当时表现最佳
-
3.5.1. 当队列增长到大约有数万条消息时,管理队列的线程将承受更多的开销
-
3.5.2. 默认情况下,代理使用运行节点40%的可用内存
3.6. 数据安全与性能权衡
-
3.6.1. 所有消息传递系统都面临着性能与可靠性权衡的两难境地
-
3.6.2. 核心问题是消息传递的可靠性,通常称为数据安全
3.7. 可用性与性能权衡
-
3.7.1. 单个代理发生故障属于单点故障,因此如果代理崩溃或经历短暂的网络故障,就会导致系统不可用
-
3.7.2. 高可用性的典型解决方案是代理和队列复制
-
3.7.3. RabbitMQ提供了两种支持高可用性的方法,分别是镜像队列和仲裁队列
-
3.7.3.1. 需要部署两个或多个RabbitMQ代理并配置成一个集群
-
3.7.3.2. 每个队列都有一个领导者版本,以及一个或多个追随者
-
3.7.3.3. 发布者向领导者发送消息,领导者负责将每条消息复制给追随者
-
3.7.3.4. 消费者也连接到领导者,当领导者收到消息成功处理的应答时,消息也会从追随者中删除
-
3.7.3.5. 由于所有发布者和消费者的队列行为都由领导者执行,仲裁队列和镜像队列虽然都提升了可用性,但不支持负载均衡
> 3.7.3.5.1. 消息吞吐量受到领导者副本的性能限制
- 3.7.3.6. 关键的区别在于如何复制消息,以及在领导者发生故障的情况下如何选择新的领导者
> 3.7.3.6.1. 仲裁本质上意味着超过半数
> 3.7.3.6.2. 如果有五个队列副本,那么至少需要三个副本(领导者和两个追随者)来持久保存新发布的消息
> 3.7.3.6.3. 仲裁队列实现了RAFT算法以便管理副本,并在领导者不可用时选择新的领导者
> 3.7.3.6.4. 仲裁队列必须是持久性的,因此主要适用于数据安全性和可用性优先于性能的用例
> 3.7.3.6.4.1. 在故障处理方面,它比镜像队列的实现更有优势
4. 消息传递模式
4.1. 竞争消费者
- 4.1.1. 消息传递系统的一个常见需求是尽可能快地消费队列中的消息
4.2. 可用性
- 4.2.1. 如果一个消费者发生故障,系统仍然可用,这个消费者的消息份额只是简单地分发给其他竞争消费者
4.3. 故障处理
- 4.3.1. 如果一个消费者发生故障,它未确认的消息将传递给另一个队列消费者
4.4. 动态负载均衡
- 4.4.1. 新的消费者可以在高负载期间启动并在负载减少时停止,而无须更改任何队列或消费者配置
4.5. 严格遵守一次处理原则
-
4.5.1. 在异步消息传递系统中,重复处理消息来源于两个问题
-
4.5.1.1. 第一个是来自发布者的重复发布
> 4.5.1.1.1. 一些消息代理提供了对这种重复检测的支持,从而确保重复的消息不会被发布到队列中
> 4.5.1.1.2. 利用每条消息在客户端生成的唯一幂等键值
> 4.5.1.1.2.1. 发布者只需要将特定的消息属性设置为唯一值
> 4.5.1.1.3. 代理利用缓存来存储幂等键值并检测重复项,有效地消除了队列中的重复消息,解决了第一个问题
> 4.5.1.1.4. 在消费者端,代理将消息传递给消费者后,消费者对消息进行了处理,但之后无法发送应答(消费者崩溃或网络丢失应答),就会发生重复给消费者传递消息的情况
- 4.5.1.2. 第二个是消费者多次消费
> 4.5.1.2.1. 消费者有义务防止重复处理
> 4.5.1.2.2. 为已处理的消息维护一个缓存或幂等键值数据库
> 4.5.1.2.3. 大多数代理将设置一个消息头,指示消息是否为重新传递
> 4.5.1.2.3.1. 消费者可以用它来实现幂等性
> 4.5.1.2.3.2. 它不能保证消费者已经处理了该消息
> 4.5.1.2.3.3. 它只是告诉你代理已传递过该消息并且消息仍未得到应答
- 4.5.1.3. 两者都需要解决,以确保每条消息都只处理一次
4.6. 有害消息
-
4.6.1. 最常见的可能是生产者发送了错误的消息,消费者无法处理
-
4.6.2. 导致消费者崩溃
-
4.6.2.1. 在研发和测试系统中最为常见
-
4.6.2.2. 有时这些问题也会流入生产环境,而消费者发生故障肯定会导致一些严重的运营难题
-
4.6.3. 导致消费者拒绝消息,因为它无法成功处理消息负载
-
4.6.4. 有害消息将被传递给另一个消费者,会得到可预测的、不良结果
-
4.6.4.1. 如果没办法检测到有害消息,则会无限期地传递它们
-
4.6.4.2. 最好的结果是只占用处理能力,减少系统吞吐量
-
4.6.5. 有害消息处理的方案是限制重新传递消息的次数
-
4.6.5.1. 当达到重新传递的限制时,消息会自动转移到一个收集问题请求的队列
> 4.6.5.1.1. 这个队列在传统上被称为死信队列
-
4.6.6. 每个消息传递平台实现有害消息处理的确切机制都有所不同
-
4.6.7. 有害消息处理的最后一部分是诊断出消息被重定向到死信队列的原因
-
4.6.8. 最重要的是你需要设置某种形式的监视警报给工程师发送消息处理失败的通知