Skip to content

Latest commit

 

History

History
76 lines (52 loc) · 6.53 KB

File metadata and controls

76 lines (52 loc) · 6.53 KB

RelayOps 架构说明

部署形态

RelayOps 采用模块化单体架构,同一构建产物提供两个可独立扩展的运行角色:

  • api:认证、端点/API 密钥管理、事件接入、查询、模拟器和 Outbox 发布。
  • worker:消费 RabbitMQ 消息并发起真实的对外 HTTP 投递。
  • all:本地开发和单命令演示时同时启用两个角色。

这一边界允许只扩容 Worker,又无需承担独立版本微服务的运维成本。每个功能包管理自己的 Controller、应用服务、Repository 和 DTO;数据库实体不会直接作为接口响应返回。

持久化事件流程

  1. 认证层从 JWT 或 API 密钥解析租户,Controller 从不接收客户端传入的租户 ID。
  2. 接入层执行分布式租户限流并校验载荷。
  3. 单个 PostgreSQL 事务写入事件、创建所有匹配的投递,并为每个投递插入一条 Outbox 记录。重复的 (tenant_id, idempotency_key) 返回原事件。
  4. Outbox 发布器通过 FOR UPDATE SKIP LOCKED 认领记录,向 RabbitMQ 发布持久化消息,等待发布确认后再把记录标记为已发布。
  5. Worker 只有在持久化记录尝试结果或下一状态后,才手动确认 RabbitMQ 消息。
  6. 可重试结果(429、5xx、超时、连接失败)进入有限 TTL 重试队列,TTL 到期后通过死信路由返回 ready 队列。不可重试的其他 4xx 立即进入终止状态。重试耗尽后,投递同时进入数据库持久化死信状态和 RabbitMQ 运维死信队列。
  7. 回放操作在同一事务中将死信投递恢复为待处理,并写入新的 Outbox 记录。

可靠性语义

平台提供的是至少一次投递尝试,不是恰好一次。Outbox 消除了数据库与消息系统之间的原子性缺口,但当目标端已接受 HTTP 请求、而本地结果尚未提交时发生崩溃,外部副作用在本质上无法判定。RelayOps 会重试请求、携带稳定的 X-RelayOps-Delivery-Id 并对请求体签名,接收方可据此去重。这是处理外部 HTTP 副作用时诚实且可落地的工程取舍。

重复 RabbitMQ 消息不会破坏状态,因为 Worker 会先认领投递行,并忽略终止状态或已被其他 Worker 持有的工作。发布确认后发布器崩溃可能产生重复消息,同一认领逻辑会吸收重复。尝试表通过唯一约束 (delivery_id, attempt_number) 保证历史序号不重复。

PostgreSQL 数据模型

所有应用表均位于 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 状态码和延迟。