行业资讯

从 0 构建 AI Workload Platform(七):可观测性、故障注入与性能验证

发布时间:2026/8/28 5:36:19
从 0 构建 AI Workload Platform(七):可观测性、故障注入与性能验证 摘要我正在从零开发 AI Workload Platform一个面向 Agent 与确定性程序任务的可靠运行时和调度平台。Agent智能体是能够调用模型和工具、根据执行结果继续决定后续动作的程序确定性程序任务则按照预先定义的代码和输入执行。工作流Workflow是按依赖关系组织的多步骤执行定义平台负责管理两类任务共有的工作流依赖、状态、重试、取消、恢复和执行节点。模块 4 已经把任务交给多个独立 Worker 执行并用 PostgreSQL 关系数据库保存分发记录、租约和执行结果。它证明了 Worker 失联后任务能够重新分配但还不能系统回答故障发生在哪里、影响范围多大、恢复是否完成以及增加诊断代码后性能是否发生变化。模块 5 因此增加结构化日志、可聚合指标、跨组件调用链、告警状态机和测试专用故障注入并在真实 PostgreSQL 与多进程 Worker 上复现八类故障、五类告警和四种观测配置的性能对照。本文不把日志和指标当作业务状态也不把一次本机基准写成生产容量而是从数据契约、失败边界和实验方法解释怎样建立可验证的可观测性。目录为什么多 Worker 之后需要可观测性可观测性是什么观测数据为什么不是业务事实结构化日志怎样建立关联Counter、Gauge 和 Histogram 分别记录什么低基数为什么是指标设计边界Trace、Span 与上下文传播告警不是一次条件判断滚动窗口、最小样本量与滞回故障注入怎样证明恢复行为性能数据怎样避免误导技术选型与替代方案真实验证与性能结果当前限制与下一步总结1. 为什么多 Worker 之后需要可观测性控制面Control Plane是接收请求、保存运行状态并决定任务何时可以执行的服务部分。Worker执行节点是从控制面领取并实际执行任务的独立进程。模块 4 在二者之间建立了持久化分发协议控制面创建 Dispatch - Worker 领取租约 - Worker 周期发送 Heartbeat - Worker 提交 Complete - PostgreSQL 原子推进 Task、Attempt 和 Dispatch 状态Dispatch分发记录表示一个任务已经进入可领取队列租约Lease表示 Worker 在有限时间内拥有执行权Heartbeat心跳用于报告 Worker 存活并续期租约Complete完成请求用于提交一次执行结果。Run运行实例是某个工作流版本的一次实际运行Task任务是 Run 中一个可调度工作单元Attempt执行尝试是 Task 的一次真实执行记录。这条链路能够恢复并不等于维护者能够快速解释它。只查询最终 Run 状态无法区分以下情况任务仍在等待上游依赖Dispatch 已经创建但没有可用 WorkerWorker 正在执行尚未提交结果Complete 请求失败Worker 正在重试租约已经过期协调器尚未完成回收PostgreSQL 连接池已经接近耗尽。如果没有统一的诊断数据维护者需要临时拼接进程输出、数据库表和请求结果。更严重的是无法稳定复现故障时“看起来恢复了”不能证明状态机在并发和中断下仍然正确。模块 5 不重新设计模块 4 的租约和状态机而是在已有操作边界旁增加诊断能力并用故障实验检查这些诊断数据能否解释真实恢复过程。2. 可观测性是什么可观测性Observability是通过系统输出推断内部状态的能力。它不是某一个软件名称也不等于“多打日志”。本模块使用三类基础信号信号适合回答的问题本项目中的例子日志Log某个具体操作发生了什么哪个 Worker 的哪次 Complete 返回lease_lost指标Metric一段时间内发生多少、当前有多少当前队列深度、在线 Worker 数、HTTP 错误量调用链Trace一次请求经过哪些组件、各阶段耗时多少Worker Client 与控制面处理同一次 Heartbeat 的父子 Span单个操作记录表中的 HTTP 是Hypertext Transfer Protocol超文本传输协议它是控制面与客户端、Worker 交换请求和响应时使用的网络协议。三类信号解决的问题不同日志可以保存单次操作的高基数标识但大量日志不适合直接计算长期趋势指标适合聚合、绘图和告警但必须限制标签取值数量Trace 适合查看一次请求内部的时间关系但需要采样、传播和存储策略。一个成熟观测系统通常还包括采集代理、长期存储、查询语言、图表和通知平台。模块 5 先验证应用侧契约不部署完整监控后台。当前提供的是日志、标准时序指标文本、开放标准调用链以及通过 HTTP 回调发送的告警通知。3. 观测数据为什么不是业务事实业务事实源Source of Truth是系统判断正式状态时唯一可信的数据来源。本项目中Workflow、Run、Task、Attempt、Dispatch 和租约的正式状态仍由 PostgreSQL 与工作流状态机保存。日志、指标和 Trace 都可能丢失状态事务提交后进程可能在写日志前崩溃指标保存在进程内存中重启后会重新开始Trace 可能因采样被跳过也可能因导出队列已满被丢弃告警 HTTP 回调接收器可能超时或返回错误。因此不能根据“一条成功日志”直接认定数据库状态已经提交也不能因为“没有看到 Trace 记录”就认定操作没有发生。诊断过程应先确定问题范围再回到状态 API、事件历史和 PostgreSQL 已提交记录确认最终事实。APIApplication Programming Interface应用程序编程接口是组件对外提供的结构化调用契约。这个边界还影响指标设计。Run、Task、Attempt 和 Dispatch 的状态变化发生在 PostgreSQL 事务内。如果应用在事务外根据返回值增加生命周期 Counter只增不减的累计计数器可能出现两类不一致事务已经提交进程在 Counter 增加前崩溃计数偏少客户端没有收到响应而重放请求应用重复增加 Counter计数偏多。所以模块 5 没有发布从事务外推测的 Run、Task、Attempt 和 Dispatch 生命周期总数。以后如果确实需要严格累计值应从已提交事件或outbox事务内事件待发布表派生。Outbox 与业务状态在同一事务写入提交后再异步发布能够避免“业务状态成功但事件没有留下记录”的窗口。4. 结构化日志怎样建立关联结构化日志Structured Logging是由稳定字段组成的键值记录。与一整段自由文本相比固定字段更容易被程序过滤、聚合和建立查询条件。模块 5 使用 Go 标准库log/slog统一支持text和json两种格式。text便于本地终端阅读json便于日志采集系统解析。slog 的Handler日志处理器负责级别过滤、字段处理和最终编码。一次 Worker Complete 失败可以形成以下结构levelWARN msgworker operation failed request_idreq_01 run_idrun_01 task_keysummarize attempt2 worker_idworker_01 dispatch_iddispatch_01 operationcomplete duration_ms12 error_codelease_lost这些字段分别承担明确职责字段含义request_id一次 HTTP 请求的标识run_id、task_key、attempt定位工作流运行中的一次任务尝试worker_id、dispatch_id定位分布式执行者和分发记录operation有限操作名例如claim、heartbeat、completeduration_ms当前操作耗时单位为毫秒error_code稳定错误类别不使用任意错误文本做分类trace_id、span_id关联调用链中的具体 Trace 和 SpanSpan 是其中一个操作记录字段“可以省略”和“可以伪造默认值”是两件事。如果某个组件还不知道 RunID就应省略该字段不能生成一个看似真实但无法对应数据库记录的 ID。4.1 日志级别日志级别表示事件的重要程度DEBUG记录轮询和成功的高频协议操作默认生产配置可以关闭INFO记录 Run、Worker 和恢复等生命周期事件WARN记录可恢复错误、租约拒绝和告警通知失败ERROR记录使当前进程无法继续履责的错误。成功的 Claim、Heartbeat 和 Complete 属于高频操作。如果全部使用 INFO正常流量会淹没真正需要关注的生命周期信息因此本项目把成功协议操作放在 DEBUG失败操作放在 WARN。4.2 脱敏与长度限制脱敏Redaction是在日志写出前移除或替换凭据和敏感内容。模块 5 的日志 Handler 会过滤 Bearer Token、密码、API Key、Worker Session Token 和租约令牌等常见格式生产错误文本还限制为 512 字节。长度限制不仅控制日志量也避免外部依赖返回超长正文。UTF-88-bit Unicode Transformation Format8 位 Unicode 转换格式是 Go 字符串常用的可变长度字符编码按字节截断时必须保证编码完整否则一个中文字符可能在中间被切断产生无效文本。完整任务输入、模型提示词和任务结果不进入通用日志需要保存业务产物时应使用后续专门设计的产物存储和访问权限。5. Counter、Gauge 和 Histogram 分别记录什么指标Metric是可以随时间采集和聚合的数值。Prometheus是一套开源监控与时序指标生态其指标模型常用三种类型。5.1 Counter累计发生多少次Counter计数器只能增加适合记录累计事件。例如workload_http_requests_total workload_alerts_total workload_db_pool_wait_seconds_totalCounter 重启后可以从零开始查询端通常使用一段时间内的增长量或增长速率而不是把进程启动以来的绝对值当作永久总数。5.2 Gauge当前有多少Gauge仪表值可以增加也可以减少表示某一时刻的当前状态。例如workload_queue_depth workload_workers workload_active_leases workload_db_pool_connections队列任务被领取后队列深度会下降Worker 离线后在线数量也会下降。这类值不适合 Counter因为它们不是只增不减的累计事件。5.3 Histogram数值分布是什么Histogram直方图把每次观测值放入预先定义的区间并同时记录样本数和总和。它适合请求耗时与租约回收耗时workload_http_request_duration_seconds workload_operation_duration_seconds workload_lease_reclaim_duration_secondsHistogram 保存的是多个独立样本的分布。例如每次 Complete 花费 5 ms、8 ms、20 ms就应该分别调用三次Observe。5.4 为什么数据库累计等待时间不能重复写入 Histogrampgx 是本项目使用的 PostgreSQL Go 驱动和连接池。它提供的AcquireDuration是“进程启动以来等待数据库连接的累计时长”不是最近一次等待耗时。假设连续三次读取快照得到1 秒 - 3 秒 - 6 秒如果把 1、3、6 分别写入 HistogramHistogram 会把它们误认为三次独立等待得到总和 10 秒真实累计增长只有 6 秒。模块 5 因此把相邻快照增量写入 Counter第一次增加 1 秒 第二次增加 2 秒 第三次增加 3 秒 最终 Counter 为 6 秒如果 pgx 连接池重建导致累计值变小采集器先把上次快照归零再从新累计周期计算增量。这个例子说明选择指标类型前必须先确认源数据是“单次样本”“当前值”还是“累计值”。6. 低基数为什么是指标设计边界标签Label是指标旁用于分组的键值。以下指标可以按方法、路由模板和状态分组workload_http_requests_total{methodGET,route/api/v1/runs/{run-id},status200}基数Cardinality是一组标签可能形成的不同组合数量。GET、POST等 HTTP 方法只有少量稳定取值属于低基数。RunID、TaskKey、WorkerID 和 DispatchID 会不断产生新值属于高基数。如果给每个 Run 建立一条指标时间序列10 万个 Run 再乘以状态、路由和错误码会迅速形成大量序列。每条序列都需要内存、磁盘和索引查询与告警成本也会增加。因此模块 5 采用两层策略指标只使用有限白名单标签例如operation、outcome、error_code、HTTP 方法和路由模板单个 Run 的定位使用日志、Trace、状态 API 和数据库事件。原始 URL 也不能直接作为标签。/api/v1/runs/run-a与/api/v1/runs/run-b必须归一化为同一个路由模板/api/v1/runs/{run-id}。未知标签值统一映射为unknown不能因为新增任意字符串而动态扩展序列。/metrics是只读取进程内 Registry 的 HTTP 端点。Registry注册器保存当前进程已经注册的指标收集器。该端点不查询业务数据库避免监控抓取反过来增加数据库压力需要数据库快照的 Gauge 由协调器周期刷新。7. Trace、Span 与上下文传播Trace调用链表示一次请求经过多个组件的完整时间关系。Span跨度表示 Trace 中一个有开始时间、结束时间、状态和属性的操作。一个 Trace 由多个具有父子关系的 Span 组成。模块 5 使用 OpenTelemetry。OpenTelemetry开放遥测是定义日志、指标和 Trace API、上下文传播及导出协议的一组开放标准和实现。一次 Complete HTTP 请求当前可以形成worker.complete Worker Client Span - HTTP POST /workers/.../complete 控制面 HTTP Span - worker.complete 控制面 Worker API Span三个 Span 使用相同 TraceID并通过不同 SpanID 和父 SpanID 表示层级。成功操作设置Ok状态失败操作设置Error和有限error_code不把原始 Token、任务输入或结果写入属性。7.1 W3C Trace ContextW3CWorld Wide Web Consortium万维网联盟Trace Context是在 HTTP 请求之间传播 Trace 身份的标准。Worker Client 把当前上下文写入traceparent请求头控制面提取后创建子 Span。这里的Context上下文是随调用传递 Trace 身份、取消信号和截止时间的数据载体。该标准解决的是“同一次同步 HTTP 调用怎样跨进程保持父子关系”。如果请求头缺失或格式非法服务端创建新的根 Span。RequestID 与 TraceID 也不是同一概念RequestID 用于日志和 API 排查TraceID 用于调用链关系二者可以同时出现但不能互相伪造。7.2 为什么整条 Run 还不是一条 TraceRun 创建、Coordinator 异步扫描和 Worker 稍后领取不是同一个同步调用栈创建 Run 请求结束 - 状态提交到 PostgreSQL - 一段时间后 Coordinator 扫描 - 创建 Dispatch - 又一段时间后 Worker Claim原始 HTTP Context 在第一个请求结束后已经释放。当前 Dispatch 没有持久化 Trace Context因此不能声称“创建 Run 到所有任务完成”自动属于一条 Trace。要跨越异步边界需要决定保存哪些 Trace 字段、保存多久、谁能读取、重试时继续旧 Trace 还是建立 Span Link。Span Link跨度链接用于表示当前 Span 与另一个 Trace 或 Span 有因果关系但不是严格父子调用。这个设计还会增加数据量和访问控制问题所以模块 5 只保证单次 Claim、Heartbeat、Complete HTTP 请求的父子关系整条 Run 继续通过 RunID、TaskKey、WorkerID 和 DispatchID 日志关联。7.3 采样与批处理导出采样Sampling决定一个 Span 是否需要记录和导出。off模式使用NeverSample不创建可记录 Spanstdout模式只用于本地检查。同步导出会让业务 goroutine 在Span.End时等待 Writer。goroutine是 Go 运行时调度的轻量级并发执行单元Writer写入器是接收字节输出的 Go 接口。如果终端或网络后端变慢诊断旁路就会直接增加业务延迟。当前 stdout 模式使用Batch Span Processor批量 Span 处理器结束的 Span 先进入容量为 256 的有界队列后台按最多 64 条一批导出批处理等待上限为 100 ms单次导出超时为 1 秒。这样能够隔离短暂 Writer 延迟但不是零丢失方案。队列已满时允许丢弃 Span服务关闭时会在给定 Context 内刷新并停止TracerProvider调用链提供器它负责统一配置采样、Span 处理和生命周期。观测系统的优先级低于任务状态提交不能为了保存每条 Span 无限阻塞 Complete。8. 告警不是一次条件判断告警Alert是聚合状态持续满足规则后产生的通知。它与“某次检查返回 true”不同还需要保存跨检查周期的状态。模块 5 的规则引擎使用以下状态正常 - 条件首次满足pending - 持续时间满足firing发送一次触发通知 - 异常继续保持 firing不重复发送 - 恢复条件首次满足进入恢复等待 - 恢复持续时间满足resolved发送一次恢复通知如果必需输入缺失规则进入unknown。unknown未知表示当前证据不足既不能确认异常仍在也不能把它误判为恢复。unknown 只在状态变化时通知一次持续缺少数据不会每秒重复发送。模块 5 验证五类规则规则触发条件恢复条件队列堆积队列有任务且没有可用槽位持续 10 秒队列清空或出现可用槽位持续 5 秒Worker 全离线队列有任务但在线 Worker 为 0持续 5 秒至少一个 Worker 在线持续 5 秒租约回收错误30 秒窗口内出现回收错误连续一个窗口没有回收错误Complete 错误率30 秒窗口内至少 20 个样本错误率大于 5%持续 30 秒样本仍足够且错误率低于 1%连接池接近耗尽使用率大于等于 80%持续 10 秒使用率低于 70%持续 10 秒这些数值是本地实验默认值不是SLOService Level Objective服务等级目标。生产阈值必须结合真实流量、允许延迟和支持流程重新确定。8.1 去重和非阻塞通知同一规则使用固定名称作为去重键不带 RunID、TaskKey 或 WorkerID。队列中有 1,000 个任务时系统性 Worker 离线应该形成一条告警而不是 1,000 条通知。通知通过AlertSink接口发送该接口把规则状态机与具体发送方式隔离当前实现是本地 HTTP Webhook。Webhook是由发送方主动向指定 HTTP 地址提交事件的一种回调方式。负载只包含规则名、状态、摘要、开始与结束时间、规则版本和有限标签。当前标签白名单只有component与severity并且两者的值也来自固定集合RunID、TaskKey、Token 或任意调用方字符串会被丢弃。超时、网络错误和 5xx 服务端错误使用有限重试4xx 客户端错误不会通过重复请求解决因此只发送一次。告警 Runner周期执行器使用有界异步发送循环。Webhook 变慢或失败只记录通知结果不阻塞下一轮数据库快照采集也不能阻塞任务调度和状态提交。9. 滚动窗口、最小样本量与滞回9.1 为什么错误率需要时间窗口如果使用进程启动以来的累计 Complete 数量系统运行越久早期大量成功样本越容易掩盖最近故障。模块 5 使用 30 秒滚动窗口只统计近期 Complete 总数、Complete 错误数和租约回收错误数。实现使用 64 个秒级固定桶。每个桶保存某一秒的计数当前秒通过时间戳对数组长度取余定位。读取 30 秒窗口时只汇总时间范围内的桶固定 64 个桶 - 当前秒覆盖对应旧桶 - 查询时汇总最近 30 秒 - 内存占用不随进程运行时间增长固定容量的代价是只能准确查询不超过桶容量的近期窗口。模块 5 的规则固定使用 30 秒因此 64 个桶能够覆盖需求并容纳时间边界。9.2 为什么要有最小样本量只有 1 个 Complete 且恰好失败时错误率是 100%但样本不足以说明系统性问题。Complete 错误率规则要求窗口内至少 20 个样本避免低流量下一个错误立即触发比例告警。最小样本量也有代价低流量系统可能长期达不到 20 个样本此时规则无法仅凭比例发现问题。生产方案可以同时保留“连续错误次数”和“错误率”两类规则但必须分别定义用途不能让一个阈值承担所有流量形态。9.3 为什么触发和恢复阈值不同滞回Hysteresis是使用不同的触发和恢复阈值避免数值在边界附近反复切换状态。例如连接池使用率达到 80%允许触发 降到 79%仍保持 firing 低于 70% 且持续满足恢复Complete 错误率同样使用“大于 5% 触发、低于 1% 恢复”。如果触发和恢复都使用 5%4.9% 与 5.1% 的轻微波动会不断产生 firing 和 resolved增加通知噪声。10. 故障注入怎样证明恢复行为故障注入Fault Injection是在受控测试环境中主动加入错误、延迟、取消、进程退出或资源耗尽用实际结果验证恢复行为。它与在代码中随意返回错误不同每个实验必须明确注入点、次数、预期状态、恢复条件和证据边界。模块 5 使用构造函数传入故障Plan计划。构造函数注入是创建组件时显式提供依赖或测试替身而不是让生产代码读取隐藏的全局开关。Plan 为指定操作保存按顺序消费的 ActionOperationComplete: 第一次 - 返回一次性错误 第二次 - 不再注入调用真实 RepositoryPlan 内部使用锁保护并深拷贝动作列表使并发测试不会共享消费位置。延迟等待响应 Context 取消Plan 关闭后不再执行新动作。生产 HTTP API 没有故障注入路由也不读取WORKLOAD_FAULT_*环境变量。10.1 故障真实度必须分级不同注入方法证明的范围不同级别示例能证明什么不能证明什么调用边界错误Repository 前返回一次错误调用方错误处理和下一次调用恢复真实网络断开、数据库重启和协议行为真实资源竞争用 pgx 单连接池占满连接Context 超时和释放连接后恢复多节点数据库容量与云网络抖动真实进程故障强杀一个测试 Worker 进程租约过期、Attempt 中断和其他 Worker 接管机器掉电、磁盘故障和跨主机网络分区真实时间状态等待租约过期并提交迟到结果旧租约拒绝和新租约完成外部副作用能够自动撤销postgres-unavailable场景是在真实 Repository 前注入一次错误。Repository仓储接口隔离应用逻辑和 PostgreSQL 持久化操作该场景没有停止共享 Docker PostgreSQL只能证明调用方在一次 Repository 错误后能够继续不等价于 PostgreSQL 进程崩溃恢复。Worker 强杀使用真实子进程Kill连接池耗尽使用真实 pgx 连接池这两类实验比函数返回错误更接近目标故障但仍然只发生在本机测试环境。10.2 故障实验需要哪些证据一次可靠实验至少记录环境和输入规模注入操作与发生时机预期中间状态和最终状态日志、指标、Trace 或数据库记录恢复耗时没有覆盖的真实故障。只证明“测试函数返回成功”不够。例如 Worker 强杀实验还应看到第一次 Attempt 为interrupted、第二次 Attempt 为succeeded并确认 Run 最终收敛为succeeded。11. 性能数据怎样避免误导11.1 输入必须可比较模块 5 的每组基准都处理 1,000 个无依赖 Mock模拟任务它们不执行真实业务动作。测试使用同一台 Mac mini M4、同一 PostgreSQL 16 Docker Compose 镜像和相同连接池上限。Docker Compose使用配置文件定义并启动一组本地容器本项目当前只用它提供 PostgreSQL。变量只有三组Worker 数量1、4、16Run 形态单个 1,000 任务 Run或 8 个各 125 任务 Run观测配置off、logs、logs_metrics、logs_metrics_tracing。每个组合先预热 1 轮再正式执行 5 轮。预热Warm-up用于提前完成编译缓存、连接建立和代码路径初始化预热数据不进入正式结果。11.2 吞吐和延迟回答不同问题吞吐Throughput表示单位时间完成的任务数本报告使用 tasks/s。延迟Latency表示一次操作从开始到结束的时间分别记录 Claim、Heartbeat 和 Complete。P50、P95 和 P99 是延迟的百分位数PercentileP50 表示 50% 的样本不超过该值P95 表示 95% 的样本不超过该值P99 表示 99% 的样本不超过该值。P99 比 P50 更能反映少量慢请求但 1,000 个本机样本仍不能代表生产网络中的长期尾延迟。11.3 为什么报告中位数和范围每个组合有 5 个正式吞吐结果。中位数Median是排序后位于中间的值范围Range是最小值到最大值。中位数不容易被单个离群值拉动范围则保留波动信息。离群值Outlier是明显偏离其余样本的观测值。它不应被无声删除因为它可能来自系统真实抖动也不能只用一个离群值判断某种观测配置必然更慢。本文同时报告中位数和最小至最大范围不只报告均值。比较观测开销时还要检查差值是否大于自然波动。如果off的五轮范围已经覆盖logs_metrics的中位数就不能把一个很小的百分比差异解释为确定开销。Trace 基准写入io.Discard它是 Go 标准库提供的丢弃型 Writer接收数据但不保存。因此基准只测量应用侧 Span 创建、排队和批处理不代表网络导出与后端存储成本。基准还直接调用 Repository没有经过生产 HTTP 中间件、Coordinator 聚合查询和 Webhook也没有采集最大队列、连接池峰值、CPU、goroutine 或 Span 丢弃数这些未测路径不能从表格差值反推。11.4 单 Run revision 为什么限制并发revision修订号是 Run 每次状态提交后递增的版本号用于拒绝旧快照覆盖新状态。同一个 Run 的 1,000 个任务共享一条 revision 序列因此每次 Claim 和 Complete 都会竞争同一个 Run 的串行提交点。8 个 Run 各自拥有独立 revision可以并行提交。基准中多 Run 吞吐明显高于单 Run说明增加 Worker 不能绕过单 Run 状态序列化。这不是观测代码制造的新限制而是当前一致性模型的性能证据。基准还记录B/op和allocs/op前者表示每轮分配的内存字节数后者表示内存分配次数。单 Run 路径需要反复加载和推进较大的完整 Run 状态内存分配远高于 8×125 多 Run 路径。这里的数字用于发现优化方向不能直接换算成生产容量。12. 技术选型与替代方案12.1 为什么继续使用log/slog项目已有 slog 请求日志标准库支持结构化字段、级别和 Handler 包装。Zap 和 Zerolog 是两套常用的 Go 高性能结构化日志库当前没有证据证明日志编码是主要瓶颈因此没有引入它们避免同时维护两套日志 API。代价是极端高日志吞吐下性能可能不如专用库只有基准证明 slog 成为主要瓶颈时才值得迁移。12.2 为什么选择 Prometheus 指标模型Prometheus 官方 Go Client 提供 Counter、Gauge、Histogram、标签约束和标准文本输出测试可以直接读取独立 Registry。StatsD 是通过网络发送计数和耗时的轻量指标协议本模块需要显式标签、Histogram 契约和测试内直接读取因此没有选择它。项目也没有自研指标格式因为自研会增加采集、兼容和查询成本。当前只提供/metrics没有部署 Prometheus 服务和 Grafana 可视化后台。12.3 为什么选择 OpenTelemetryOpenTelemetry 的 API 和上下文传播不绑定 Jaeger、Tempo 或某个商业后端Jaeger 和 Tempo 都是存储、查询 Trace 的后端。自动化测试为内存中的采样 Provider 注册SpanRecorderSpan 记录器它保存测试期间已经结束的 Span供断言父子关系、状态和属性stdout 导出器则适合本地检查。没有选择厂商专用SDKSoftware Development Kit软件开发工具包是为了保留后端替换能力没有立即部署 Jaeger 或 Tempo是因为本模块先验证 Span 契约和传播边界。代价是新增 SDK 生命周期、采样和导出队列配置。12.4 为什么先使用进程内告警与 Webhook当前还没有长期 Prometheus 服务因此使用进程内规则状态机能够直接验证持续时间、去重、unknown 和恢复逻辑AlertSink接口使通知方式可以替换。Alertmanager 是 Prometheus 生态中负责告警去重、分组、路由和通知的组件现在接入它会同时增加一套外部配置和部署。项目也没有接入邮件或企业即时通信因为开源仓库不能依赖个人账号和真实凭据。代价是进程停止后告警状态不会保留不能承担生产级监控职责。12.5 为什么使用构造函数级故障注入构造函数注入让每个测试拥有独立故障计划不需要修改生产请求协议。没有开放故障 HTTP API是为了避免远程破坏入口没有只依赖 Mock Repository是因为关键实验仍需要真实 PostgreSQL、pgx 连接池和真实 Worker 进程。代价是部分调用边界故障的真实度有限必须在报告中明确分级。13. 真实验证与性能结果13.1 故障与告警结果模块 5 在本地 PostgreSQL 16 上完成八类故障实验Repository 一次性不可用、Claim 延迟取消、Complete 一次性错误重试、Heartbeat 一次性错误恢复、协调锁检查错误、Worker 进程强杀、租约过期与迟到结果、连接池耗尽。Worker 强杀后第二个 Worker 在约 2.05 秒内接管第一次 Attempt 记录为interrupted第二次 Attempt 为succeededRun 最终为succeeded。租约过期实验中旧 Worker 的迟到 Complete 返回lease_lost新租约提交成功。五类告警使用本地 HTTP Webhook 接收器完成端到端验证。每条规则持续满足后只发送一次firing恢复条件持续满足后只发送一次resolved共收到 10 个生命周期事件。Webhook 返回 5xx 或超过客户端超时时告警 Runner 仍继续采集快照4xx 不重试任意标签被过滤通知失败没有阻塞调度路径。13.2 正式性能矩阵下面两张表给出五轮正式运行的总耗时中位数与最小至最大范围。每个组合都处理 1,000 个任务总耗时越小、吞吐越高。单个 1,000 任务 RunWorker观测配置总耗时中位数范围吞吐中位数范围1off46.754 s46.060~47.27921.39 tasks/s21.15~21.711logs47.214 s46.212~77.67121.18 tasks/s12.87~21.641logs_metrics47.703 s47.528~77.72620.96 tasks/s12.87~21.041logs_metrics_tracing47.651 s47.243~49.97020.99 tasks/s20.01~21.174off25.804 s25.634~30.52138.75 tasks/s32.76~39.014logs26.558 s25.903~30.11637.65 tasks/s33.20~38.614logs_metrics26.087 s25.600~30.06638.33 tasks/s33.26~39.064logs_metrics_tracing26.577 s25.735~30.25637.63 tasks/s33.05~38.8616off27.130 s26.069~67.52836.86 tasks/s14.81~38.3616logs28.046 s27.089~66.97635.66 tasks/s14.93~36.9116logs_metrics27.157 s25.939~30.71436.82 tasks/s32.56~38.5516logs_metrics_tracing28.863 s25.952~69.23234.65 tasks/s14.44~38.538 个各 125 任务的 RunWorker观测配置总耗时中位数范围吞吐中位数范围1off35.604 s35.472~35.70628.09 tasks/s28.01~28.191logs35.624 s35.611~35.65228.07 tasks/s28.05~28.081logs_metrics35.650 s35.474~35.68928.05 tasks/s28.02~28.191logs_metrics_tracing35.576 s35.455~35.65928.11 tasks/s28.04~28.204off7.513 s6.384~7.867133.1 tasks/s127.1~156.64logs7.115 s6.338~8.281140.5 tasks/s120.8~157.84logs_metrics7.072 s6.247~7.662141.4 tasks/s130.5~160.14logs_metrics_tracing6.745 s6.655~6.876148.3 tasks/s145.4~150.316off6.079 s5.877~6.357164.5 tasks/s157.3~170.216logs5.797 s5.693~6.655172.5 tasks/s150.3~175.716logs_metrics5.992 s5.714~6.213166.9 tasks/s160.9~175.016logs_metrics_tracing5.726 s5.663~6.409174.6 tasks/s156.0~176.6单 Run 中三种观测配置相对同组off的中位耗时差值约为 0.10%~6.39%但多个组合存在 30~69 秒或约 77 秒的离群轮次。多 Run 中部分观测配置的中位耗时反而比off低 1.43%~10.22%。观测代码不会因此让状态提交更快负差值说明这些顺序执行的本机样本包含数据库缓存、系统调度和运行时波动。当前证据只支持“应用侧观测开销没有稳定大到压过样本波动”不支持一个可推广到所有输入的固定开销百分比。更明确的结果来自 Run 形态。关闭观测时1、4、16 Worker 的单 Run吞吐中位数为 21.39、38.75、36.86 tasks/s多 Run 为 28.09、133.1、164.5 tasks/s。16 Worker 没有继续提高单 Run 吞吐而多 Run 能利用独立 revision 并行提交符合第 11.4 节的串行点分析。关闭观测时单 Run 每轮内存分配中位数约为 10.7 GiB、分配次数约 1.02 亿多 Run 约为 1.3 GiB、分配次数约 1,350 万。GiBGibibyte吉比字节按 1,073,741,824 字节计算。这里使用的Go BenchmarkGo 基准测试会重复执行目标路径并统计时间和分配这些数值是整轮 1,000 任务状态推进的累计分配量不是进程同时占用的常驻内存但已经说明完整 Run 快照推进值得在后续模块单独分析和优化。完整原始输出保存在本机临时证据目录不提交到 Git。仓库中的验证报告记录环境、正式汇总和限制任何单次运行都不代表生产SLAService Level Agreement服务等级协议或容量承诺。14. 当前限制与下一步模块 5 已经建立应用侧可观测契约和可重复实验但仍有以下边界PostgreSQL 仍是业务事实源日志、指标、Trace 和 Webhook 都允许丢失或重启归零stdout Trace 导出和测试 SpanRecorder 不代表生产采集后端有界队列也不承诺零丢失Run 创建与异步调度之间没有持久化 Trace Context当前只能保证单次 Worker HTTP 请求的父子关系lease_reclaim_errors规则状态机已经验证但生产 Coordinator 遇到回收数据库错误会安全退出不能保证退出前一定等到一个告警周期并发送通知postgres-unavailable是一次性 Repository 包装器错误不等价于真实网络分区或数据库进程崩溃本地 Webhook 没有持久化告警状态、生产认证和真实通知路由当前基准使用 Mock Executor、本机回环网络和单机 PostgreSQL不包含模型调用、真实任务资源消耗或跨主机网络性能矩阵直接调用 Repository不包含生产 HTTP、Coordinator 聚合查询和 Webhook也没有最大队列、连接池峰值、CPU、goroutine 或 Span 丢弃数单 Run revision 是串行提交点完整 Run 状态推进产生较高内存分配仍需后续按性能证据优化。模块 6 将处理另一类尚未解决的问题Worker 当前仍只执行安全 Mock Action没有真实资源和权限隔离。下一阶段会进入受限执行环境、容器和 Kubernetes同时继续沿用模块 5 的日志、指标、Trace、告警和故障验证方法。容器与编排系统会增加新的网络、调度和节点故障不能因为加入 Kubernetes 就默认获得可靠性。15. 总结模块 5 的核心不是增加几个输出端点而是建立一组不能改变业务事实的诊断契约结构化日志定位单次操作低基数指标观察聚合趋势Trace 串联同步请求告警状态机处理持续异常与恢复故障注入用可重复实验验证失败路径。这组实现还给出四个重要结论状态事务与观测旁路必须分开不能用事务外 Counter 伪造业务事实指标类型取决于源数据语义累计值不能被当作多次 Histogram 样本W3C Trace Context 能串联一次 HTTP 请求但跨异步阶段需要额外持久化设计性能结论必须同时给出输入、轮次、中位数、范围和离群值不能只选一个最好或最差数字。可观测性让已经存在的执行语义更容易解释和验证但它不会自动修复状态机、撤销外部副作用也不会替代资源隔离和生产监控平台。参考资料Golog/slogpackagePrometheus Metric TypesOpenTelemetry Go DocumentationW3C Trace ContextpgxpoolStat.AcquireDurationGo Benchmark package documentation项目源码本文对应模块 5。完整源码、故障实验、性能报告和后续模块见 AI Workload Platform GitHub 仓库。