消息架构的设计难题以及应对之道

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 消息架构的设计难题以及应对之道

概述


在微服务开发中我们经常会引入消息中间件实现业务解耦,执行异步操作, 现在让我们来看看使用消息中间件的好处和弊端。

首先需要肯定是使用消息组件有很多好处,其中最核心的三个是:解耦、异步、削峰。

  • 「解耦」:客户端只要讲请求发送给特定的通道即可,不需要感知接收请求实例的情况。
  • 「异步」:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度。
  • 「削峰」:消息中间件在消息被消费之前一直缓存消息,消息处理端可以按照自己处理的并发量从消息队列中慢慢处理消息,不会一瞬间压垮业务。

当然消息中间件并不是银弹,引入消息机制后也会有如下一些弊端:

  • 「潜在的性能瓶颈」:消息代理可能会存在性能瓶颈。幸运的是目前主流的消息中间件都支持高度的横向扩展。
  • 「潜在的单点故障」:消息代理的高可用性至关重要,否则系统整体的可靠性将受到影响,幸运的是大多数消息中间件都是高可用的。
  • 「额外的操作复杂性」:消息系统是一个必须独立安装、配置和运维的系统组件,增加了运维的复杂度。

这些弊端我们借助消息中间件本身提供的扩展、高可用能力可以解决,但是要真正用好消息中间件我们还需要关注可能会遇到的一些设计难题。


处理并发和顺序消息


在生产环境中为了提高消息处理的能力以及应用程序的吞吐量,一般会将消费者部署多个实例节点。那么带来的挑战就是「如何确保每个消息只被处理一次,并且是按照他们的发送顺序来处理的。」

例如:假设有3个相同的接收方实例从同一个点对点通道读取消息,发送方按顺序发布了 Order CreatedOrder UpdatedOrder Cancelled 这3个事件消息。简单的消息实现可能就会同事讲每个消息给不同的接收方。若由于网络问题导致延迟,消息可能没有按照他们发出时的顺序被处理,这将导致奇怪的行为,服务实例可能在另一个服务器处理 Order Created 消息之前处理 Order Cancelled消息。

Kafka 使用的解决方案是使用分片(分区)通道。整体解决方案分为三个部分:

  1. 一个主题通道由多个分片组成,每个分片的行为类似一个通道。
  2. 发送方在消息头部指定分片键如orderId,Kafka使用分片键将消息分配给特定的分片。
  3. 将接收方的多个实例组合在一起,并将他们视为相同的逻辑接收方(消费者组)。kafka将每个分片分配给单个接收器,它在接收方启动和关闭时重新分配分片。

顺序消息

如上图所示,每个Order事件消息都将orderId作为其分片键。特定订单的每个事件都发布到同一个分片。而且该分片中的消息始终由同一个接收方实例读取,因此这样就能够保证按顺序处理这些消息。


处理重复消息


引入消息架构必须要解决的另一个挑战是处理重复消息。在理想情况下,消息代理应该只传递一次消息,但保证消息有且仅有一次的消息传递的成本通常很高。相反,很多消息组件承诺至少保证成功传递一次消息。

在正常情况下,消息组件只会传递一次消息。但是当客户端、网络或消息组件故障可能导致消息被多次传递。假设客户端在处理消息后发送确认消息前,他的数据库崩溃了,这时消息组件将再次发送未确认的消息,在数据库重新启动时向该客户端发送。

处理重复消息有以下两种不同的方法:

  • 编写幂等消息处理程序
  • 跟踪消息并丢弃重复项

编写幂等消息处理器

如果应用程序处理消息的逻辑是满足幂等的,那么重复消息就是无害的。程序的幂等性是指,即使这个应用被相同输入参数多次重复调用时,也不会产生额外的效果。例如:取消一个已经取消的订单,就是一个幂等性操作。同样,创建一个已经存在的订单操作也必是这样。满足幂等的消息处理程序可以被放心的执行多次,只要消息组件在传递消息时保持相同的消息顺序。

但是不幸的是,应用程序通常不是幂等的。或者你现在正在使用的消息组件在重新传递消息时不会保留排序。重复或无序消息可能会导致错误。在这种情况下,你需要编写跟踪消息并丢弃重复消息的消息处理程序。


