第 16 章:运行时基础设施:横切能力的工程原理
16.1 工程问题的本质:横切而非纵切
可观测性、缓存、事件、存储、认证、调度——这些能力不属于任何业务环节,而是贯穿所有环节的横切面。横切能力最常见的两种失败:缺失(出了问题无法定位)与反客为主(横切能力成为系统的主干,业务逻辑围着基础设施转)。
设计原理: 横切能力的工程形态是"框架提供,业务选用,缺省降级"。框架提供统一实现(追踪上下文、结构化日志、缓存、事件总线、存储抽象、SSE、认证、调度、模块开关),业务按需注入真实后端,未注入时降级到最小可信实现。横切能力是"运行的底座",不是"业务的主干"。
16.2 可观测性:追踪与日志的一体化
链路追踪以 contextvars 提供线程安全的统一上下文;日志统一附加 trace_id;审核链的 chain_id 复用 trace_id。
原理: 可观测性的本质是关联——一次请求穿过审核链、智能体、模型、数据库、日志,若每一环各记各的 ID,故障时就无法还原全貌。单一 ID 贯穿使"一次请求的一生"可以被重放:从入口到审核链到落库,任何一个环节的异常都能沿 ID 定位。可观测性不是"多打日志",而是"一条链、一个 ID、一份可重放的时间线"。
16.3 事件驱动:解耦与顺序性
事件总线基于 Redis Streams(未装 redis 降级内存 pub/sub)。通知事件的发布方与消费方解耦:协调器发布审批事件,业务侧订阅落库。
原理: 事件驱动的价值是发布方与消费方的生命周期解耦——协调器不需要知道"通知最终存进哪张表",消费方不需要知道"事件从哪个流程来"。但解耦引入了顺序性问题:失效与创建两个事件若分属不同主题,消费方可能先处理创建、后处理失效,导致待办数量错误。框架将失效合并进创建事件的载荷(expire_before 标志),消费 handler 单线程内先失效旧待办再创建新待办——解耦的是模块,不是顺序。跨主题的顺序性风险必须由设计消除。
16.4 降级与容错:每一级能力都有最小可信实现
基础设施层普遍遵循"可选依赖 + 内存/本地降级":Redis 未装降级内存、boto3 未装降级本地文件系统(含路径穿越防护)、调度器 DB 不可用降级内存记账。
原理: 降级体系回答"外部依赖失效时系统还能做什么"。关键原则:降级到最小可信实现,而非静默失败。内存降级使会话、缓存、事件在依赖失效时仍可运行(虽然不持久);本地文件系统使存储可用(虽然不跨机)。调度器降级内存记账允许重启后重复触发(同日去重降级为内存判断)——这一已知风险被显式记录,而非被掩盖。
16.5 认证:身份与授权的分离
认证模块实现 JWT 签发/校验(标准库实现,不依赖第三方库),认证与授权分离:认证回答"你是谁",授权(第 12 章)回答"你能做什么"。模拟用户源受 DEBUG 门控,正式模式恒返回 None。
原理: 认证与授权的分离是安全设计的基本边界:认证失败时应 fail-closed(不因模拟用户源绕过),授权失败时应 fail-closed(不因权限缺失静默放行)。DEBUG 门控尤其关键——调试后门只能存在于调试模式,模拟用户源在正式模式不可用,防止"为了测试方便"成为生产入口。
16.6 调度与模块开关:运行期的编排
任务调度器(进程内守护线程,零第三方依赖):任务注册、DB 持久化配置、同日去重、异常隔离(处理器异常不中断循环)。模块开关管理模块启用状态,关闭时自动降级返回统一响应。
原理: 调度器的关键设计是配置与代码分离 + 异常隔离:调度时间/启停存配置(管理员改库即可调整),处理器异常不拖垮调度循环(隔离失败)。模块开关解决"业务模块的发布节奏"——某个模块可独立启停而不影响其他模块,是模块独立性的运行期表达。
16.7 可观测性与事件投递的深度设计
16.7.1 日志的结构化 schema
结构化日志的价值不在"是 JSON",而在字段语义可消费。专家级的日志 schema 设计有三个层次:
| 层次 | 字段 | 用途 |
|---|---|---|
| 关联层 | trace_id、span_id、chain_id | 跨环节关联(检索定位) |
| 语境层 | event、action、agent_name、user_id | 语义过滤(什么类型的事件) |
| 内容层 | input/output/tokens/duration | 详情分析(性能、质量) |
原理: 日志采集(ELK/Loki)的可查询性取决于 schema 的稳定性——字段名、类型、缺省语义一旦漂移,检索表达式即失效。因此结构化 schema 本身需要治理:新增字段须兼容演进(追加而非改名)、敏感字段在 schema 层声明脱敏。日志是给机器读的,schema 是日志的契约。
16.7.2 追踪的采样策略
全量追踪在高流量下的成本(存储、IO、敏感数据暴露面)不可忽视。采样的工程权衡:
| 策略 | 语义 | 适用 |
|---|---|---|
| 全量 | 每条请求都记录 trace | 低流量 / 合规强制场景 |
| 固定采样 | 按比例抽样 | 流量稳定、求统计特征 |
| 头部采样(基于入口) | 入口统一决策 | 保证一条请求各环节一致取舍 |
原理: 追踪的价值在"异常时能定位",而非"每条都存"。全量存储的价值衰减与成本累积不成比例——大多数正常请求的 trace 永远不会被查看。框架以 trace_id 全量生成、存储按业务策略取舍为默认:ID 永远可关联,存储按需取舍。
16.7.3 事件投递语义:at-least-once 与幂等消费
事件总线(Redis Streams)的投递语义决定了消费方必须如何写:Redis Streams 消费组默认 at-least-once(至少一次)——消息可能被重复投递。这意味着消费方必须幂等:重复处理同一事件不应产生重复副作用(重复创建待办、重复失效通知)。
原理: 事件驱动的"恰好一次"(exactly-once)在分布式语义下代价高昂(需协调协议),工程上常用"at-least-once + 幂等消费"替代:投递方保证不丢,消费方保证重放无害。幂等的实现要点:消费处理以事件 ID 去重、状态更新为幂等操作(置位而非累加)、副作用有唯一键约束。投递语义决定消费方的正确性要求——这是事件驱动架构的核心契约。
16.8 失败模式与权衡
| 失败形态 | 根因 | 设计应对 |
|---|---|---|
| 跨层日志无法关联 | 各环各记 ID | trace_id 单一贯穿 + chain_id 复用 |
| 事件顺序错乱 | 跨主题顺序性 | 合并进载荷 + 单线程消费 |
| 依赖失效全线不可用 | 强依赖外部服务 | 可选依赖 + 内存/本地降级 |
| 调试后门泄漏到生产 | 模拟源无门控 | DEBUG 门控,正式模式 fail-closed |
权衡: 内存降级以"不持久"为代价换取"可用";事件解耦以"顺序需设计"为代价换取"模块独立"。降级行为的已知限制(如重启后重复触发)被显式记录,保证降级路径可预期。
16.9 本章小结
- 横切能力框架提供、业务选用、缺省降级,是底座而非主干;
- 可观测性是"一条链、一个 ID、一份可重放的时间线";
- 事件驱动解耦模块但不断开顺序,跨主题顺序性风险由设计消除;
- 降级到最小可信实现而非静默失败;DEBUG 后门只能存在于调试模式。