运维、可观测性与生产风险¶
本页把 生产架构设计 转成可执行的发布、监控、恢复和风险检查表。 当前仓库只有控制面领域模型、快照和最小 Runner spine;Gateway、队列、真实 IM/Storage Adapter、Dashboard 和告警规则仍是后续平台实现,不应把本页当作已经部署的运行手册。
运行边界与值班目标¶
生产请求分为四个可观测阶段:callback_accept(验签和幂等接收)、execution(Runner、
Model 和 Tool)、state_commit(Session/Event/Memory/Audit)、reply_delivery(分段、
发送和重试)。每个阶段都必须携带 request_id、trace_id、tenant_id(仅在可信上下文
建立后)、binding_id、idempotency_key 和外部 message_id 的脱敏引用。
建议把以下目标作为容量和告警的起始线,最终阈值在压测后按租户合同确定:
| 阶段 | 目标 | 触发动作 |
|---|---|---|
| callback_accept | 在供应商回调时限内确认;企业微信按 5 秒约束设计 | 超时、签名失败突增或重复率突增时检查 Adapter、Secret Resolver 和公开入口 |
| execution | 受 Tenant 并发/预算、Model timeout 和 Tool deadline 共同限制 | 排队时延、模型超时、工具错误或取消率超阈值时限流/降级 |
| state_commit | event_seq 无空洞,CAS 冲突可重试且不覆盖新版本 | 冲突重试、outbox 积压或 summary 落后时暂停切换并修复 |
| reply_delivery | 可重试错误进入有界退避,最终状态可查询 | 429/5xx、DLQ、缓存回复未发送时按通道 runbook 处理 |
发布、灰度与回滚¶
- 预检:校验控制面 schema/Provider Catalog、Secret capability、Queue 连接、Session adapter conformance test 和 OTel 导出;任何必需能力未加载都不得标记 readiness。
- 兼容发布:先发布可读旧格式的新版本,再发布写入者;配置字段增加时使用显式 schema version,不能让 Worker 猜未知字段。
- 小租户灰度:以
tenant_id或 Binding 为环,先选无生产副作用的测试租户,观察 callback、execution、state、reply 四阶段指标和审计事件完整性。 - 固定快照:每个新执行选择一套 Tenant/App Revision/Model/Backend version 和 digest; 灰度切换只影响之后的执行,不能在一个 Runner 中途热换 Model 或 Storage。
- 扩大范围:按租户并发、消息类型和工具权限分层扩大;当错误预算、成本或 DLQ 超过阈值 时停止扩大,而不是仅重启 Worker。
- 回滚:把路由/Binding 或默认 Profile 指针切回历史 published 版本,保留新版本的审计 和已提交事件;进行中的 plan 继续使用原版本,新的 plan 使用回滚版本。配置回滚不撤销已 发送消息,也不删除审计事实。
控制面发布采用乐观锁和 Outbox;缓存失效必须带版本而非只依赖 TTL。Secret rotation 要
先写新 secret_ref、验证候选回调和出站调用,再撤销旧 handle,最后切换新 Binding 版本。
旧 Secret 值不出现在回滚记录、trace 或错误消息中。
监控、日志和审计¶
Metrics¶
指标按阶段和受控低基数维度聚合:
| 类别 | 指标示例 | 推荐标签 |
|---|---|---|
| 流量 | callback 数、有效/无效签名数、重复投递数 | channel、provider、status、error_class |
| 延迟 | callback、队列等待、Runner、Model、Tool、Storage、reply 延迟 | operation、provider、result |
| 执行 | active executions、取消、超时、Tool deny/approval | model_family、tool_family、decision |
| 数据 | CAS 冲突、event_seq gap、outbox backlog、summary lag、vector index lag | backend、capability、state |
| 回复 | 成功率、分段数、429/5xx、重试、DLQ | channel、reply_type、error_class |
| 成本 | tokens、模型成本、Tool 成本、租户预算使用率 | tenant 的授权/聚合维度、model_family |
不能把 message_id、完整 URL、原始 user/chat/session 或 request body 作为指标 label;高基数
问题用日志关联、trace exemplar 和审计查询解决。租户成本指标必须有访问控制,匿名租户或
超过基数预算的维度归入受控聚合桶。
Trace 与结构化日志¶
入口创建或提取 trace_id,子 span 至少覆盖:
im.callback
└── binding.candidate_lookup
└── binding.verify_and_decrypt
└── idempotency.claim
└── execution.plan_load
└── runner.run
├── model.call
├── tool.call
├── session.event/state
├── memory.write/vector.enqueue
└── im.reply.enqueue/send
日志字段使用 request_id、trace_id、tenant_id、binding_id、session_id 的脱敏或哈希
形式以及 error_class、event_seq、plan_digest、retry_count。不得记录 Secret、模型
API key、完整 webhook XML/JSON、用户原文或可重放 token。必要的原文只在受控 Artifact/审计
存储中按租户权限保存,并以 digest 关联。
审计不是 trace 的别名。audit_log 采用 append-only 约束或 WORM/不可篡改归档,写入失败
必须影响提交策略或进入可观测 repair 状态,不能因为 telemetry exporter 故障而静默丢失。事件
至少有 tenant_id、channel、user/session 引用、app/revision、tool、decision、latency、
error_type、cost、request_id、trace_id、actor、reason、occurred_at 和前后版本;保留期限
遵循 Tenant policy,合规安全事件不能被普通 trace sampling 丢弃。
故障恢复 Runbook¶
IM 回调重复、乱序或验签失败¶
- 先按
channel/provider/error_class看签名失败率、重复率和message_event状态分布; 不用消息正文搜索日志。 - 验签失败只检查候选 route、时间窗、Token/AES key 的 secret version 和 clock skew;不把 失败 payload 直接当可信 Tenant,也不为了“止重试”而跳过验签。
running/completed/reply_pending/replied的重复请求只返回确认或重放缓存回复; 不重新执行模型和副作用 Tool。- 回调重复不负责回收执行;对
running/execution-reconciling的队列任务检查 execution owner、heartbeat、lease deadline 和 fencing token。过期任务先对账已提交 event、Tool 幂等键和 provider receipt, 安全可恢复才用新 fence 重排;副作用结果不明则进入 failed/DLQ/人工处理,不能盲目重跑。 execution-reconciling的任务必须先检查tool_invocation的 request digest、外部幂等键、 claim/fence 和 provider receipt;已接受不重跑,结果不明或无供应商幂等能力的副作用转manual/DLQ,不能只看聚合message_event或audit_log决定重试。- 多段回复按
reply_id + segment_index查看 outbox;发送前检查 owner、lease deadline 和 fencing token,过期sending/unknown先用原幂等键向供应商对账,确认未接受后再换新 fence 进入 retryable;全部 segment 确认成功后才把聚合状态置为replied,仍不明的分段保留 unknown 并告警/DLQ,不能盲目重发。 - 乱序事件进入 pending/repair,按外部序号或收到时间保留,不修改已经提交的旧 event_seq。
Session CAS 冲突或 outbox 堵塞¶
先确认是否同一 Session 的并发消息峰值或租约/fencing 错误,再检查冲突是否最终收敛。短时 冲突可按固定次数重新读取最新 state 并重放未提交 event;持续冲突时对该 Session 限流或串行 化,而不是无限扩大重试。Outbox 堵塞时暂停新回复发送的扩容,保留幂等记录,修复 provider 或消费者后按 cursor 重放,并监控重复发送保护。
提交屏障固定为 event/state → durable Memory → reply outbox → async Summary:Memory 写入
失败时不放行可发送的 outbox,进入 repair;Memory 已成功而 Summary 失败时保留事件和 Memory,
只重排 Summary,不回滚或重新执行 Runner。
completed 只在执行结果和 reply cache 持久化后成立;必须先幂等物化完整 reply outbox,再 CAS
为 reply_pending。发现 completed 缺段或 reply_pending 缺段时,按 cache ref 和 repair
cursor 补齐;修复必须按 tenant_id + event_id + reply_id 关联 event,校验 segment_count,
不重新运行 Runner,也不能把另一 event 的 outbox 归入本次回复或跳过分段直接标记 replied。
Model、Tool 或数据库故障¶
- Model:使用 request deadline、有限重试和 token 预算;超时写失败/取消 event,返回固定 兜底。若模型调用可能产生外部副作用,重试前必须有 Tool capability 证明。
- Tool/MCP:按错误类型区分参数拒绝、权限拒绝、超时和 provider 故障;危险工具默认转人工 审批或补偿,不因网络错误自动重放。
- Redis/SQL:读取/写入策略由 Backend Profile 声明;不能用过期授权缓存绕过 Tenant/App 状态。恢复后从 outbox/event cursor 补写,校验 event_seq、state digest 和审计完整性。
Worker 进程终止与优雅关闭¶
关闭顺序为:停止接收新回调 → 停止领取新队列任务 → 取消超出 deadline 的执行 → 等待有限
时间消费 Runner Event channel → 写入完成/取消/失败和 reply outbox → 关闭 worker 自己拥有
的 client、consumer 和 exporter。tRPC-Agent-Go Runner 负责其 Event 语义,平台负责传递
context.Context、排空、超时和资源所有权。超时后宁可让幂等队列重新投递,也不能让旧 Worker
继续写新版本 Session。
容量模型¶
在没有压测数据前只定义变量和保护关系,不给出虚假的单节点 QPS:
concurrent_executions
≈ callback_rate × p95(execution_duration + reply_queue_delay)
session_write_qps
≈ callback_rate × (inbound_events + runner_events + state_commits)
reply_api_qps
≈ callback_rate × average(reply_segments) × retry_multiplier
monthly_model_cost
≈ Σ(tenant_input_tokens + tenant_output_tokens) × model_price
压测至少覆盖:单租户突发、多个租户同时突发、同 Session 并发、长模型调用、Tool 超时、IM 429、数据库 failover、向量索引落后和 Worker 滚动重启。容量上限由最小者决定:租户并发/预算、 Worker CPU/memory、Model provider 限额、Session 写 QPS、队列吞吐、IM 发送配额或 OTel exporter backpressure。高峰保护使用租户级 token bucket、全局队列上限、每 Session 串行化和 DLQ,而 不是无限增加 Worker。
迁移操作摘要¶
Redis Session → SQL Session¶
迁移必须是 Backend Profile 版本迁移,不修改旧 Profile 的语义:
- 记录租户、源版本、目标版本和迁移 correlation ID;冻结新执行的切换点。
- 全量复制 Session/Event/State/Summary/Idempotency/Outbox,保留
event_seq、版本和 digest。 - 用 outbox 双写和源 cursor 做增量追平;任何丢失、冲突或审计写失败都阻止切读。
- 进行行数、最大序号、摘要哈希、随机内容、权限过滤和未完成回复校验。
- Shadow read 稳定后仅让新 plan 切 SQL;旧 plan 继续访问旧 Profile。
- 保留回滚窗口。目标 provider 出现延迟/冲突时切回旧 Profile,并从 cursor 修复,不能撤回 已经对外发送的副作用。
本地向量库 → 远端向量库¶
以稳定文档/Memory ID 和原文 digest 导出,按 embedding model/version 分批导入;维度不符就 重建,不做未经验证的转换。完成数量、digest、租户过滤、抽样查询和删除 tombstone 校验后, 通过新的 Backend/Knowledge index version 进行 shadow query 和灰度。向量索引只负责检索, 源事件和权限仍在 SQL/Session 真相中。
生产风险清单¶
每项风险都必须有触发条件、影响范围、检测信号和可执行缓解/恢复措施。以下清单是上线门禁, 不是泛化的“注意安全”。
| 风险 | 触发条件 | 影响范围 | 检测信号 | 缓解或恢复措施 |
|---|---|---|---|---|
| 跨租户访问 | 未验签 payload/header 直接提供 tenant_id;Adapter/Storage 漏掉租户谓词 |
读取或写入其他租户的配置、Session、Memory、审计 | 同一 request 的候选租户与可信租户不一致;双租户 conformance test 失败;越权告警 | 验签成功后才建立可信 Tenant;所有表使用显式 tenant_id 和复合约束;失败关闭并审计,暂停有问题 Binding |
| Secret 泄露 | secret 进入 plan、缓存、trace、错误、备份或日志 | 伪造 IM 回调、模型/数据库被接管 | secret pattern 扫描、日志 DLP、Secret Manager 访问异常、rotation 后旧 handle 使用 | 只保存 secret_ref;短路径解析 handle;脱敏和最小权限;rotation 主动失效并重放验证 |
| IM 重复/乱序 | 企业微信超时重试、Telegram 重复 update、网络重排 | 重复模型调用、重复扣费、summary 错乱、用户收到多条回复 | duplicate rate、message_event unique conflict、event_seq gap、DLQ |
tenant+channel+message_id 幂等;状态机/CAS;pending/repair;completed 后仅重放缓存回复 |
| Session 并发冲突 | 同一群/会话多节点同时执行;lease 过期旧 Worker 回写 | event/state 覆盖、上下文丢失、不可重放 | CAS conflict、fencing reject、state version 回退、summary lag | SQL lock/CAS 或 Redis 原子脚本;fencing token;每 Session 串行化/限流;冲突重读重放 |
| 后端迁移分叉 | 全量复制后 cursor 未追平、双写失败或错误切读 | 新旧 Session/Event/Memory 不一致,回滚无法恢复 | row/count/digest mismatch、outbox backlog、shadow read diff | Backend Profile 版本固定;全量+双写+增量+校验+shadow read;保留旧源和明确回滚窗口 |
| 模型超时/成本失控 | provider 延迟、上下文过长、重试无预算 | 回调积压、租户预算耗尽、Worker 资源耗尽 | model p95/p99、timeout、retry、token/cost burn rate | context deadline、有限退避、token/并发预算、降级文案;不重试有副作用执行 |
| Tool 副作用重放 | reply 失败或 Worker crash 后把完整执行重新跑一遍 | 重复发货、工单、写外部系统或扣费 | tool idempotency mismatch、外部系统重复号、审计 decision | 默认标记不可重试;外部 idempotency key、确认/审批、outbox 和人工补偿;缓存最终回复 |
| 指标高基数 | 把 tenant/user/session/message/URL 全放 label | Prometheus/Collector 内存和费用上涨,查询不可用 | active series、remote write lag、collector backpressure | 低基数 label 白名单;高基数放 trace/log;租户成本用受控聚合和 exemplars |
| 配置回滚不完整 | 回滚只改指针但缓存、Binding 或 Worker 仍用新版本 | 一次执行混用版本,故障持续或无法复现 | plan key 与路由版本不一致;cache hit old/new mismatch | 版本化快照和主动失效;回滚只影响新执行;审计每次 plan;旧 plan 允许完成/取消 |
| 回复重试风暴 | IM 429/5xx、固定间隔重试、无 per-chat 限速 | 供应商封禁、用户刷屏、队列雪崩 | retry multiplier、429、DLQ、outbox age | 指数退避+jitter、解析 Retry-After、按通道/chat 分桶、最大预算和 DLQ |
| goroutine/事件泄漏 | context 未传递、Runner Event channel 未排空、consumer 无关闭边界 | Worker 内存上涨、滚动发布卡住、重复消费 | goroutine、FD、channel backlog、shutdown duration 持续上升 | owner 明确;context deadline;有界 drain;supervisor/health check;超时交给幂等重投递 |
当前实现状态与后续门禁¶
本仓库目前可以验证 Tenant、Agent App/Revision、Model Profile、Backend Profile、无密钥
Execution Plan、Runner policy 和 Tenant-scoped Session 的模型/边界测试;不能验证真实 IM
验签、跨节点 CAS、队列至少一次投递、SQL/Redis 迁移或生产告警。后续实现每落地一个 Adapter
都必须补充:双租户隔离测试、重复/乱序/验签失败测试、provider 一致性契约测试、故障注入、
审计字段检查和 mkdocs build --strict。