
消息系统在现代分布式架构中扮演关键角色:建立可靠通信管道、支撑异步处理流程,以及实现组件间松耦合。此外,消息系统也可提升应用的可用性,应对流量峰值。目前的消息代理有基于流处理的也有队列类型的服务,这些各有其优缺点。
根据作者和多个工程团队的合作经验,消息代理的选型往往缺乏系统的方法论支持。这方面的决策往往受技术潮流的风向、个人喜好或特定技术的便利性驱动,而不是基于应用场景的客观需求。但科学的选型应该是将代理的技术特性与业务需求相匹配,这也正是本文的关注重点。
我们将深入剖析两大主流方案:Apache Kafka(流式架构)与亚马逊 SQS(队列架构),这二者也正是 EarnIn 主要采用的两大消息代理。通过探讨它们的技术特性与常见消息模式的适配度,本文将为读者构建一个决策的参考框架;在掌握了这一分析范式后,开发者将能更好地评估其他消息场景和代理方案,最终能选定与业务需求契合度最高的技术方案。
消息代理
本章节将聚焦行业主流消息代理方案,横向对比其核心特性,在理解了不同消息代理间差异后,我们才能判断哪些代理技术更加适配现代应用中的典型消息模式。需要说明的是,本文中不涉及各类代理技术的深度原理解析,不熟悉相关技术的读者朋友可以参考官方文档以获取更详细的信息。
亚马逊 SQS(简单队列服务)
亚马逊 SQS 作为一款全托管消息队列服务,有效地简化分布式系统中解耦组件间的通信流程,该服务在保障消息可靠传递的同时,将基础设施管理、弹性扩展及错误处理等复杂逻辑完全封装。其核心特性如下:
消息生命周期管理机制:SQS 支持单条消息或最多十条的小批量消息管理。每条消息可根据业务需求执行接收、处理、删除或延迟操作。一般来说,应用接收消息,完成处理后会将其从队列中删除,这一机制确保了消息处理的可靠性。
尽力有序性保障:标准队列虽然是按发送顺序投递消息,但却无法保证消息严格的顺序性,尤其在重试或并行消费场景下。这种设计可以在非严格顺序需求场景下允许更高吞吐量,但若业务需求严格的顺序保障,可选择 FIFO SQS(先进先出队列)从而实现特定顺序的消息处理(详见更多有关 FIFO 队列解析)。
内置死信队列(DLQ)支持:SQS 原生集成死信队列功能,可自动隔离无法处理的异常消息,避免其对正常消息流造成阻塞。
读写吞吐能力:该服务提供近乎无限的读写吞吐量,特别适合需要高效处理海量消息的高并发场景,确保系统在流量激增时仍保持稳定。
消费者自动扩缩:SQS 支持基于队列深度的自动扩缩资源计算(如 AWS Lambda、EC2 或 ECS 服务),具体可参见官方文档。消费者可根据队列消息数量动态扩缩容:流量高峰时自动扩容,负载降低时自动缩容。这种全自动的弹性能力让系统能够自主应对各类无规律的流量波动而无需人工干预。
发布-订阅(pub-sub)支持:虽然 SQS 本身采用点对点通信模型(每条消息仅被单一消费者处理),不支持“发布-订阅”模式,但通过与亚马逊 Simple Notification Service(SNS)集成就可实现发布-订阅架构:消息发布至 SNS 主题后,可同时分发至多个订阅该主题的 SQS 队列,使不同消费者能独立处理同一消息,从而基于 AWS 服务栈构建完整的发布-订阅系统。
亚马逊 FIFO SQS
FIFO SQS 在标准 SQS 基础上进行了功能上的增强,通过“消息组”的逻辑分区机制确保了消息严格有序的处理。该服务特别适用于需要保持事件顺序的情况,例如用户个人通知流、金融交易处理等对消息顺序有严格要求的场景。
消息组分区的逻辑隔离机制:FIFO SQS 为每条消息设置了 MessageGroupId 标识,并通过该标识在队列内创建逻辑分区。相同 MessageGroupId 的消息组将严格按序处理,而不同消息组之间则可并行消费。以用户行为处理场景为例(如用户触发的一系列通知或动作),为每个用户分配独立 MessageGroupId 后,SQS 能保证该用户的所有关联消息严格按照入队顺序处理,同时其他用户的消息(不同 MessageGroupId)仍可并行处理;在保障了系统高吞吐量的同时又可确保用户相关消息的顺序性。相较于标准 SQS 或 Apache Kafka、Amazon Kinesis 等流式消息代理,FIFO SQS 在这方面体现出了其独特的价值。
死信队列(DLQ):FIFO SQS 虽然内置了死信队列支持,但使用过程却还是需要极其小心,因其可能会破坏消息组的严格顺序性。比如说同属一个 MessageGroupId
的两条消息message1
和 message2
先后入队时,如果 message1
处理失败转入 DLQ,message2
仍可被成功处理。这种情况将破坏消息组内的顺序性保障,违背 FIFO 队列的核心设计目标。
毒丸消息隔离:在未启用 DLQ 时,FIFO SQS 会无限重试失败的消息。虽然这也是维持了严格的顺序性,但却会带来连锁的阻塞反应:同一消息组内的所有后续消息的处理都将无法进行,直到该失败消息最终成功处理或被手动删除。反复处理失败的消息被称作是“毒丸消息”,在部分消息系统中,毒丸消息可阻塞整个队列或分区,导致后续所有消息都无法被处理。但是在 FIFO SQS 中,毒丸的影响范围被限制在单一的消息组(逻辑分区)中,只要消息组的设计合理,这种隔离机制就能显著降低故障的影响范围。
若要最大限度地减少影响,关键在于 MessageGroupId 的合理选择:既要保证逻辑分区足够小,还要确保需求有序处理的消息处于同一分区中。举例来说,多用户应用中使用 user ID 作为 MessageGroupId 可确保这类故障只会影响当前用户相关的消息;在电商应用中,使用订单 ID 作为 MessageGroupId 可以确保失败订单不会影响其他客户的订单。
通过以下场景我们可以直观地看出毒丸隔离的效果:
无隔离或仅分区级隔离时,一个毒丸消息可能阻塞区域内的所有订单(比如某国的全部亚马逊订单)。
采用 FIFO SQS 隔离时,仅单个用户的订单会受影响,其他订单仍可正常处理。
因此,毒丸消息隔离是 FIFO SQS 的一重要特性,显著提升了分布式消息系统的容错能力。
吞吐量:FIFO SQS 的默认吞吐量限制为每秒 300 条消息,启用高吞吐量模式则能将这一限制提高到每秒九千条消息。要想能达到如此之高的吞吐量,就需要仔细设计消息组从而实现足够的并行处理能力。
消费者自动扩缩:与标准 SQS 相类似,FIFO SQS 也支持根据队列内消息数量自动扩缩计算资源。虽然 FIFO SQS 的扩展性和消息组(逻辑分区)数量挂钩,并非是真正的无限制,但也可以通过设计非常多的消息组来提升其能力(比如为每个用户都分配一个消息组)。
发布-订阅支持:与标准 SQS 类似,FIFO SQS 也可通过搭配支持 FIFO topic 的 SNS 来实现发布-订阅功能。
Apache Kafka
Apache Kafka 是一款开源的分布式流处理平台,专为实时事件流处理和高吞吐应用而设计。与 SQS 等传统消息队列不同,Kafka 是采用基于流的处理模式,消费者通过 offset(偏移量)来追踪消费的进度。在 Kafka 中,消费者可以向前移动 offset(或向后移动以实现消息重放),也可批量提交多条消息。这种基于 offset 的机制是 Kafka 和传统消息队列的关键区别,后者往往需要独立处理和确认每一条消息,以下是 Kafka 的核心特性:
物理分区机制:Kafka 的主题(topic)在创建时就被划分为多个物理分区(也称“分片”),每个分区维护独立的 offset 且自主管理消息顺序。虽然 Kafka 支持动态增加分区,但这种操作可能会破坏消息的顺序性,需谨慎处理。减少分区则更为复杂且通常来说应当避免此操作,因为这会影响数据的分布和消费者负载均衡。考虑到分区策略可直接影响系统的扩展性和性能,这点应当在设计初期就进行仔细归规划。
原生发布-订阅支持:Kafka 原生支持发布-订阅模型,允许多个消费者组独立消费同一主题,让不同应用或服务能互不干扰地处理相同数据。每个消费组都能有独立的主题视图,这种设计为消息生产者和消费者提供了灵活的扩展能力。
高吞吐量和批量处理:Kafka 针对高吞吐量场景进行了专门的优化,使其能够高效地处理海量数据。通过合并数据库写入和 offset 提交操作,消费者可以批量处理消息从而显著降低系统开销(如单次处理上万条消息)。这种批量处理机制是流式系统与队列系统的本质区别,后者往往需要逐条或小批量处理消息。
消息回放能力:Kafka 会按配置保留消息(默认保留七天),允许消费者通过回退 offset 实现消息回放。这一特性在调试、历史数据重新处理或应用故障恢复等场景尤为实用;消费者可以自主控制处理进度,必要时也能进行消息重试,这让 Kafka 极其适合需要数据具备持久化和容错能力的场景。
毒丸消息处理:在 Kafka 中,毒丸消息会阻塞其所在的全部物理分区,导致该分区内的所有消息处理延迟。以电商系统为例,若是按照地区划分 Kafka 分片,那么单条毒丸消息就可能会导致整个地区的订单处理停滞,从而造成重大业务影响。这种设计缺陷凸显出了严格物理分区和队列系统(如 FIFO SQS)逻辑分区的关键差异:后者能将故障隔离在更小的消息组范围内。如果对严格的消息顺序没有要求,则可以通过死信队列隔离毒丸消息,避免其阻塞分区内所有的消息处理。
自动扩缩限制:Kafka 的扩展性受其分区模型的限制,每个分区(分片)需保持严格的顺序性,且同一时间只能由一个计算节点处理。也就是说,增加的计算节点若是超过了分区数量,那么额外的节点将处于闲置状态,吞吐量无法得到提升。因此,Kafka 与自动扩缩消费者模式的适配性较差,活跃的消费者数量实际受限于分区数量。相较于 FIFO SQS 等采用逻辑分区的消息系统,Kafka 无法通过细粒度分区实现更为灵活的消费者扩展,在动态扩缩场景中显得较为局限。
消息代理对比
消息模式与其对代理选型的影响
在分布式系统中,消息模式决定了服务之间的通信和信息处理方式,不同模式对消息的顺序性、可扩展性、错误处理或并行性等都有不同的要求,这些要求直接影响了消息代理的选择。本文中将重点分析三种典型消息模式:命令模式(Command Pattern)、事件携带状态转移(ECST)和事件通知模式(Event Notification Pattern),并探讨这些模式与亚马逊 SQS、Apache Kafka 等主流消息代理的适配性,这类型的分析框架同样适用于其他消息模式的选型分析。
命令模式
命令模式是一种将请求或操作封装为独立对象的设计范式,这些命令对象通过消息代理进行异步处理,允许发送方无需等待相应便可继续执行操作。
通过命令的持久化和自动重试机制,这种设计模式确保了系统的可靠性。即使消费者暂时不可用,生产者也能持续运行。此外,消费者还可以根据自身能力调节消费速率,从容应对流量峰值。
需要注意的是,命令处理通常包含了复杂的业务逻辑,其中可能设计数据库事物、外部 API 调用等操作,因此其实现需要有可靠的并行处理、自动扩缩,以及完善的毒丸消息处理能力。
核心特性
多来源、单一目的地:命令可由一个或多个服务产生,但通常仅由单个服务消费。每条命令通常只需处理一次,多个消费者节点通过竞争机制获取命令。因此,命令模式通常不需要发布-订阅功能支持。
高吞吐量:多个生产者可以高速率生成命令,因此所选的消息代理必须能支持高吞吐量和低延迟,才能确保命令的生成不会成为上游服务的性能瓶颈。
消费者自动扩缩:命令处理在消费端往往涉及耗时操作(如数据库写入和外部 API 调用),为避免资源争用,命令的并行处理至关重要。消息代理选择方面需能支持消费者并行获取命令并独立处理,不受少量并行工作流(如物理分区)的限制。通过这种水平扩展能力,系统可根据命令吞吐量的波动进行动态调整,在高峰期间增加消费者实例以应对需求,在低活跃期间缩减规模以优化资源利用率。
毒丸消息风险:命令的处理通常涉及复杂的工作流程和网络调用,这会增加处理失败的可能性,进而增加毒丸消息的可能性。为缓解此风险,消息代理必须要能支持高基数的毒丸隔离机制,确保失败的消息仅会影响一部分命令,而不会破坏整体系统的运行。通过将毒丸消息隔离到独立的消息组或分区中,系统可以在保障可靠性的同时,继续高效处理其他未受影响的命令。
消息代理适配性
考虑到并行消费、自动扩缩和毒丸隔离等需求,Kafka 并不适合处理命令:正如前文所述,Kafka 的物理分区数量固定且无法动态扩展。此外,一条毒丸消息就有可能阻塞整个物理分区,从而影响大量用户。
如果对顺序没有要求,标准 SQS 是消费和处理命令的绝佳选择:支持无限吞吐量的并行消费、动态扩缩,还能通过死信队列(DLQ)隔离毒丸消息。
对于需保证顺序和划分到多个逻辑分区的情况,FIFO SQS 是不错的方案。通过策略性地选择消息组 ID 来创建大量小型逻辑分区,系统可以实现近乎无限的并行性和吞吐量。此外,任何毒丸消息也只会影响单个逻辑分区(例如应用内的一个用户),确保其影响被隔离到最小。
事件携带状态转移(ECST)
事件携带状态转移模式是分布式系统中用于实现数据复制和去中心化处理的设计方法。该模式以事件作为服务或系统间状态变更传递的主要机制,每个事件中都包含其他组件更新其本地状态所需的全部信息(状态),使其无需依赖对源服务的同步调用。
通过服务间解耦并减少实时通信的需求,ECST 模式增强了系统弹性,使得各组件在系统部分服务暂不可用时仍能独立运作。此外,ECST 还通过将数据复制到需要的地方,让服务可以依赖本地状态副本而无需反复调用源系统 API,有效地减轻了源系统负载。这种模式特别适用于事件驱动框架和接受最终一致性的场景。
核心特性
单一来源,多目的地:在 ECST 模式中,事件由状态所有者发布,由多个需要复制该状态的领域或服务消费;这要求消息代理必须支持发布-订阅模式。
低毒丸消息风险:毒丸消息风险低:由于 ECST 模式很少涉及业务逻辑,通常情况下会避免调用其他服务的 API,毒丸消息的风险可以忽略不计。因此,在该模式中通常不需要使用死信队列(DLQ)。
批量处理:作为一款数据复制模式,ECST 极大受益于批量处理:大批量复制数据能显著提升性能并降低成本,尤其是在目标数据库支持单次操作批量插入的情况下。将高效大批量提交的消息代理与针对批量操作优化的数据库相结合,可以大幅提升应用性能。
严格顺序性:ECST 通常严格要求消息的顺序,以确保领域内的实体状态是按照正确的顺序被复制;这样能防止旧版本的实体覆盖新版本。当事件携带增量变更(例如“设置某属性”)时,顺序性尤为关键,因为乱序事件不能简单地被丢弃。支持严格顺序的消息代理可以极大地简化事件消费过程并确保数据完整性。
消息代理适配性
考虑到要支持发布-订阅、严格顺序性和批量处理等需求,再加上低毒丸消息风险的特点,Apache Kafka 成为了 ECST 模式的理想选择。Kafka 允许消费者单次操作大批量消息并提交 offset,比如单次处理 10,000 条事件、批量写入数据库(需数据库支持批量写入)、通过单次网络调用完成提交;这种机制使得 Kafka 在此类场景下的效率显著优于亚马逊 SQS。此外,低毒丸消息风险也消除了其对死信队列(DLQ)的依赖,大幅简化了错误处理流程。除批量处理的优势外,Kafka 的分区机制还能通过事件分片提高整体吞吐量。
不过,如果目标数据库不支持批量操作,数据库写入可能会成为性能瓶颈,这种情况下 Kafka 的批量提交优势难以得到体现。针对这种场景,可以将 Kafka 消息导入 FIFO SQS 队列,或直接使用 FIFO SNS 和 SQS 的组合(不使用 Kafka)。如前文所述,FIFO SQS 通过细粒度的逻辑分区实现了并行处理与顺序保障的双重目标,这种设计可以通过增加消费者节点实现动态扩展,确保系统在高负载下仍能维持高效处理能力。
事件通知模式
事件通知模式允许服务将系统中发生的重大事件通知到其他服务,一般来说通知都是简短且只包含事件必要信息的(如一个标识符)。为了处理通知,消费者往往需要调用 API 从源服务或其他服务中获取更多详细信息。此外,消费者可能也需要进行数据库更新、创建命令或发布其他系统中可消费的通知。这种模式在分布式架构中提供了松耦合和实时相应的能力,但由于通知处理的潜在复杂性(如 API 调用、数据库更新和事件发布),其可扩展性和错误处理能力都是必须要考虑的关键因素。
核心特性
事件通知模式的核心特性与命令模式的有明显重叠部分,主要体现在处理复杂且耗时任务的通知时。在这些情况中,事件通知模式的实现需要支持并行消费、自动扩缩消费者和毒丸隔离才能实现可靠和高效的处理。此外,该模式还需支持 PubSub,以实现事件的一对多分布。
在处理通知时,某些情况下工作流会较为简单,如更新数据库或向下游系统发布事件。这类情况下,事件通知模式的特性更接近于 ECST 模式。
需要注意的是,同一通知的不同消费者可能会有不同的处理方式,可能会出现消费者 A 需要执行复杂的处理,消费者 B 则在执行简单到几乎不可能失败的任务。
消息代理适配性
在通知的消费者和消费命令的特性相符时,SQS(或 FIFO SQS)自然是个好选择。但如果消费者只需要处理简单的数据库更新时,Kafka 的消费通知可能更高效,因为 Kafka 是能够批量处理通知并进行大规模批处理提交的。
通知处理方面的挑战之一是往往无法提前预测消费者的消费模式,导致在生产通知时究竟是选择 SNS 还是 Kafka 变得很困难。
在灵活性方面,我们在 EarnIn 决定使用 Kafka 作为唯一的消息代理来发布通知。如果消费者需要借助 SQS 的特性进行消费,那就可以通过 AWS EventBridge 将信息从 Kafka 路由到 SQS。如果消费者不需要 SQS 的特性,就能直接从 Kafka 消费,并享受到其高效批处理的能力。此外,使用 Kafka 而非 SNS 来发布通知,就算最后还会将消息路由到 SQS 进行消费,消费者也可以享受到 Kafka 的重放能力。
最后,由于 Kafka 也适合于 ECST 模式,而命令模式不需要支持 PubSub,这让我们没理由继续使用 SNS。因此,我们将 Kafka 作为唯一的 PubSub 代理,极大简化了我们的工作流:在所有事件都经流 Kafka 后,我们就可以构建工具,将 Kafka 事件复制到 DataLake 中进行调试、数据分析、重放或回填等用途。
结论
要想选择适合的应用程序的消息代理,就需要了解可用选项的特点以及要使用的消息模式,要考虑的关键因素包括:流量模式、自动扩展能力、对毒丸消息的容忍度、批量处理需求以及顺序要求。
本文虽以 Amazon SQS 和 Apache Kafka 为例,但更宏观的选型本质是在队列与流式架构间做出抉择。不过,通过组合使用二者优势也是可行方案。
采用单一代理作为事件生产的标准,可以让企业集中资源在一套系统上构建工具链、复制机制和监控体系,降低维护标准。消费者端则可以通过 EventBridge 等服务将消息路由至最合适的代理进行处理,从而在褒词运维效率的同时获得架构灵活性。
评论