跟踪消息并丢弃重复消息

考虑一个授权消费者信用卡的消息处理程序。它必须为每个订单仅执行一次信用卡授权操作。这段应用程序每次调用时都会产生不同的效果。如果重复消息导致消息处理程序多次执行该逻辑,则应用程序的行为将不正确。执行此类应用程序逻辑的消息处理程序必须通过检测和丢弃重复消息而让它成为幂等的。

一个简单的解决方案是消息接收方使用 message id 跟踪他已处理的消息并丢弃任何重复项。例如,在数据库表中存储它消费的每条消息的 message id。

丢弃重复消息

当接收方处理消息时,它将消息的 message id 作为创建和变更业务实体的事务的一部分记录在数据表里。如上图所示,接收方将包含message id 的行插入 PROCESSED_MESSAGE表。如果消息是重复的,则INSERT将失败,接收方可以选择丢弃该消息。

另一个解决方案是消息处理程序在应用程序表,而不是专门表中记录 message id。当时用具有受限事务模型的NoSQL数据库时,此方法特别有用,因为 NoSQL数据库通常不支持将针对两个表的更新作为数据库事务。


处理事务性消息


服务通常需要在更新数据库的事务中发布消息,数据库更新和消息发送都必须在事务中进行,否则服务可能会更新数据库然后在发送消息之前崩溃。

如果服务不以原子方式执行者两个操作,则类似的故障可能使系统处于不一致状态。

接下来我们看一下常用的保证事务消息的两种解决方案,最后再看看现代消息组件RocketMQ的事务性消息解决方案。


使用数据库表作为消息队列

如果你的应用程序正在使用关系型数据库,要保证数据的更新和消息发送之间的事务可以直接使用「事务性发件箱模式,Transactional Outbox」

事务性发件箱

此模式使用数据库表作为临时消息队列。如上图所示,发送消息的服务有个OUTBOX数据表,在进行INSERT、UPDATE、DELETE 业务操作时也会给OUTBOX数据表INSERT一条消息记录,这样可以保证原子性,因为这是基于本地的ACID事务。

OUTBOX表充当临时消息队列,然后我们在引入一个消息中继(MessageRelay)的服务,由他从OUTBOX表中读取数据并发布消息到消息组件。

消息中继的实现可以很简单,只需要通过定时任务定期从OUTBOX表中拉取最新未发布的数据,获取到数据后将数据发送给消息组件,最后将完成发送的消息从OUTBOX表中删除即可。


使用事务日志发布事件

另外一种保证事务性消息的方式是基于数据库的事务日志,也就是所谓的数据变更捕获,Change Data Capture,简称CDC。

一般数据库在数据发生变更的时候都会记录事务日志(Transaction Log),比如MySQL的binlog。事务日志可以简单的理解成数据库本地的一个文件队列,它主要记录按时间顺序发生的数据库表变更记录。

这里我们利用alibaba开源的组件canal结合MySQL来说明下这种模式的工作原理。

更多操作说明可以参考官方文档:https://github.com/alibaba/canal

canal工作原理

  • canal 模拟 MySQL slave 的交互协议,把自己伪装成一个MySQL的 slave节点 ,向 MySQL master 发送dump 协议;
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );
  • canal 解析 binary log 对象(原始为 byte 流),然后可以将解析后的数据直接发送给消息组件。

RocketMQ事务消息解决方案

Apache RocketMQ在4.3.0版中已经支持分布式事务消息,RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示。

RocketMQ事务消息

RocketMQ实现事务消息主要分为两个阶段:正常事务的发送及提交、事务信息的补偿流程。

整体流程为:

  • 正常事务发送与提交阶段 1、生产者发送一个半消息给MQServer(半消息是指消费者暂时不能消费的消息) 2、服务端响应消息写入结果,半消息发送成功 3、开始执行本地事务 4、根据本地事务的执行状态执行Commit或者Rollback操作
  • 事务信息的补偿流程 1、如果MQServer长时间没收到本地事务的执行状态会向生产者发起一个确认回查的操作请求 2、生产者收到确认回查请求后,检查本地事务的执行状态 3、根据检查后的结果执行Commit或者Rollback操作 补偿阶段主要是用于解决生产者在发送Commit或者Rollback操作时发生超时或失败的情况。

