RelayOps 采用模块化单体架构,同一构建产物提供两个可独立扩展的运行角色:
api:认证、端点/API 密钥管理、事件接入、查询、模拟器和 Outbox 发布。worker:消费 RabbitMQ 消息并发起真实的对外 HTTP 投递。all:本地开发和单命令演示时同时启用两个角色。
这一边界允许只扩容 Worker,又无需承担独立版本微服务的运维成本。每个功能包管理自己的 Controller、应用服务、Repository 和 DTO;数据库实体不会直接作为接口响应返回。
- 认证层从 JWT 或 API 密钥解析租户,Controller 从不接收客户端传入的租户 ID。
- 接入层执行分布式租户限流并校验载荷。
- 单个 PostgreSQL 事务写入事件、创建所有匹配的投递,并为每个投递插入一条 Outbox 记录。重复的
(tenant_id, idempotency_key)返回原事件。 - Outbox 发布器通过
FOR UPDATE SKIP LOCKED认领记录,向 RabbitMQ 发布持久化消息,等待发布确认后再把记录标记为已发布。 - Worker 只有在持久化记录尝试结果或下一状态后,才手动确认 RabbitMQ 消息。
- 可重试结果(429、5xx、超时、连接失败)进入有限 TTL 重试队列,TTL 到期后通过死信路由返回 ready 队列。不可重试的其他 4xx 立即进入终止状态。重试耗尽后,投递同时进入数据库持久化死信状态和 RabbitMQ 运维死信队列。
- 回放操作在同一事务中将死信投递恢复为待处理,并写入新的 Outbox 记录。
平台提供的是至少一次投递尝试,不是恰好一次。Outbox 消除了数据库与消息系统之间的原子性缺口,但当目标端已接受 HTTP 请求、而本地结果尚未提交时发生崩溃,外部副作用在本质上无法判定。RelayOps 会重试请求、携带稳定的 X-RelayOps-Delivery-Id 并对请求体签名,接收方可据此去重。这是处理外部 HTTP 副作用时诚实且可落地的工程取舍。
重复 RabbitMQ 消息不会破坏状态,因为 Worker 会先认领投递行,并忽略终止状态或已被其他 Worker 持有的工作。发布确认后发布器崩溃可能产生重复消息,同一认领逻辑会吸收重复。尝试表通过唯一约束 (delivery_id, attempt_number) 保证历史序号不重复。
所有应用表均位于 relayops Schema,由 Flyway 创建。tenants、user_accounts 和 api_keys 构成身份域;webhook_endpoints 与 endpoint_subscriptions 定义事件路由;events、deliveries 和不可变的 delivery_attempts 构成运维查询所需的持久化模型;outbox_messages 是数据库通往消息系统的唯一桥梁。
所有可查询的业务记录都包含 tenant_id。全局唯一的 API 密钥哈希和登录邮箱只作为认证索引,业务查询和变更仍必须包含已认证租户。(tenant_id, idempotency_key) 防止重复事件,(event_id, endpoint_id) 防止重复创建投递,(delivery_id, attempt_number) 在回放后仍维持单调递增的审计历史。状态/时间索引服务于 Worker 认领和运维列表;JSONB 仅用于事件载荷、请求头和 Outbox 信封,不替代关系约束。
Outbox 发布器使用短事务的 FOR UPDATE SKIP LOCKED 认领,因此多个 API 副本可以并行发布。只有收到 RabbitMQ publisher confirm 后才将记录标记完成。过期的发布认领和 Worker IN_PROGRESS 状态都可以恢复,因为所有权有明确期限,而持久化记录始终是权威状态。
| 故障场景 | 持久化行为 |
|---|---|
| 数据库事务回滚 | 事件、投递和 Outbox 均不落库 |
| 发布器在 confirm 前退出 | Outbox 保持待处理/已认领,随后由恢复任务重新处理 |
| 发布器在 confirm 后退出 | 可能产生重复消息,由投递认领逻辑吸收 |
| Worker 在持久化完成前退出 | RabbitMQ 重新投递,过期执行状态可被重新认领 |
| 接收方成功但本地结果提交失败 | 作为不确定副作用重试;接收方用稳定投递 ID 去重 |
| HTTP 429 | 在上限内遵循 Retry-After 后重试 |
| HTTP 5xx、超时、连接错误 | 进入有限指数退避队列,耗尽后死信 |
| 其他 HTTP 4xx | 不重试,直接终止失败 |
| 单个端点缓慢或持续失败 | 端点级令牌桶和并发租约隔离其压力 |
| Outbox/ready 积压超过阈值 | API 关闭式拒绝,返回 503 和 Retry-After |
- 所有持久化业务行都带
tenant_id;Repository 操作按租户限定,并通过跨租户集成测试强制验证。 - Redis Lua 令牌桶在多个实例之间原子执行租户接入限流和端点投递限流。
- RabbitMQ prefetch、监听器并发、HTTP 连接数、响应体大小以及数据库连接池均有上限。
- 每个端点的并发租约避免单个慢目标占满 Worker 池;未取得租约的工作进入短暂且有限的重试路径。
- 当租户速率或实测 Outbox/队列积压超过阈值时,事件接入返回
429或503。
| 组件 | 用途 | 选择原因 |
|---|---|---|
| PostgreSQL | 业务事实源、Outbox、查询模型、尝试审计 | 通过事务将已接收事件与持久化工作绑定 |
| RabbitMQ | 工作队列、背压缓冲、重试桶、Broker DLQ | 路由、确认和重新投递语义适合 Webhook 任务 |
| Redis | 分布式限流桶和短期租约 | 为 API/Worker 副本提供低延迟协调 |
当前有意不使用 Kafka:RelayOps 已有 PostgreSQL 审计日志和 RabbitMQ 工作队列,不需要额外的长保留、多消费者流式回放。MySQL 同样不使用,因为引入第二个关系型业务事实源不会带来有价值的新能力。
密码采用自适应单向哈希。API 密钥和引导令牌不以明文持久化。JWT 签名材料和中间件凭据只通过环境变量提供。端点 URL 会经过校验;对外 HTTP 客户端默认阻止回环、链路本地、组播和私网地址,以降低 SSRF 风险。开发配置会显式放行内置模拟器。Webhook 请求体使用每个端点独立的 HMAC 密钥签名。
Actuator 提供存活/就绪探针、Prometheus 指标和依赖健康状态。领域指标覆盖已接收/重复事件、尝试耗时与结果、Outbox 发布与失败,以及定期采样的 Outbox 和 ready 队列积压。Worker 使用结构化日志,并在适用时携带 tenant_id、delivery_id、attempt、outcome、HTTP 状态码和延迟。