
后端系统拆得再细服务之间的通信方式选错了拆得越多反而越痛苦。这几年的架构评审里几乎都会聊到事件驱动架构核心目标只有一个解耦。AWS EventBridge 是 AWS 上落地这套架构时绕不开的那个字——事件总线。以前我把事件处理逻辑写在一个消息中心里服务发消息、订阅消息全走一套代码后来碰到 EventBridge最大的感受是整个事件链路的治理变得明确了。它帮你接收事件、按规则过滤和路由、转换格式、再投递给目标服务。你不必再关心消息该投给谁、什么条件下投、投递失败了怎么重试剩下的是把规则写清楚、把观测指标盯起来。这篇内容适合给正在做微服务拆分、想把同步链路改成事件驱动或者已经在 AWS 上建了系统但觉得消息越来越乱的后端、架构和运维朋友。我会从 EventBridge 的几个核心概念讲起用一个订单场景走通整套流程最后重点分享可观测性的落地方法和一些文档里不会写的坑。1. 事件驱动架构的“为什么”从同步调用到事件总线1.1 解耦的底层逻辑你大概率写过这样的下单接口创建订单之后同步调用库存服务扣减库存调用积分服务加积分调用短信服务发通知。接口响应时间取决于这几个下游里最慢的那一个任何一个下游抖动你的下单链路就跟着抖动。这还只是性能问题更麻烦的是耦合。下游接口签名一变上游就得发版下游要扩容上游根本不知道下游阶段性故障上游可能直接超时。于是大家开始把同步调用改成异步消息生产者把消息丢进队列消费者自己拉取处理。这一步确实解决了一部分问题但队列本身没有业务感知能力它只负责存和取。事件驱动架构把这一步走得更彻底生产者的职责从“调用某个服务”变成“发布一个事实”。订单创建完成我只往总线里丢一个事件谁关心谁去听。生产者不点名消费方消费方也不影响生产者的成败。这里面藏着三层解耦时间解耦下游不需要时刻在线事件可以先存下来空间解耦生产者不需要知道消费者在哪实现细节解耦生产者不关心事件最终被谁消费、怎么消费。用一个生活化的类比这就像办公室里你不直接拉同事去办事而是写一张公告贴在公告栏上大家按需认领。公告栏不需要知道谁会来看它只负责把公告摆在那里。EventBridge 在 AWS 生态里扮演的就是这个公告栏但它不是普通木板而是一个带有过滤、路由、转换、重放能力的智能公告栏。1.2 EventBridge 的边界和 SNS、SQS 有什么不同很多人第一次接触 EventBridge 会问一个问题既然已经有 SNS 和 SQS为什么还要多一个 EventBridge这个问题问得很好边界确实需要先捋清楚。SQS 是点对点队列一条消息被一个消费者取走之后就没了适合任务分发和削峰填谷SNS 是发布订阅一条消息广播给所有订阅者但它基本不做内容级过滤订阅者想筛数据得自己处理EventBridge 则更像是两者的增强形态它不做消息堆积而是做实时路由并且可以根据事件内容做精细化过滤还能把事件格式转换后再发给不同目标。维度SQSSNSEventBridge通信模式点对点队列发布订阅事件总线 规则路由内容过滤不支持仅订阅过滤事件模式按字段精确匹配消息转换不支持不支持输入转换器多目标路由单消费者多订阅者同内容多目标可拿到不同内容消息重放无无Archive Replay可观测能力队列指标主题指标事件追踪、指标、审计日志这个对比不是功能堆料而是选择逻辑。SQS 更像一根管道适合点对点的任务处理SNS 更像一个喇叭适合无差别广播EventBridge 像一个有脑子的分拣员它会看事件内容里的字段决定这封信要不要送、送给谁、送的时候要不要重新包装。具体到选型如果只是把任务丢给一个 Worker 去处理SQS 仍然是最简单可靠的方案如果要做多服务订阅相同事件并且每个订阅者逻辑不同EventBridge 的规则和输入转换器会比 SNS 干净得多。这篇文章后续所有实践都基于 EventBridge但你要记住它不是万能的第 6 章我会单独聊聊它不适合的场景。2. 吃透 EventBridge 的核心概念才能写出好用不埋坑的规则2.1 事件总线、规则、目标三个最常用的“零件”EventBridge 的世界里事情按这样的顺序流转事件源产生事件事件进入事件总线总线交给规则去匹配匹配成功的路由到目标。事件总线是默认存在还是自己建的这里有个细节每个账号都有一个默认总线专门接收来自 AWS 服务的事件比如自动伸缩、系统状态、运维事件。如果要承载业务系统自己的事件建议单独建一条自定义总线比如 order-bus这样业务事件和 AWS 内部事件互不干扰权限和指标也好哈分离。事件本身有固定的顶层结构。source、detail-type、detail 三个字段是规则的判断依据。source 表示事件来源detail-type 表示事件类型detail 是一个 JSON 对象放业务数据。一个典型的业务事件长这样{ version: 0, id: a83db5c2-6f2e-4f0a-9b2e-8ad3f4c7e9a1, source: order.service, detail-type: OrderCreated, detail: { orderId: ORD-20250601-001, amount: 199.00, status: PAID, userId: u1001, notifyEmail: userexample.com }, region: ap-southeast-1, time: 2025-06-01T08:30:00Z }规则是 EventBridge 里最核心的零件。它由三部分组成事件模式用来筛事件目标列表指定投递到哪里输入转换器决定投递时事件长什么样。一条规则可以挂多个目标每个目标拿到的事件内容可以不同。这是 EventBridge 真正强大的一点调度逻辑不再是代码里的一堆 if-else而是集中到了可配置的规则里。规则本身是基础设施可以在控制台查看和修改也可以纳入基础设施即代码的版本管理团队的架构在不断演进时消息路由规则是透明的。2.2 事件模式过滤写 if 条件而不是写遍历事件模式就是规则里的筛选条件用 JSON 描述。很多人第一次看会觉得很绕其实它可以理解成一个 “对事件字段做判断的 if 条件”只不过条件是用 JSON 写的。简单匹配是精确匹配。下面这个模式表示我要匹配 source 等于 order.servicedetail-type 等于 OrderCreated 的事件{ source: [order.service], detail-type: [OrderCreated] }注意数组里的值是“或”的关系等价于 source 是 order.service 或者 payment.service。同一个 JSON 层级里多个字段是“与”的关系等价于同时满足。detail 内部可以做嵌套匹配。比如我要所有订单金额大于 100并且状态是 PAID 的事件{ source: [order.service], detail-type: [OrderCreated], detail: { amount: [{numeric: [, 100]}], status: [PAID] } }n 这个写法看起来复杂但它是 EventBridge 原生支持的数值比较不需要你去比较字符串。除 exact 和 numeric 之外还支持前缀匹配、后缀匹配、是否存在、排除匹配等实际项目里最常用的是精确匹配、数值比较和存在判断这三种。有一点需要特别注意条件只能写在事件的顶层字段或者 detail 下的子字段而且 detail 下的嵌套结构写法必须和事件的实际结构完全一致。写错层级不会报错只是规则永远不会匹配这种“安静失败”最容易坑人。2.3 输入转换器传什么样给消费方由你决定事件本身包含了很多字段但消费方不一定全需要。比如通知服务只需要 userId 和订单金额不想接收整个事件对象。EventBridge 的输入转换器可以指定从事件里提取哪些字段再拼一个新的 JSON 结构给目标。这比在消费方代码里过滤要干净得多因为你把协议的边界直接定义在基础设施层。输入转换器的模板语法大概长这样{ orderId: $.detail.orderId, amount: $.detail.amount, source: $.source }如果我想把原始事件里的 orderId、amount 提取出来然后组成一个新的 JSON模板就写成{ orderId: $.detail.orderId, amount: $.detail.amount }模板里的尖括号语法是取值占位符$就代表事件本身的根节点。注意字段值如果是字符串需要保留 JSON 的引号如果是数字不要加引号。这里经常有人出错后面专门讲。输入转换器还有一个实用场景给事件填充常量或上下文信息。比如可以在模板里固定加一个 env 字段让下游知道这是来自生产环境的事件。模板里也可以写固定值。2.4 Schema Registry 与事件归档重放什么时候真正值得用Schema Registry 是 EventBridge 自带的 schema 注册和能力发现工具它可以从已有事件中自动推断出 JSON Schema并且在代码端生成 Java、Python 等语言的绑定类。团队里如果事件结构经常变动可以用它来做一种“契约登记”。但我的个人经验是Schema Registry 不要一开始就引入。事件结构稳定后它确实能提高协作效率但在快速迭代阶段维护 schema 的同步成本往往比收益高。事件驱动架构的核心契约首先是规则和 target 之间的兼容性如果消费方对生产方的事件结构有强要求优先考虑用输入转换器把契约切干净而不是让所有人依赖同一个庞大的原始事件结构。Archive 和 Replay 则是另外一回事强烈推荐有条件就开启。Archive 可以把进入总线的事件保存下来最长可保留一年用于审计和重放。Replay 可以把某段时间内归档的事件重新发回总线重新触发规则和目标。这个能力在线上事故后的修复场景里价值巨大发现某段逻辑有 bug修复之后不需要补偿任务直接把那段时间的事件重放一遍让业务重新流转。3. 实战落地从零搭一条订单事件处理链路3.1 场景与路由设计订单服务只发一声几个人各干各的下面用一个订单场景串起整个流程。假设我有一个订单服务创建订单之后往事件总线丢一条 OrderCreated 事件下游需要做三件事库存服务需要扣减库存这个动作必须可靠用 SQS 队列接收由 Worker 异步处理通知服务需要给用户发邮件但用户可能关闭了通知所以事件里加一个 notifyFlag 字段作为开关数据仓库需要把每次订单创建都记录下来用于分析这个目标用 Lambda 接收即可。路由设计如下规则名过滤条件目标说明inventory-ruledetail.status 等于 PAIDSQS 库存队列扣减库存处理保证有重试notify-ruledetail.status 等于 PAID 且 notifyFlag 为 trueSNS 通知主题发给用户通知analytics-rule所有 OrderCreated 事件Lambda 数据分析函数事件入库或者做数据清洗订单服务不需要感知这三个下游的存在它只需要调用一次 PutEvents 接口把事件发到总线里。这个拆解就是文章标题里说的“解耦”。3.2 创建事件总线和规则控制台与 CLI 两种方式先用 CLI 创建自定义事件总线aws events create-event-bus --name order-bus创建完总线后创建第一条规则。CLI 方式需要先把事件模式写成 JSON然后传给 --event-pattern 参数aws events put-rule \ --event-bus-name order-bus \ --name inventory-rule \ --event-pattern { source: [order.service], detail-type: [OrderCreated], detail: { status: [PAID] } }注意 put-rule 默认走的是 default 总线一旦用了自定义总线必须显式传 --event-bus-name。这个参数我一开始就漏过几次规则建到了默认总线上业务事件根本不会匹配控制台里看着一切正常实际上全部落空。创建规则之后还要给规则绑定目标。目标可以是 SQS、SNS、Lambda、ECS、Step Functions 等。以 SQS 为例aws events put-targets \ --event-bus-name order-bus \ --rule-name inventory-rule \ --targets [ { Id: inventory-sqs, Arn: arn:aws:sqs:ap-southeast-1:123456789012:inventory-queue, RoleArn: arn:aws:iam::123456789012:role/eventbridge-inventory-role } ]绑定 SQS 目标时需要指定一个 IAM 角色EventBridge 会扮演这个角色去发送消息。角色权限如果配错规则匹配成功但消息发不出去。3.3 用事件模式做精细化路由以上面的 notify-rule 为例它要求 status 是 PAID 且 notifyFlag 为 true。事件模式写法如下{ source: [order.service], detail-type: [OrderCreated], detail: { status: [PAID], notifyFlag: [true] } }布尔值直接写在数组里EventBridge 会做等值匹配。这里要强调一下notifyFlag 这些字段名必须和事件里的完全一致大小写敏感。我一直建议把所有业务事件的字段命名规范都定下来比如统一 camelCase否则后期写规则时容易因为大小写不一致产生大量静默不匹配。如果你希望匹配金额大于某个值的订单可以叠加数值范围条件。比如支付金额必须大于 500 才发通知{ source: [order.service], detail-type: [OrderCreated], detail: { status: [PAID], amount: [{numeric: [, 500]}] } }事件模式里的同类判断都在 detail 下并列代表“与”关系。在事件驱动架构里这样的规则比在代码里写判断更容易被审计和运维人员看懂。3.4 用输入转换器控制消息到达消费端的“长相”SQS 里的 Worker 其实只关心 orderId 和 amount它不需要知道事件的 id、source、region 这些元数据。给 inventory-rule 加一个输入转换器把事件内容裁剪一下。在控制台的规则目标配置里展开“输入”选项选择“输入转换器”然后定义 InputPathsMap 和 InputTemplate。InputPathsMap 定义变量从哪个字段取值{ orderId: $.detail.orderId, amount: $.detail.amount }InputTemplate 定义输出结构{ orderId: orderId, amount: amount }amount 是数值所以模板里不加引号。如果你用 CLI 添加目标输入转换器参数需要转成 JSON 字符串。一个容易出错的地方是模板里的尖括号变量和外部 JSON 字符串的引号混在一起建议用单引号包模板或者写成文件再引用。转换器还有一个使用方法插入固定字段。比如给下游增加一个环境标识字段方便多个环境共用总线和队列时做环境区分{ orderId: orderId, amount: amount, env: prod }这个 env 在模板里是一个固定值EventBridge 会原样输出。3.5 目标接入的完整示例SQS、SNS、Lambda 怎么挂SQS 队列收到 EventBridge 投递的消息后如果消费者是 Python Worker拿到的消息体不是原始事件而是经过输入转换器处理后的 JSON。可以通过队列里的 Records 拿到具体 payload再做业务处理。这里要特别注意EventBridge 对 SQS 的投递是“尽力投递一次以上”Worker 侧要做幂等不去重的话某些重复投递会导致库存扣两次。Lambda 作为 EventBridge 目标时事件会直接作为 invocation 的 payload 传入业务函数里可以直接取 event.detail 拿到业务字段。下面是一个简单的去重处理示例import json def lambda_handler(event, context): order_id event[detail][orderId] amount event[detail][amount] dedup_key forder_event_{order_id} # 这里调用外部存储做幂等标记例如 DynamoDB conditional write # if not dedup_set.contains(dedup_key): # process_order(order_id, amount) return { statusCode: 200, orderId: order_id, amount: amount }SNS 作为通知目标时事件会被转成 SNS 消息发送到主题下游的邮件服务订阅 SNS 即可。SNS 和 SQS 的接入差异是 SNS 的目标本身也可以继续做扇出EventBridge 到 SNS 再到多个订阅者是事件驱动架构里常见的组合。4. 可观测性事件链路出问题时怎么快速定位是谁的锅4.1 事件追踪查看单个事件的一生事件驱动架构最大的难题是排障。同步调用时你可以在日志里看到完整的调用链路事件驱动里事件一旦发出去就像一颗石子扔进湖面涟漪怎么扩散是分布式的得靠专门的手段观察。EventBridge 控制台里有一个“事件追踪”入口可以按 eventId 搜索特定事件查看它是否被匹配到了规则、匹配到了哪些规则、投递结果如何。这是排查“事件发了但目标没执行”的利器。实际操作中先在生产者日志里找到 PutEvents 返回的 EventID然后去事件追踪里搜就能看到这个事件在总线内的完整旅程。但要注意事件追踪默认只能查询有限时间段内的事件并非无限期保留。对于更长期的审计还是得靠后面的归档功能。4.2 CloudWatch 指标盯这几个数比盯日志更有效EventBridge 会输出一组 CloudWatch 指标到总线维度我实际运维时主要盯以下几个指标名含义使用建议MatchedEvents匹配到至少一条规则的事件数如果业务量增长但该指标不动先怀疑事件没进总线TriggeredRules触发到的规则数量配合告警规则数量减少说明有规则被删除或禁用Invocations规则调用目标的次数对账证据目标处理量应以这个指标为准InvocationFailures调用目标失败的次数该指标大于 0 时要立刻查目标权限和健康状况DroppedEvents因为配额等原因丢弃的事件数一般情况下应该是 0异常增长说明配置有问题这些指标可以直接在 CloudWatch Alarm 里设置告警。我建议至少为 InvocationFailures 建一个阈值告警失败次数持续增长时能第一时间感知而不是等业务方投诉。4.3 CloudTrail 与控制面审计区分“业务事件”和“配置事件”EventBridge 的事件在业务层面是流式的CloudTrail 记录的是控制面操作。什么意思呢CloudTrail 会记录谁在什么时候创建了规则、修改了规则、删除了目标、改了总线策略但它不会记录你的业务订单事件。这两者的区别必须搞清楚。我看到过不少人在事故复盘时翻 CloudTrail想找某条业务事件结果找不到误以为系统丢了数据。其实业务事件要去事件追踪或者归档里查CloudTrail 适合回答的问题只有一个规则是什么时候被改的、被谁改的。所以可观测性建设要分成两层控制面变更看 CloudTrail业务面流转看事件追踪、指标和归档。4.4 事件归档与重放把坏掉的那段时间“重放”一遍归档和重放并不仅仅是一种备份机制更是一种可观测和事故恢复手段。前面提到Archive 可以按规则过滤存储事件。创建归档的命令如下aws events create-archive \ --archive-name order-bus-archive \ --event-bus-name order-bus \ --retention-days 7可以给归档设置一个事件模式比如只归档全部订单事件。如果只想归档 PAID 的事件archive 里同样可以写 event-pattern。需要重放某段时间的事件时使用 start-replayaws events start-replay \ --replay-name replay-20250601 \ --event-start-time 2025-06-01T00:00:00Z \ --event-end-time 2025-06-01T08:00:00Z \ --event-source-arn arn:aws:events:ap-southeast-1:123456789012:archive/order-bus-archive重放会把事件重新发到归档时所在的总线然后总线上的现有规则会重新触发。这要求消费方具备幂等性否则重放本身就是一次事故。我在实际项目里一般配合一个“重放消息头”来做幂等消费方检测到重放批次 ID 后可以对同一订单的重复事件做合并处理。5. 实战中踩过的坑与排查速查表5.1 IAM 权限配置规则匹配了目标却纹丝不动EventBridge 调用目标服务时需要权限。以一个 SQS 目标为例你必须准备一个 IAM 角色允许 events.amazonaws.com 以该角色身份调用 sqs:SendMessage。规则里如果不指定 RoleArnEventBridge 就没有权限发送消息。这个问题的症状非常迷惑规则匹配成功指标里 InvocationFailures 开始增长而队列始终为空。解决方法是在 IAM 中创建一个角色信任关系绑定事件总线服务然后给角色附加目标对应的操作权限。权限范围尽量最小化比如只允许发送到某几个指定的队列 ARN而不是给整个 SQS 全量权限。Lambda 目标的情况稍有不同不需要额外的角色只需要给 Lambda 的函数策略加上 Resource-based policy允许事件总线来调用函数即可。5.2 事件模式里的类型陷阱数字范围对字符串字段无效有一个真实案例订单金额在前端传入时被写成了字符串 “199.00”但在事件模式里用的是 numeric 比较规则从不匹配。这个问题排查了很久因为从业务日志和数据侧看订单确实存在金额也确实大于 100但指标里 MatchedEvents 一直是 0。根本原因就是类型不匹配。EventBridge 的 numeric 条件只能匹配 JSON 里的数字类型字符串类型的数字哪怕内容是 “199.00”也不会进入 numeric 判断。所以事件生产方必须在源头保证字段类型一致或者事件模式里改用前缀匹配等字符串条件。建议在事件发布前做一次 schema 校验类型错误在一开始就挡住。5.3 输入转换器模板的引号与转义问题输入转换器的模板本质上是一个字符串里面的 JSON 结构需要你自己保证合法性。最常犯的错误是把数字类型也加上了双引号。比如 amount 是数值你写了{ amount: amount }这个模板生成的输出里 amount 是字符串。如果下游按数值处理可能报类型错。正确写法是{ amount: amount }值必须是数字时不要加引号。反过来orderId 是字符串必须加引号。这个细节看起来很小但在实际对接时一旦出错下游会收到一串类型错误日志而且不容易和 EventBridge 联系起来。5.4 重复投递与幂等设计事件总线不保证恰好一次EventBridge 的投递语义是“至少一次”。这意味着重试、目标失败后的重新调度都可能导致消费方收到重复的事件。你设计的消费者必须做幂等处理。幂等的做法通常有三种数据库唯一约束、分布式锁、去重标记表。对订单场景最实际的做法是用 orderId 建唯一键重复事件写入时直接冲突不做二次处理。我接触过的一个项目因为早期没做幂等重放一次历史事件后用户收到了两条短信从那时起我们在所有事件消费方都强制要求幂等设计。不要抱有“EventBridge 很少重试所以不会重复”的侥幸心理。网络抖动、目标短暂不可用、重放操作任何一个都会带来重复。5.5 配额与重试机制什么时候会丢事件什么时候不会EventBridge 对规则挂载目标数量、事件大小等有配额限制。每条规则默认最多挂 5 个目标单个事件最大 256KB。如果你的路由需求很大设计上尽量拆成多条规则而不是在一条规则里猛塞目标。当目标调用失败时EventBridge 会按退避策略自动重试一段时间。但这个重试不是无限的如果目标持续不可用事件最终会被丢弃。想避免这种情况要么将目标配置为带死信队列把失败的事件先放着等目标恢复后再处理要么开启 Archive在事后进行重放。这里要分清死信队列和重放解决的是不同的故障场景。死信队列适合单条事件持续投递失败重放适合批量的时间窗口事件需要重新流转。两者最好都配置成本不高收益明显。5.6 常见问题速查表症状可能原因检查点指标 MatchedEvents 为 0事件没进总线或事件模式条件不满足查 PutEvents 调用是否成功查事件字段和模式是否一致MatchedEvents 增长但 Invocations 为 0规则没有挂正确目标或目标配置被移除查看规则的 Targets 列表InvocationFailures 增长目标权限不足或目标服务异常查 IAM 角色权限查目标服务日志目标收到了事件但结构不对输入转换器模板写错查看模板的 JSON 合法性和字段类型消费者收到重复数据重试导致重复投递确认消费者幂等逻辑是否生效重放后没有目标执行重放目的地总线配置缺失确认归档事件源和目标总线是否正确6. 写在最后用事件总线的个人体会EventBridge 是一个好用但容易“用上头”的工具。事件驱动架构的优势在解耦但解耦也意味着链路变多、排障变复杂甚至会给运维带来新的负担。如果你目前只有一个消费者或者事件本身只是简单通知优先考虑轻量方案如果你已经有多条消费链路、需要过滤规则、需要重放功能EventBridge 确实能帮你把路由和观测集中起来管理。我实践下来的体会是规则和输入转换器才是 EventBridge 的灵魂。很多人把 EventBridge 当成普通消息通道事件原样透传路由逻辑照样写在代码里那就浪费了一半能力。学会把业务判断写进事件模式把协议裁剪写进输入转换器整个链路才会真的“可配置化”而不是换个语言就要重写一遍消费代码。另外提醒一句事件字段的命名和类型规范要当成架构契约来维护。你可以不引入 Schema Registry但至少要有一个团队内部的事件字段文档。等到规则写了几十条之后字段类型不统一带来的问题会成倍放大。如果这篇文章对你有帮助可以从一个小场景开始尝试先建一条自定义总线把一个订单事件路由到两个目标把可观测指标和归档打开跑一遍完整流程。这个体验比读任何文档都来得实在。