在生产者使用RocketMQ发送事务消息的时候我们也会借鉴第一种方案即自建一张事务日志表,然后在执行本地事务的时候同时生成一条事务日志记录,让本地事务与日志事务在同一个方法中,同时添加 @Transactional 注解,保证两个操作事务是一个原子操作。

「这样如果事务日志表中有这个本地事务的信息,那就代表本地事务执行成功,需要Commit,相反如果没有对应的事务日志,则表示没执行成功,需要Rollback。」

感兴趣的同学可以翻看这篇文章:RocketMQ进阶-事务消息


如果本文对你有帮助,别忘记来个三连:点赞,转发,评论咱们下期见!

收藏 等于白嫖点赞 才是真情!

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
7月前
|
消息中间件 缓存 NoSQL
谈谈高并发系统的设计方法论
设计 `高并发` 系统,就是要让该系统保证它 `整体可用` 的同时,能够尽可能多的 `处理很高的并发用户请求`,能够 `承受很大的负载流量冲击`。
792 6
|
6月前
|
缓存 运维 负载均衡
分布式系统架构设计与挑战
【6月更文挑战第29天】分布式系统架构设计涉及组件化、通信、数据一致性、负载均衡和容错处理,旨在实现高可用性和可扩展性。然而,数据一致性、性能、可伸缩性和容错是主要挑战。解决方案包括使用一致性算法、性能优化、设计可伸缩架构和实施容错机制。随着技术进步,这类系统将继续在云、大数据和物联网中扮演关键角色。
|
5月前
|
供应链 Java 中间件
软件架构一致性问题之研发新产品创造价值如何解决
软件架构一致性问题之研发新产品创造价值如何解决
41 0
|
存储 NoSQL 关系型数据库
重构之道:揭秘大规模系统重构的经验与挑战
重构之道:揭秘大规模系统重构的经验与挑战
1079 2
|
存储 缓存 NoSQL
【分布式技术专题】「架构实践于案例分析」盘点高并发场景的技术设计方案和规划
【分布式技术专题】「架构实践于案例分析」盘点高并发场景的技术设计方案和规划
357 0
【分布式技术专题】「架构实践于案例分析」盘点高并发场景的技术设计方案和规划
|
数据采集 存储 编解码
在架构师的角度去看大型网站架构面临的挑战:技术架构的基本思路
技术架构的基本思路 技术架构既要清晰地划分功能模块或子系统,又要对整个网站系统的技术逻辑有清晰的认知。庞大的技术架构确实会让人望而却步,架构设计也变得无从入手。 如果把一个庞大的技术架构分成独立的几部分,然后再逐一深入的话,那么一个庞大的技术架构也不是不可理解的
|
敏捷开发 架构师 项目管理
架构师才能看懂的大型网站架构面临的挑战:业务架构的基本思路
业务架构的基本思路 大型网站系统有很多功能,一次性明确所有的功能需求并设计出一个庞大的业务架构是一件费力不讨好的事情。因为在项目前期,难免会忽视一些琐碎功能,而随着开发的进行,也会有很多新的想法产生,基本上不会存在完全按照最初的业务架构设计完成的软件产品。因此,业务架构不仅要做到“规整功能模块,厘清产品业务逻辑”,更重要的是如何做到“有规划性地应对项目过程中的需求变更”。
|
存储 缓存 监控
谈谈我们是如何做技术保障
面对业务迅速增长复杂度会呈几何级增加,为了降低维护复杂度而引入了微服务,只要每个服务足够简单,那么维护成本也可以降低。 服务保障也是一个非常困难的事情,今天聊一聊系统稳定性方案。
1615 1
谈谈我们是如何做技术保障
|
数据可视化 架构师 前端开发
复杂性应对之道 - 领域建模
复杂性应对之道 - 领域建模
复杂性应对之道 - 领域建模
|
运维
高并发系统设计之道(一)- 方法论(下)
高并发系统设计之道(一)- 方法论(下)
119 0