返回文章列表

AiVedio:可观测性与生产排障

围绕 task_id、attempt_id、trace_id、provider_job_id、Metrics、Logs、Traces、Business Events、SLO、告警、Runbook、未知结果排查、媒体链路诊断和安全止损动作,设计 AI 视频平台的可观测性与生产排障体系。

第 22 章:可观测性与生产排障

本章主线:AI 视频平台的可观测性不能停留在“服务有没有报错”,而要能够回答一条具体生成任务为什么慢、卡在哪、第三方是否已经受理、是否发生重复生成、媒体文件是否已安全回源、用户额度是否正确结算,以及当前可以采取什么不会扩大损失的处置动作。

对这类分钟级、跨服务、跨消息队列、跨第三方供应商的异步任务,最重要的不是制造一条持续几十分钟的巨大 Trace,而是建立以 task_id 为业务主线、以 attempt_id 区分副作用尝试、以 trace_id 描述单次因果链、以 provider_job_id 对齐外部事实的关联体系。


22.1 本章要解决的业务问题

普通 HTTP 服务的排障往往围绕一次请求展开:请求进入、调用数据库、返回结果。AI 视频生成平台不同,一次用户操作可能经历数分钟甚至数十分钟,跨越 API、PostgreSQL、Outbox、RocketMQ、调度器、第三方模型、回调、轮询、对象存储、FFmpeg Worker、CDN 和通知网关。

生产系统必须能回答以下问题:

  1. 任务是否真的卡住了?

    • 只是前端通知断开,还是后端状态没有推进?
    • 是排队、供应商处理、回源下载还是转码阶段超时?
    • Redis 缓存是否过期,而 PostgreSQL 中的事实状态其实已经完成?
  2. 第三方供应商是否退化?

    • 是提交接口 429 上升,还是已经受理但完成耗时变长?
    • 是某个模型、地区、账号配额或输入类型异常,还是供应商全局故障?
    • 回调变慢时,轮询补偿是否仍然有效?
  3. 是否发生了不可逆或付费副作用?

    • Provider Submit 超时后,对方是否已经创建任务?
    • MQ 重复投递是否触发了第二次生成?
    • 用户是否被重复结算,平台是否承担了重复供应商成本?
  4. 媒体数据面是否健康?

    • 第三方临时 URL 是否即将失效?
    • 下载吞吐是否异常、checksum 是否一致、ffprobe 是否通过?
    • FFmpeg 是 CPU 饱和、GPU 槽位不足、临时盘打满,还是输入文件损坏?
  5. 用户体验是否达标?

    • 创建任务接口是否可用?
    • 排队时间、生成完成时间、首个可播放时间和最终导出时间是否满足目标?
    • 用户看到的进度是否新鲜,而不是一个长期停留在 99% 的伪进度?
  6. 告警是否能驱动行动?

    • 值班人员收到告警后,是否知道影响范围、责任阶段、止损动作和回滚条件?
    • 告警是用户症状,还是一个无关紧要的基础设施波动?
    • 同一事故是否触发几百条重复告警?

本章的最终产物不是一堆 Dashboard,而是一套完整闭环:

发现异常
  → 判断影响范围
  → 定位到具体阶段和依赖
  → 区分“未知结果”与“明确失败”
  → 选择安全止损动作
  → 修复业务数据和资源计数
  → 复盘并增加长期防线

22.2 核心设计原则

22.2.1 以业务对象为中心,而不是以机器实例为中心

用户关心的是“任务 T123 为什么没完成”,而不是“Pod generation-service-7c9d 的 CPU 是否为 78%”。

因此,平台需要同时保留两类视角:

  • 业务视角:任务、尝试、供应商作业、资产、计费流水、项目版本。
  • 基础设施视角:服务实例、节点、区域、数据库连接池、MQ 消费组、Redis 分片、GPU Worker。

排障入口应优先从 task_id 进入,再下钻到具体实例和依赖;不能要求值班人员先猜是哪台机器出了问题。

22.2.2 指标、日志、追踪和业务事件必须可关联

四类信号职责不同:

信号适合回答的问题不适合承担的职责
Metrics是否普遍异常、趋势如何、是否达到告警阈值保存单任务完整细节
Logs某次执行发生了什么、错误上下文是什么高效计算全局百分位和长期趋势
Traces一次因果链跨了哪些服务、时间花在哪里充当分钟级任务的唯一事实记录
Business Events任务状态为何变化、发生了哪些业务副作用替代基础设施监控

这四类数据必须通过稳定标识关联,但不能把高基数业务 ID 塞进时序指标标签。

22.2.3 task_id 是长任务主线,trace_id 不是

建议统一以下标识:

标识生命周期作用
task_id一次生成任务全生命周期跨分钟、跨服务、跨重试的业务主键
attempt_id一次具有外部副作用的尝试区分第一次和后续 Provider Submit、回源或转码尝试
trace_id一次同步或短时异步因果链定位一次 API、一次消息消费、一次回调或一次 Worker 执行
provider_job_id供应商侧任务生命周期与供应商查询、回调和账单对齐
message_id一条 MQ 消息识别重复投递和消费
event_id一条业务事件去重、审计和重放
asset_id一份平台媒体资产对齐对象存储、转码产物和 CDN 发布
billing_txn_id一次额度流水对齐预占、结算、释放和退款

不要把整条生成任务做成一条持续几十分钟的 Trace。更稳妥的做法是:

  • API 创建任务形成一条 Trace。
  • Outbox Relay 投递形成一条 Trace。
  • Scheduler 消费消息形成一条 Trace。
  • Provider Submit 形成一条 Trace。
  • Callback 或 Polling 形成新的 Trace。
  • Output Fetch 和 Media Processing 各自形成 Trace。
  • 在消息中传播 Trace Context;当重试、延迟或系统边界导致父子关系不再准确时,使用 Span Link 表达关联。
  • 所有 Trace、日志和业务事件都携带 task_idattempt_id

22.2.4 低基数放 Metrics,高基数放 Logs、Traces 和事件库

适合做指标标签的维度:

service
operation
stage
provider
model_family
region
result_class
error_class
tenant_tier
worker_pool
codec

不适合做指标标签的维度:

task_id
user_id
tenant_id(租户数量很大时)
provider_job_id
asset_id
prompt
object_key
URL
error_message 原文

每一种标签组合都会产生新的时间序列。把 task_id 放入 Prometheus 标签,任务量一上来就会造成基数爆炸、内存增长、查询变慢和监控系统自身故障。

22.2.5 观察“时间年龄”,不能只看“数量”

队列深度为 10 万不一定是事故;如果消费能力很高,可能只是短时流量尖峰。队列深度只有 1 也不代表健康;那一条消息可能已经卡了两小时。

因此至少同时观察:

queue_depth
oldest_ready_age_seconds
consumer_lag_seconds
arrival_rate
completion_rate
retry_rate
DLQ_rate

其中 oldest_ready_age_seconds 往往比单纯积压条数更接近用户影响。

22.2.6 每个不可逆副作用都必须可观测

以下动作必须记录开始、结果、幂等键和外部标识:

  • 额度预占、结算、释放和退款。
  • 向供应商提交生成任务。
  • 取消供应商任务。
  • 下载第三方输出。
  • 发布平台资产。
  • 创建最终渲染任务。

尤其是 Provider Submit,不能只记录“HTTP 超时”,还要记录:

attempt_id
provider_request_id
client_idempotency_key
request_started_at
request_deadline_at
response_status
provider_job_id
outcome = ACCEPTED | REJECTED | UNKNOWN
reconcile_status

22.2.7 SLO 按用户旅程和责任边界分层

“整条视频生成 10 分钟内完成”可能受到供应商模型本身影响。平台仍然应该定义端到端 SLO,但同时必须拆出平台可控阶段:

  • 创建任务 API SLO。
  • 排队和调度 SLO。
  • Provider Submit SLO。
  • 供应商执行观测指标。
  • 回调/轮询发现结果 SLO。
  • 输出回源 SLO。
  • 媒体处理 SLO。
  • 通知新鲜度 SLO。

这样才能区分:是平台自身退化,还是外部供应商变慢。

22.2.8 告警必须可行动,并且优先基于用户症状

好的 Page 告警应当满足:

  • 正在影响真实用户或即将快速消耗错误预算。
  • 值班人员现在采取行动能够降低损失。
  • 告警包含影响范围、关键图表和 Runbook。
  • 同一根因不会衍生大量重复 Page。

CPU 高但任务吞吐正常时,通常先记录或发 Ticket;任务成功率快速下降、最老排队时间持续上升时,才更适合 Page。

22.2.9 可观测系统故障不能拖垮业务系统

遥测导出必须:

  • 异步、批量、有界缓冲。
  • 设置超时和重试上限。
  • 后端不可用时允许丢失非关键 Trace 或普通日志。
  • 不允许无限阻塞业务 goroutine。
  • 关键业务事件仍写 PostgreSQL,不依赖日志平台作为唯一事实源。

原则是:宁可监控暂时变暗,也不能让监控把生成链路拖死。


22.3 详细架构和组件职责

22.3.1 总体架构

Go API / Scheduler / Provider Adapter / Callback / Poller / Workers

        ├── Metrics ──────────────► Metrics Collector / Prometheus
        │                              ├── Recording Rules
        │                              ├── SLO / Burn Rate
        │                              └── Alert Manager

        ├── Logs ─────────────────► Log Agent / OTel Collector
        │                              └── Log Storage & Search

        ├── Traces ───────────────► OTel Collector Gateway
        │                              ├── Sampling / Tail Sampling
        │                              └── Trace Backend

        └── Business Events ──────► PostgreSQL task_events / event pipeline
                                       └── Task Observability Read Model

PostgreSQL / Redis / RocketMQ / Object Storage / CDN / GPU Nodes
        └── Exporters & Native Metrics ─► Metrics Platform

Synthetic Probes
        ├── 创建任务探针
        ├── MQ 往返探针
        ├── 对象存储上传下载探针
        └── CDN 播放探针

Single-Task Debug Console
        ├── PostgreSQL 事实状态
        ├── 业务事件时间线
        ├── Trace 链接
        ├── 日志查询
        ├── MQ 与供应商状态
        ├── 媒体资产与校验结果
        └── 受控修复动作

22.3.2 应用侧埋点层

每个 Go 服务统一接入内部 observability package,负责:

  • 从 HTTP Header、RocketMQ 消息属性中提取 Trace Context。
  • 创建 Span,设置统一资源属性,如 service.nameservice.versiondeployment.environmentcloud.region
  • trace_idspan_idtask_idattempt_id 注入结构化日志。
  • 暴露 RED、USE 和业务指标。
  • 对错误进行规范化分类,而不是把原始错误字符串作为指标标签。
  • 对敏感字段做白名单式采集和脱敏。

建议所有服务使用统一字段名,避免出现:

taskId / task_id / generation_id / jobId
provider / vendor / model_provider
error / err / error_message / exception

统一后才能跨服务检索和建立通用 Dashboard。

22.3.3 OpenTelemetry Collector 层

Collector 可以分两层:

  1. 节点或边车层

    • 接收本机应用 OTLP。
    • 批量、限流和短时缓冲。
    • 补充节点、Pod、区域等资源属性。
  2. 网关层

    • 多副本部署。
    • 做路由、过滤、采样和后端导出。
    • 对错误 Trace、慢 Trace、供应商超时 Trace做保留。
    • 对成功且低价值的高频 Trace 做比例采样。

不要在应用中硬编码具体观测后端地址和厂商语义;应用只负责标准化发出信号,Collector 负责后端适配。

22.3.4 Metrics 平台

Metrics 平台负责:

  • 抓取或接收服务指标。
  • 保存低基数时间序列。
  • 通过 Recording Rule 预计算常用比例和分位数。
  • 计算 SLI、错误预算和 Burn Rate。
  • 触发告警。

Metrics 用来回答“是否普遍异常”,不直接存储单个任务详情。

22.3.5 日志平台

日志必须是结构化 JSON,至少包含:

{
  "timestamp": "2026-06-25T10:03:21.456Z",
  "level": "ERROR",
  "service": "provider-adapter",
  "service_version": "2026.06.25-3",
  "region": "us-west-2",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "task_id": "gen_01J...",
  "attempt_id": "att_01J...",
  "provider": "provider_a",
  "model_family": "video-gen-v3",
  "operation": "submit_generation",
  "outcome": "UNKNOWN",
  "error_class": "DEADLINE_EXCEEDED",
  "provider_request_id": "req_xxx",
  "elapsed_ms": 10012,
  "message": "provider submit timed out after request body was sent"
}

日志中的 message 供人阅读,查询与聚合应尽量依赖稳定字段。

22.3.6 Trace 平台

建议的 Span 层级包括:

HTTP POST /generation-tasks
  ├── auth.verify
  ├── moderation.check_input
  ├── billing.reserve
  ├── db.create_task_and_outbox
  └── response

MQ consume generation.dispatch
  ├── scheduler.acquire_tenant_slot
  ├── scheduler.acquire_provider_slot
  ├── route.select_provider
  └── mq.publish provider.submit

Provider submit
  ├── db.create_attempt
  ├── http.provider.submit
  └── db.persist_provider_result

Callback handling
  ├── signature.verify
  ├── callback.deduplicate
  ├── state.transition
  └── mq.publish output.fetch

Output fetch
  ├── validate_url
  ├── download.stream
  ├── checksum.verify
  ├── object_storage.put
  └── media.probe

Media processing
  ├── ffmpeg.transcode_proxy
  ├── thumbnail.generate
  ├── waveform.generate
  └── asset.publish

需要避免:

  • 每一帧创建一个 Span。
  • 每次轮询都创建大量详细子 Span,并永久保留。
  • 在 Span 属性中记录完整 Prompt、签名 URL 或用户隐私信息。

22.3.7 业务事件与任务可观测读模型

task_events 是排障的重要事实来源。它记录状态变化和关键业务动作,而不是普通调试日志。

示例事件:

TASK_CREATED
CREDIT_RESERVED
DISPATCH_ENQUEUED
PROVIDER_SUBMIT_STARTED
PROVIDER_ACCEPTED
PROVIDER_SUBMIT_OUTCOME_UNKNOWN
PROVIDER_STATUS_CHANGED
CALLBACK_RECEIVED
OUTPUT_FETCH_STARTED
OUTPUT_PERSISTED
MEDIA_PROCESSING_STARTED
ASSET_PUBLISHED
CREDIT_SETTLED
TASK_SUCCEEDED
TASK_FAILED
TASK_CANCEL_REQUESTED
TASK_CANCELLED

可以异步构建 task_observation_snapshot 读模型,为排障页面提供快速查询,但它不是事实源;出现冲突时以 PostgreSQL 中的任务、尝试、事件和计费流水为准。

22.3.8 单任务全链路排障页面

这是本章最能体现资深程度的设计之一。输入 task_id 后,页面至少展示:

A. 任务摘要

当前状态
当前阶段
状态版本
租户等级
模型与供应商
创建时间
已运行时长
预计阶段截止时间
last_progress_at
是否进入人工处理

B. 标识关联

task_id
attempt_id 列表
provider_job_id
trace_id 列表
message_id 列表
asset_id
billing_txn_id

C. 状态时间线

按服务端时间展示所有业务事件,并标出:

  • 重复事件。
  • 乱序回调。
  • 被状态机拒绝的迁移。
  • 长时间空档。
  • 人工操作。

D. 尝试与副作用

每个 attempt_id 展示:

开始时间
幂等键
请求是否已发出
供应商 HTTP 结果
本地 outcome
provider_job_id
最后一次对账时间
是否产生供应商成本

E. 队列和资源

当前消息所在 Topic / Consumer Group
入队时间
被消费时间
重试次数
DLQ 状态
租户槽位
供应商槽位
Worker 槽位
租约到期时间

F. 媒体链路

第三方 URL 到期时间
下载尝试
HTTP Range / Content-Length
已下载字节数
checksum
ffprobe 结果
转码退出码
FFmpeg 进度
临时盘占用
对象存储 key
CDN 发布状态

G. 计费链路

预估成本
额度预占
供应商实际成本
平台结算
退款或释放
对账差异

H. 安全修复动作

只提供经过状态机和权限校验的动作:

  • 立即查询供应商,不重新提交。
  • 重试输出下载。
  • 重投媒体处理消息。
  • 修复已确认泄漏的并发槽位。
  • 触发计费对账。
  • 标记人工审核。

“重新提交生成”必须是高风险动作:只有确认供应商未受理,或业务明确允许承担重复成本时才能执行。


22.4 文字版时序图

1. Browser 调用 API Gateway 创建生成任务,并携带 Idempotency-Key。

2. API Gateway 创建 trace_id=A,将上下文传给 Generation Service。

3. Generation Service:
   - 记录 API RED 指标;
   - 校验参数、审核输入、预占额度;
   - 在 PostgreSQL 同一事务中创建 generation_task、credit_ledger、outbox_event;
   - 写 TASK_CREATED 与 CREDIT_RESERVED 业务事件;
   - 日志携带 task_id、trace_id=A。

4. Outbox Relay 扫描事件,创建 trace_id=B:
   - 读取原始 trace context,按因果关系建立 parent 或 span link;
   - 向 RocketMQ 发送消息;
   - 消息属性携带 task_id、event_id、traceparent、created_at;
   - 记录 send latency、send result 和 message_id。

5. Scheduler 消费消息,创建 trace_id=C:
   - 计算 message queue age;
   - 获取租户、供应商和模型并发槽位;
   - 记录调度等待时间和路由决策;
   - 创建 attempt_id;
   - 发布 Provider Submit 消息。

6. Provider Adapter 创建 trace_id=D:
   - 写 PROVIDER_SUBMIT_STARTED;
   - 使用稳定 client idempotency key 调用供应商;
   - 若明确成功,保存 provider_job_id 并写 PROVIDER_ACCEPTED;
   - 若明确拒绝,按错误分类处理;
   - 若请求已发出但本地超时,写 outcome=UNKNOWN,不得直接重提;
   - 记录 provider submit latency、429、5xx、timeout 和 unknown outcome 指标。

7. 供应商完成后回调 Callback Gateway,创建 trace_id=E:
   - 验签、防重放、去重;
   - 用 provider_job_id 找到 task_id 和 attempt_id;
   - 记录 callback lag;
   - 状态机以版本号或条件更新;
   - 重复或乱序回调只记事件,不重复触发副作用。

8. 若回调未到,Polling Service 创建 trace_id=F:
   - 按退避计划查询供应商;
   - 与回调共用状态归一化和幂等逻辑;
   - 记录 poll lag、poll result 和 callback/poll discrepancy。

9. Output Fetch Worker 创建 trace_id=G:
   - 读取 URL 到期时间;
   - 流式下载到对象存储;
   - 记录首字节时间、吞吐、下载时长、重试和 checksum;
   - URL 失效时先尝试向供应商刷新结果地址,不重新生成视频。

10. Media Worker 创建 trace_id=H:
    - ffprobe 校验格式、时长、分辨率和流信息;
    - FFmpeg 生成代理视频、缩略图、波形或 HLS;
    - 记录进度、处理时长、实时因子、CPU/GPU、内存和临时盘;
    - 产物全部成功后原子发布资产。

11. Task State Service:
    - 写 ASSET_PUBLISHED、CREDIT_SETTLED、TASK_SUCCEEDED;
    - 更新 PostgreSQL 事实状态;
    - Redis 只更新短期状态缓存和通知加速;
    - Notification Gateway 通过 SSE/WebSocket 通知浏览器。

12. 单任务排障页面可用 task_id 将 A~H 多条 Trace、业务事件、日志、MQ 消息、provider_job_id、asset_id 和 billing_txn_id 关联起来。

22.5 关键数据结构、数据库表和消息字段

22.5.1 task_attempts

CREATE TABLE task_attempts (
    attempt_id              uuid PRIMARY KEY,
    task_id                 uuid NOT NULL,
    stage                   text NOT NULL,
    attempt_no              integer NOT NULL,
    idempotency_key         text NOT NULL,
    provider                text,
    provider_job_id         text,
    provider_request_id     text,
    outcome                 text NOT NULL,
    error_class             text,
    started_at              timestamptz NOT NULL,
    deadline_at             timestamptz,
    finished_at             timestamptz,
    last_reconciled_at      timestamptz,
    trace_id                text,
    created_at              timestamptz NOT NULL DEFAULT now(),
    UNIQUE (task_id, stage, attempt_no),
    UNIQUE (provider, provider_job_id)
);

outcome 建议归一化为:

STARTED
ACCEPTED
REJECTED
SUCCEEDED
FAILED
CANCELLED
UNKNOWN

其中 UNKNOWN 不是失败,而是“当前无法确认外部副作用是否发生”。它需要进入对账流程。

22.5.2 task_events

CREATE TABLE task_events (
    event_id          uuid PRIMARY KEY,
    task_id           uuid NOT NULL,
    attempt_id        uuid,
    event_type        text NOT NULL,
    state_before      text,
    state_after       text,
    state_version     bigint,
    source_service    text NOT NULL,
    source_kind       text NOT NULL,
    source_id         text,
    trace_id          text,
    payload           jsonb NOT NULL DEFAULT '{}'::jsonb,
    occurred_at       timestamptz NOT NULL,
    recorded_at       timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_task_events_task_time
ON task_events(task_id, occurred_at, event_id);

应同时保留:

  • occurred_at:事件声称发生的时间。
  • recorded_at:平台实际落库时间。

两者差值可用于分析回调延迟、消息延迟和时钟偏差。

22.5.3 task_stage_deadlines

可选择单独建表,也可作为任务字段:

CREATE TABLE task_stage_deadlines (
    task_id             uuid NOT NULL,
    stage               text NOT NULL,
    entered_at          timestamptz NOT NULL,
    expected_deadline   timestamptz NOT NULL,
    hard_deadline       timestamptz NOT NULL,
    last_progress_at    timestamptz,
    watchdog_status     text NOT NULL,
    PRIMARY KEY (task_id, stage)
);

它使“卡住”成为可查询、可告警的明确条件,而不是依赖某个工程师临时写 SQL 猜测。

22.5.4 RocketMQ 消息信封

{
  "schema_version": 3,
  "event_id": "evt_01J...",
  "message_id": "msg_01J...",
  "event_type": "OUTPUT_FETCH_REQUESTED",
  "task_id": "gen_01J...",
  "attempt_id": "att_01J...",
  "tenant_tier": "enterprise",
  "created_at": "2026-06-25T10:03:21.456Z",
  "not_before": "2026-06-25T10:03:31.456Z",
  "traceparent": "00-4bf92f...-00f067...-01",
  "tracestate": "...",
  "retry_count": 1,
  "payload": {
    "provider": "provider_a",
    "provider_job_id": "job_xxx"
  }
}

注意:

  • task_id 用于业务关联。
  • event_id 用于消费端去重。
  • message_id 用于定位 MQ 传递。
  • traceparent 用于当前因果链传播。
  • 重试消息不应修改业务幂等键。

22.5.5 错误分类

不要直接把供应商原始错误字符串作为监控维度。建议分层:

error_domain:
  PLATFORM | DATABASE | REDIS | MQ | PROVIDER | MEDIA | STORAGE | CLIENT

error_class:
  INVALID_ARGUMENT
  AUTHENTICATION_FAILED
  RATE_LIMITED
  QUOTA_EXHAUSTED
  DEADLINE_EXCEEDED
  CONNECTION_FAILED
  PROVIDER_5XX
  OUTCOME_UNKNOWN
  CALLBACK_SIGNATURE_INVALID
  OUTPUT_URL_EXPIRED
  MEDIA_INVALID
  CHECKSUM_MISMATCH
  TEMP_DISK_FULL
  RESOURCE_EXHAUSTED
  STATE_CONFLICT
  INTERNAL

retry_class:
  NON_RETRYABLE
  RETRY_SAFE
  RETRY_AFTER_RECONCILE
  MANUAL_REVIEW

22.5.6 核心指标字典

API RED

指标类型关键标签
api_requests_totalCounterservice、operation、status_class
api_request_duration_secondsHistogramservice、operation
api_inflight_requestsGaugeservice、operation
api_rejected_totalCounterreason、tenant_tier

Worker USE

指标含义
worker_slot_utilization_ratio槽位利用率
worker_queue_wait_seconds等待 Worker 槽位时长
worker_saturation_ratio待执行数与可用槽位关系
worker_errors_total按稳定错误类统计
worker_heartbeat_age_secondsWorker 心跳新鲜度

RocketMQ

指标含义
mq_ready_messages可消费消息数
mq_oldest_ready_age_seconds最老待消费消息年龄
mq_consumer_lag_seconds最老未完成消息年龄
mq_consume_duration_seconds消费处理时长
mq_retry_total重试量
mq_dlq_total进入死信量

Provider

指标含义
provider_submit_total提交量,按结果分类
provider_submit_duration_seconds提交接口时延
provider_unknown_outcome_total已发出但结果未知的提交量
provider_http_errors_total429、5xx、网络错误
provider_job_duration_seconds供应商从受理到终态耗时
provider_callback_lag_seconds供应商完成到平台收到回调的延迟
provider_poll_discovery_lag_seconds轮询发现终态延迟
provider_completion_success_ratio供应商作业成功率

Output Fetch

指标含义
output_fetch_time_to_first_byte_seconds首字节时延
output_fetch_duration_seconds完整下载时长
output_fetch_bytes_total下载字节
output_fetch_throughput_bytes_per_second下载吞吐
output_url_expired_totalURL 失效次数
output_checksum_mismatch_total校验失败次数
output_fetch_retry_total下载重试次数

FFmpeg / GPU

指标含义
media_job_duration_seconds媒体任务耗时
media_realtime_factor处理耗时 ÷ 媒体时长
media_job_exit_total按退出类统计
media_frames_processed_total已处理帧数
media_progress_age_seconds最后进度更新时间
worker_cpu_utilization_ratioCPU 使用率
worker_gpu_utilization_ratioGPU 使用率
worker_gpu_memory_utilization_ratioGPU 显存使用率
worker_temp_disk_free_bytes临时盘剩余空间

PostgreSQL

指标含义
db_pool_in_use_connections使用中连接数
db_pool_idle_connections空闲连接数
db_pool_wait_total因无连接发生的等待次数
db_pool_wait_duration_seconds等待连接累计时长
db_query_duration_secondsSQL 延迟
db_lock_wait_seconds锁等待
db_deadlocks_total死锁次数
db_replication_lag_seconds副本延迟

Go 的 database/sql 应同时导出 DB.Stats() 中的连接使用和等待指标,并与 PostgreSQL pg_stat_activitywait_event_type、慢 SQL 和锁信息交叉验证。

Redis

指标含义
redis_command_duration_seconds命令延迟
redis_pool_wait_seconds客户端连接池等待
redis_timeouts_total超时次数
redis_errors_total错误分类
redis_evicted_keys_total驱逐量
redis_blocked_clients被阻塞客户端
redis_slot_lease_reconcile_gap并发槽位账实差异

业务与成本

task_created_total
task_succeeded_total
task_failed_total
task_cancelled_total
task_stage_duration_seconds
task_stuck_total
duplicate_provider_job_total
credit_reserve_failure_total
billing_reconcile_difference_amount
provider_cost_amount
cost_per_accepted_job
cost_per_successful_video
refund_ratio

cost_per_successful_video 可按以下方式计算:

供应商生成成本
+ 输出下载流量成本
+ 媒体处理 CPU/GPU 成本
+ 存储成本
+ CDN 成本
--------------------------------
成功发布的视频数

它是经营指标,不应替代不可变计费流水和供应商账单对账。


22.6 正常流程

22.6.1 请求进入

  • API Gateway 创建或继续 Trace。
  • 记录请求数、时延、状态码和拒绝原因。
  • 不在指标标签中记录用户 ID、任务 ID 或原始 URL。
  • Generation Service 创建 task_id 后,立即把它写入当前 Span 和后续日志。

22.6.2 事务创建任务

  • 在同一 PostgreSQL 事务中写任务、额度流水和 Outbox。
  • 事务成功后记录 TASK_CREATED
  • 事务失败时,Span 标记错误,但不能留下“任务创建成功”的业务事件。
  • 指标只反映结果数量,事实仍以数据库事务为准。

22.6.3 消息传递

  • Outbox Relay 发送消息时记录发送耗时和结果。
  • 消费者收到消息后计算:
message_queue_age = consume_started_at - message.created_at
  • 消费完成时记录处理时长、结果和重试分类。
  • 正常重投消息可以产生新 message_id,但必须保留原 event_id 或业务幂等键。

22.6.4 供应商提交

  • 创建 attempt_id,先落库再提交。
  • 请求开始前写 PROVIDER_SUBMIT_STARTED
  • 明确成功时保存 provider_job_id
  • 明确失败时按错误类决定是否重试。
  • HTTP 超时且请求可能已送达时,标记 UNKNOWN,进入查询和对账,而不是自动重提。

22.6.5 状态发现

  • 回调路径记录签名校验结果、供应商事件时间和到达时间。
  • 轮询路径记录查询周期、退避次数和发现终态延迟。
  • 回调和轮询调用同一个状态归一化与状态机更新函数。
  • 状态迁移成功后,只发布一次下游事件。

22.6.6 回源和媒体处理

  • Output Fetch 记录 URL 剩余有效期、下载进度、吞吐和 checksum。
  • FFmpeg 使用可机器解析的进度输出,定期更新 Worker 内存态或低频进度事件。
  • 不要每 0.5 秒更新 PostgreSQL 主任务行;可按时间或进度阈值节流,例如 5~15 秒或每推进 5% 写一次短期状态。
  • 最终产物校验通过后再发布 ASSET_PUBLISHED

22.6.7 完成与通知

  • PostgreSQL 提交终态和计费结算。
  • Redis 更新短期缓存,但不是终态事实源。
  • 通知失败不应把已成功任务回滚为失败;Notification Gateway 通过快照和重连补偿。
  • 记录从任务终态到用户收到通知的延迟。

22.7 异常流程和竞态条件

22.7.1 “任务卡住”是假象

可能原因:

  • PostgreSQL 已完成,Redis 缓存仍是旧状态。
  • SSE/WebSocket 断线,浏览器未刷新快照。
  • 通知事件丢失,但任务事实已更新。
  • 排障读模型消费延迟。

排查顺序:

PostgreSQL 当前状态
→ task_events 最后事件
→ 当前阶段 deadline 与 last_progress_at
→ MQ 消息状态
→ Redis 缓存版本
→ 通知链路

不能仅凭前端显示或 Redis 值判定任务失败。

22.7.2 Provider Submit 超时

这是最危险的异常之一:

平台发送请求
→ 供应商已经创建任务
→ 返回包在网络中丢失或本地超时
→ 平台不知道 provider_job_id

如果立即重试,可能产生两个真实生成任务和两份成本。

正确处理:

  1. 将 attempt 标记为 UNKNOWN
  2. 使用客户端幂等键、provider request ID 或供应商查询接口对账。
  3. 暂时不释放已预占额度,也不直接重复结算。
  4. 达到 reconcile deadline 后进入人工或规则化决策。
  5. 指标单独统计 provider_unknown_outcome_total,并对比例升高告警。

22.7.3 回调与轮询竞态

回调和轮询可能同时发现完成:

Callback:RUNNING → SUCCEEDED
Polling:RUNNING → SUCCEEDED

必须通过:

  • 条件更新或乐观版本。
  • 唯一业务事件约束。
  • 下游事件 Outbox 幂等。

保证只有一个路径真正触发 Output Fetch。另一路只记录“重复终态观察”。

22.7.4 取消与完成竞态

用户请求取消
供应商同时完成

平台需要定义优先级和计费语义。例如:

  • 供应商已经成功并产生费用,平台可将任务标记为 SUCCEEDED_AFTER_CANCEL_REQUEST 或按产品规则隐藏结果,但不能假装供应商未收费。
  • 取消请求已被供应商确认且任务未完成,则释放未结算额度。
  • 不能仅根据事件到达顺序推断真实发生顺序,应参考供应商事件时间和状态机规则。

22.7.5 队列深度正常,但最老消息很旧

可能是:

  • 某个分区或顺序队列被 poison message 阻塞。
  • 某个租户的消息因资源条件永远无法满足。
  • 消费者本地缓冲取走了消息但处理线程卡住。

因此必须联合检查 oldest_ready_ageconsumer_lag、本地缓存消息数和处理时长。

22.7.6 队列很深,但系统未失控

突发流量时队列深度会快速上升,但若:

completion_rate > arrival_rate
oldest_age 未持续恶化
预计清空时间可接受

可以只触发 Ticket 或容量观察,不必立即 Page。

预计清空时间可粗略估算为:

backlog / (completion_rate - arrival_rate)

当完成速率不高于到达速率时,积压不会自行消失,需要限流、扩容或降级。

22.7.7 FFmpeg 有进程但无进度

可能原因:

  • 输入 URL 卡住。
  • 解码器在坏帧处阻塞。
  • GPU 驱动异常。
  • 临时盘写满。
  • 子进程仍存活,但 stdout/stderr 管道未被正确读取导致阻塞。

需要联合检查:

process_alive
last_progress_at
frame/out_time
CPU/GPU 利用率
read/write bytes
临时盘
子进程 stderr

只看“进程存在”会误判为健康。

22.7.8 遥测导出故障

若 Collector 或日志后端不可用:

  • 应用导出队列可能堆满。
  • 内存上涨。
  • 导出 goroutine 阻塞。
  • 业务请求被连带拖慢。

应用必须记录自身遥测丢弃数、队列长度和导出失败数,并采用有界队列。普通 Trace 可以丢弃,业务事实不能丢。

22.7.9 时钟偏差造成错误延迟

Callback Lag、Queue Age 和跨服务时延依赖时间戳。节点时钟漂移可能制造负延迟或虚假长延迟。

措施:

  • 所有节点使用可靠时间同步。
  • 优先使用同一系统内的服务端接收时间计算关键告警。
  • 同时保留供应商事件时间和平台接收时间。
  • 发现负值或异常偏差时单独记录 clock skew 指标。

22.7.10 采样丢失最重要的异常 Trace

纯头部 1% 采样可能恰好丢掉低频超时、未知结果和重复计费链路。

建议:

  • 所有错误 Trace 保留。
  • 所有超过阶段阈值的慢 Trace 保留。
  • 所有 outcome=UNKNOWN、状态冲突、checksum mismatch、重复生成 Trace 保留。
  • 正常成功请求再按比例采样。
  • 即使 Trace 被采样掉,业务事件和核心错误指标仍必须存在。

22.8 幂等、一致性、重试和补偿设计

22.8.1 遥测不是业务事实源

Metrics 可能因重启、抓取失败或网络故障出现短暂误差;日志可能重复或丢失;Trace 可能采样。

以下信息必须保存在 PostgreSQL:

  • 任务事实状态。
  • 供应商尝试和 provider_job_id
  • 计费流水。
  • 资产发布状态。
  • 关键业务事件。

监控系统用于发现和解释问题,不能作为余额、任务终态或退款依据。

22.8.2 业务事件幂等

  • 每个事件有全局唯一 event_id
  • 消费端维护 Inbox 去重或依靠唯一业务约束。
  • 状态更新使用条件:
UPDATE generation_tasks
SET state = $new_state,
    version = version + 1
WHERE task_id = $task_id
  AND version = $expected_version
  AND state = ANY($allowed_from_states);

影响行数为 0 时,不应盲目重试状态写入,而应重新读取并判断是重复、乱序还是非法迁移。

22.8.3 重试必须以 attempt_id 建模

对外部副作用的每次尝试都要区分:

  • 同一网络请求的传输级重试。
  • 同一个供应商作业的状态查询重试。
  • 新建供应商作业的业务级重试。

三者不是一回事。

安全规则:

明确未送达:可在相同 attempt / idempotency key 下重试
供应商支持可靠幂等:可在同一 idempotency key 下重试
结果未知:先对账,禁止直接创建新 attempt
明确失败且可重试:创建新 attempt,并记录成本风险

22.8.4 Watchdog 和补偿任务

每个阶段维护:

entered_at
expected_deadline
hard_deadline
last_progress_at
recovery_policy

Watchdog 周期扫描:

SELECT task_id, stage
FROM task_stage_deadlines
WHERE hard_deadline < now()
  AND watchdog_status = 'ACTIVE'
ORDER BY hard_deadline
LIMIT 500
FOR UPDATE SKIP LOCKED;

不同阶段采取不同补偿:

阶段首选补偿禁止动作
QUEUED检查消息与调度条件,必要时安全重投直接新建生成任务
PROVIDER_SUBMIT_UNKNOWN查询和账单对账立即重复提交
PROVIDER_RUNNING查询供应商、检查回调因“慢”就切换供应商重做
OUTPUT_FETCH刷新 URL、断点或重新下载重新生成视频
MEDIA_PROCESSING新 Worker 重跑确定性处理修改原始资产事实
NOTIFICATION重发通知或客户端拉快照回滚任务成功状态

22.8.5 Redis 并发槽位补偿

Redis 中的并发槽位是准入加速,不是最终事实。

建议:

  • 槽位使用租约和到期时间。
  • Worker 周期续租。
  • PostgreSQL 记录真实 attempt 状态。
  • Reconciler 比较“活跃 attempt 数”和“Redis 槽位数”。
  • 修复时使用 fencing token 或版本,防止旧 Worker 恢复后再次释放新租约。

监控 slot_reconcile_gap,但不要仅凭一个 Gauge 自动大规模修改业务状态。

22.8.6 计费补偿

  • Metrics 中的成本只是聚合观察。
  • 真实结算基于不可变 credit_ledger
  • Provider Submit UNKNOWN 时,账务状态也应进入待对账,而不是直接退款后又在供应商账单中出现成本。
  • 每日按 provider_job_id、供应商账单和本地 attempt 对账。
  • 发现重复生成时,区分用户责任、平台责任和供应商重试语义,决定是否承担成本和退款。

22.9 性能瓶颈和容量估算方法

22.9.1 先估算业务基数

假设峰值:

创建任务到达率:100 task/s
平均生成生命周期:300 s
平均每任务关键业务事件:12 条
平均每任务可保留 Span:20 个
平均每任务结构化日志:25 条

根据 Little’s Law:

在途任务数 = 到达率 × 平均生命周期
          = 100 × 300
          = 30,000 个

由此可进一步估算:

业务事件写入速率 = 100 × 12 = 1,200 events/s
Span 产生速率     = 100 × 20 = 2,000 spans/s
日志基础速率       = 100 × 25 = 2,500 logs/s

实际日志还包括轮询、重试和媒体处理进度,因此应使用压测和生产基线校准。

22.9.2 日志容量

若平均 5,000 条日志/s,每条压缩前 1.2 KB:

每日原始量 ≈ 5,000 × 1.2 KB × 86,400
          ≈ 518 GB/day

如果所有日志保留 30 天并建立全文索引,成本会很高。可采用:

  • ERROR/WARN 热存储时间更长。
  • INFO 短期热存储,随后转冷存储。
  • DEBUG 默认关闭,通过动态采样针对具体 task 或 service 临时开启。
  • 对重复轮询日志做聚合或限频。
  • 不记录大 Prompt、响应体和媒体 URL。

22.9.3 Trace 容量与采样

若 2,000 spans/s、每 Span 约 1 KB:

每日原始量 ≈ 2,000 × 1 KB × 86,400
          ≈ 173 GB/day

建议采样策略:

错误、未知结果、状态冲突、重复生成:100%
超过阶段阈值的慢任务:100%
企业租户关键任务:可提高采样率,但需控制成本
普通成功任务:1%~10% 动态采样

Tail Sampling 能根据最终结果保留错误和慢 Trace,但会增加 Collector 的内存、等待窗口和复杂度;需要对 Collector 本身做容量规划。

22.9.4 Metrics 基数估算

时间序列数量近似为:

metric_family_count × 各标签基数乘积

例如某个 Provider 指标标签为:

provider=6
model_family=12
region=3
result_class=8

则单个指标最多约:

6 × 12 × 3 × 8 = 1,728 条时间序列

如果再加入 100 万个 task_id,监控系统将不可接受。因此应在设计评审阶段为每个新增 Label 做基数预算。

22.9.5 Dashboard 查询成本

常见性能问题:

  • Dashboard 默认查询 30 天原始高分辨率数据。
  • 每次刷新计算复杂分位数和大范围正则匹配。
  • 变量包含所有 task、tenant 或 provider_job_id。
  • 多个面板重复执行相同聚合。

优化:

  • 使用 Recording Rules 预聚合 5 分钟、1 小时和 1 天 SLI。
  • 默认展示最近 1~6 小时,长周期使用降采样数据。
  • 单任务查询走日志、Trace 和事件索引,不走 Metrics 标签。
  • 为常用筛选字段建立日志索引,但避免对所有 JSON 字段全文索引。

22.9.6 关键容量公式

Queue 预计清空时间

T_clear = backlog / (consume_rate - arrival_rate)

仅当 consume_rate > arrival_rate 时成立。

Provider 并发需求

provider_inflight ≈ provider_submit_rate × provider_average_duration

媒体 Worker 槽位

required_slots ≈ media_job_rate × average_processing_duration / target_utilization

例如每秒 2 个媒体任务,平均处理 40 秒,目标利用率 70%:

required_slots ≈ 2 × 40 / 0.7 ≈ 115

还要按 codec、分辨率、CPU/GPU 类型拆分,因为不同任务资源需求差异很大。


22.10 高可用和降级方式

22.10.1 应用埋点降级

  • Metrics 导出失败不得阻塞请求。
  • Trace 和日志使用有界队列。
  • 导出失败超过预算后丢弃低优先级数据,并增加 telemetry_dropped_total
  • 保留关键业务事件写库。
  • 禁止无限重试造成内存堆积。

22.10.2 Collector 高可用

  • 网关多副本,无状态或使用可恢复队列。
  • 按区域就近接收,避免跨区域遥测影响业务网络。
  • 负载均衡器设置健康检查。
  • Collector 自身暴露接收速率、拒绝量、队列长度、导出失败和内存指标。

22.10.3 Metrics 平台故障

降级策略:

  • 服务 /metrics 仍保留当前数据,平台恢复后继续抓取新值。
  • 值班期间依赖合成探针、云基础设施指标和 PostgreSQL 关键查询做临时判断。
  • 不因 Metrics 暂时缺失自动执行高风险业务补偿。
  • 监控系统恢复后检查告警空窗和数据缺口。

22.10.4 日志和 Trace 后端故障

  • 应用本地只保留有限滚动日志,防止磁盘打满。
  • 关键错误同时以低基数 Counter 暴露。
  • 单任务页面仍可从 PostgreSQL 业务事件还原主流程。
  • 动态 Debug 采集在后端故障时应关闭或限量。

22.10.5 单任务排障页面降级

页面数据源可能部分不可用,应显示每个数据源的状态:

PostgreSQL:正常
RocketMQ:正常
Trace Backend:不可用
Log Backend:查询超时
Provider Query:限流

不能因为 Trace 后端超时就让整个页面空白。应先展示事实状态,再异步补充辅助数据。

22.10.6 告警平台自身监控

需要“监控监控系统”:

  • 告警规则最近一次执行时间。
  • 通知发送成功率。
  • Page 到达值班终端的合成测试。
  • 数据抓取新鲜度。
  • Collector 丢弃率。
  • 日志和 Trace 摄取延迟。

22.10.7 观测变暗时的业务策略

当核心监控不可用且无法判断供应商和调度健康度时,可采取保守降级:

  • 降低新任务准入上限。
  • 暂停高成本、长时长模型。
  • 禁止自动跨供应商重做。
  • 保留查询和取消能力。
  • 通过业务数据库的硬阈值防止在途任务无限增长。

这不是因为监控是事实源,而是因为失去可见性时应降低不可逆成本风险。


22.11 安全风险

22.11.1 敏感数据泄漏

不得默认记录:

  • 完整 Prompt 和 Negative Prompt。
  • 用户上传文件名中的隐私信息。
  • 第三方签名 URL、对象存储凭证和 Authorization Header。
  • Provider API Key。
  • Webhook 原始密钥。
  • 人脸、身份证明或未成年人相关元数据。
  • 完整请求和响应 Body。

必要时记录摘要、哈希、长度、内容分类和脱敏片段。

22.11.2 Trace Baggage 滥用

Baggage 会跨服务传播,错误放入敏感字段会扩大泄露面。只允许白名单字段,并限制长度。外部客户端传入的 Trace Header 和 Baggage 不应被无条件信任或原样写入内部系统。

22.11.3 多租户日志越权

  • 日志和排障页面必须按角色和租户授权。
  • 普通客服只能查看脱敏摘要。
  • 平台运维访问完整数据要有审计。
  • 企业租户之间不得通过 task_id 猜测访问。

22.11.4 日志注入

用户输入可能包含换行、JSON 片段或控制字符。必须由结构化日志库编码,不能字符串拼接原始输入,避免伪造日志行和污染查询。

22.11.5 pprof 与诊断端点暴露

Go pprof、expvar、内部 Metrics 和健康诊断端点可能暴露内存、路径、SQL、goroutine 栈和内部拓扑。应放在独立管理端口或内网,使用认证和网络策略限制访问。

22.11.6 高风险排障动作

单任务页面中的以下动作应有强权限和审计:

  • 重新提交生成。
  • 强制终止或完成任务。
  • 修改计费流水。
  • 释放并发槽位。
  • 重放 DLQ。
  • 重新发布资产。

建议记录:操作者、原因、前后状态、审批信息、命令参数和结果。批量操作需要更高权限和限速。

22.11.7 数据保留

日志和 Trace 的保留周期不应默认等同于业务数据保留周期。应按敏感度和用途分层:

安全审计日志:较长时间、不可篡改
业务事件:随任务和合规策略保留
普通应用日志:短期热存储
Trace:按采样和故障需要保留
Prompt / 内容信息:最小化并支持删除流程

22.12 常见错误设计及其后果

错误设计后果
只监控 API QPS 和 p99长任务在 MQ、供应商或媒体阶段卡住时无法发现
只看 Queue Depth,不看 Oldest Age少量长期卡死消息被忽略,突发积压又造成误报
task_iduser_id 放入 Metrics 标签时间序列基数爆炸,监控平台成本和延迟失控
用一条 Trace 覆盖几十分钟全生命周期Trace 超长、上下文断裂、采样和存储困难,重试语义混乱
只记录原始错误字符串无法稳定聚合,同一错误形成大量维度
每次轮询和每个 FFmpeg 进度都写 PostgreSQL 主任务行写放大、热行竞争、WAL 和 autovacuum 压力上升
Provider Submit 超时后自动重试重复生成、重复计费、供应商成本失控
把 Redis 状态当作任务事实缓存过期、主从切换或计数泄漏后产生错误结论
告警直接绑定 CPU 70% 或一次 5xx告警噪音高,值班人员逐渐忽略真正事故
所有错误都 Page告警疲劳,无法区分立即处理和工作时间处理
只做服务 Dashboard,没有单任务页面排查一次投诉需要跨多个系统手工拼接 ID
日志记录完整 Prompt、签名 URL 和响应体隐私、密钥和内容安全风险
监控导出同步阻塞业务请求观测后端故障时反向拖垮生产服务
采样只做固定 1% 头采样低频但高损失的未知结果和重复计费 Trace 被丢失
计费使用 Metrics 统计结果数据缺失或重复采集导致错误结算
生产环境随意开启 Redis MONITOR 或公开 pprof额外性能影响和敏感信息泄漏

22.13 面试官可能追问的 10 个问题

  1. 为什么已经有 trace_id,还需要 task_idattempt_id
  2. 如何判断一个任务真的“卡住”,而不是通知或缓存延迟?
  3. 为什么不能把 task_id 作为 Prometheus Label?单任务又如何查询?
  4. Queue Depth 和 Oldest Message Age 各自说明什么?告警应该看哪个?
  5. Provider Submit 已经发出但本地超时,如何观测和恢复?
  6. 第三方供应商耗时不受平台控制,端到端 SLO 应该怎样定义?
  7. 如何设计告警,既能快速发现事故,又不造成告警疲劳?
  8. 日志、Trace 或 Metrics 平台挂了,业务系统是否还能安全运行?
  9. 单任务全链路排障页面应该展示什么,哪些修复动作可以开放?
  10. 如何估算可观测系统的存储成本,并决定 Trace 采样策略?

22.14 每个追问的资深回答

问题 1:为什么已经有 trace_id,还需要 task_idattempt_id

资深回答:

trace_id 描述的是一次因果链,适合一次 HTTP 请求、一次消息消费或一次 Worker 执行。AI 视频任务会跨几分钟、多个异步边界和多次重试,强行使用一条 Trace 会造成超长 Trace、上下文丢失和父子关系不准确。

task_id 是跨全生命周期的业务主键;attempt_id 区分会产生外部副作用的具体尝试,例如两次 Provider Submit 或两次回源。provider_job_id 则与供应商事实对齐。我的做法是每个短执行单元有自己的 Trace,同时在消息中传播上下文或建立 Span Link,所有日志、Trace 和业务事件都携带 task_id、必要时携带 attempt_id。这样既保留因果链,也能串起整条业务时间线。

问题 2:如何判断一个任务真的“卡住”?

资深回答:

我不会只看状态值,而是给每个阶段定义 entered_atexpected_deadlinehard_deadlinelast_progress_at。Watchdog 根据当前阶段判断超时,再结合 PostgreSQL 事实状态、最后业务事件、MQ oldest age、供应商状态和 Worker 心跳判断。

例如前端仍显示 RUNNING,但 PostgreSQL 已经 SUCCEEDED,那是通知或缓存问题;如果状态是 OUTPUT_FETCHING,URL 即将过期且 last_progress_at 20 分钟未更新,才更像真实卡住。不同阶段的补偿不同,Provider Submit Unknown 只能先对账,不能直接重提;媒体转码通常可以确定性重跑。

问题 3:为什么不能把 task_id 作为 Prometheus Label?

资深回答:

Prometheus 每个 Label 组合都是独立时间序列,task_id 是无界高基数。假设每天百万任务,每个任务进入多个指标,就会产生数百万甚至更多时间序列,造成内存、磁盘和查询成本爆炸。

Metrics 用于聚合趋势,标签只保留 provider、stage、region、result_class 等有界维度。单任务查询通过 task_id 查 PostgreSQL 业务事件、结构化日志和 Trace 索引,排障页面再把这些数据关联起来。

问题 4:Queue Depth 和 Oldest Age 应该看哪个?

资深回答:

两个都要看。Depth 表示工作量,Oldest Age 更接近等待体验。十万条积压如果消费速率远高于到达速率,可能很快清空;只有一条消息但已等待两小时,也可能是顺序队列被毒消息卡住。

我会联合观察 arrival rate、completion rate、depth、oldest ready age、consumer lag 和 DLQ。Page 通常更倾向于持续上升的 oldest age 或错误预算快速燃烧,而不是单次 depth 尖峰。

问题 5:Provider Submit 超时如何恢复?

资深回答:

关键是把它分类成 UNKNOWN,而不是 FAILED。请求已发出后,本地超时无法证明供应商未创建任务。立即重试可能重复生成和重复成本。

我会在提交前创建 attempt,携带稳定幂等键和 provider request ID。超时后通过供应商幂等查询、请求查询、回调、账单或后续列表接口对账。确认未受理才能重试;确认已受理则补写 provider_job_id 并继续跟踪。整个过程单独监控 unknown outcome 数量和最长未决时间。

问题 6:第三方不受控,SLO 怎样定义?

资深回答:

我会同时定义用户端到端 SLO和平台分层 SLO。端到端 SLO代表真实体验,例如“95% 的标准任务在 8 分钟内可播放”;但内部再拆成创建 API、排队调度、Provider Submit、终态发现、回源和媒体处理。

供应商执行耗时作为外部依赖 SLI单独展示。这样既不逃避用户体验责任,也能准确判断错误预算消耗来自平台还是供应商,并指导路由、降级和商务 SLA。

问题 7:怎样减少告警噪音?

资深回答:

第一,Page 以用户症状和错误预算燃烧为主,而不是每个底层指标单独告警。第二,使用持续时间、双窗口或多窗口 Burn Rate,过滤瞬时抖动。第三,对同一根因做告警分组和抑制,例如 Provider 全局故障时抑制其下游多个服务的派生告警。第四,Page 必须附带影响范围、Dashboard 和 Runbook;无法行动的告警降为 Ticket 或报表。

我还会定期审查每条告警的触发次数、有效率和平均处理时间,删除长期无行动价值的规则。

问题 8:观测平台挂了,业务还能运行吗?

资深回答:

应该能,但会进入保守模式。应用遥测导出必须异步、有界、可丢弃,不能阻塞业务。任务、计费和资产事实仍写 PostgreSQL,日志和 Trace 不是事实源。

观测变暗时,我可能降低高成本模型准入、暂停自动跨供应商重做,并依靠合成探针、数据库硬阈值和云基础设施指标维持基本判断。恢复后再补做数据缺口审计。核心原则是可观测系统故障不能成为业务故障放大器。

问题 9:单任务排障页面放什么?

资深回答:

页面至少包括任务摘要、阶段 deadline、业务事件时间线、所有 attempt、provider_job_id、Trace 和日志入口、MQ 重试与 DLQ、并发槽位、媒体下载与 FFmpeg 进度、资产发布和计费流水。

修复动作必须按风险分级。查询供应商、重试下载、重投确定性转码通常可自动化;重新提交生成、修改计费和强制终态属于高风险动作,需要权限、原因和审计。页面的目标不是“看起来信息很多”,而是让工程师在几分钟内判断下一步安全动作。

问题 10:如何估算观测成本和采样?

资深回答:

先从任务到达率推导事件、日志和 Span 速率,再乘平均记录大小和保留周期。Metrics 要做 Label 基数预算;日志要区分热存储、冷存储和索引字段;Trace 根据错误、慢请求和业务风险做差异化采样。

例如错误、状态冲突、unknown outcome、重复生成和 checksum mismatch 100% 保留;正常成功链路按 1%~10% 采样。若采用 Tail Sampling,要把 Collector 的等待窗口、内存和故障行为纳入容量规划。成本优化不能牺牲关键业务事件,因为那是恢复和对账依据。


22.15 三分钟口述稿

我们这个 AI 视频平台的可观测性不是只看服务 QPS 和 CPU,而是围绕一条具体生成任务建立全链路排障能力。因为一次任务会跨 API、PostgreSQL、Outbox、RocketMQ、调度器、第三方供应商、回调或轮询、输出回源、FFmpeg 和 CDN,生命周期可能持续几分钟,所以我不会用一条超长 Trace 覆盖全过程。

我会定义四类核心标识:task_id 是任务全生命周期的业务主线,attempt_id 区分每次可能产生外部副作用的尝试,trace_id 表示一次 API、消息消费或 Worker 执行的短因果链,provider_job_id 用来和供应商事实及账单对齐。跨 MQ 时传播 Trace Context,重试或异步断点使用 Span Link;所有结构化日志、Trace 和业务事件都携带 task_id,但 task_id 绝不能放进 Prometheus Label,否则会造成高基数爆炸。

指标方面,API 看 RED,Worker 看 USE,RocketMQ 同时看 queue depth、oldest age、consumer lag、到达率和完成率。供应商重点看提交时延、429、5xx、任务完成耗时、callback lag,以及最危险的 unknown outcome。Provider Submit 请求发出后本地超时,不能直接当失败重试,因为供应商可能已经受理。我会把 attempt 标记为 UNKNOWN,通过幂等键、请求 ID、回调、查询或账单对账,确认未受理后才允许重提,避免重复生成和重复计费。

媒体链路要看下载首字节、吞吐、URL 失效、checksum、ffprobe、FFmpeg 实时因子、CPU/GPU 和临时盘。每个任务阶段都有 entered_at、expected deadline、hard deadline 和 last_progress_at,Watchdog 根据阶段选择补偿:供应商结果未知先对账,输出 URL 失效先刷新地址,转码失败可以确定性重跑,通知失败只补通知,不能回滚成功任务。

SLO 我会分层:创建 API、排队调度、Provider Submit、状态发现、输出回源、媒体处理和通知新鲜度,同时保留端到端用户 SLO。告警优先看用户症状和错误预算燃烧,配合多窗口,减少瞬时噪音。最后提供单任务排障页面,把 PostgreSQL 事实状态、业务事件、attempt、MQ、Trace、日志、媒体资产和计费流水串起来,并提供受权限控制的安全修复动作。

核心原则是:PostgreSQL 保存事实,Metrics 发现趋势,Logs 解释细节,Traces 展示因果链;观测系统即使故障,也不能反向拖垮业务。


22.16 十分钟深入讲解提纲

第 1 分钟:为什么普通监控不够

  • 分钟级长任务跨多个异步边界。
  • 用户投诉通常是“某个任务卡住”,而非“某个接口 500”。
  • 外部供应商存在结果未知和真实成本。
  • 媒体数据面有下载、校验、转码和发布等独立故障域。

第 2 分钟:关联标识体系

  • task_id:全生命周期业务主线。
  • attempt_id:每次外部副作用尝试。
  • trace_id:一次短因果链。
  • provider_job_id:供应商事实。
  • event_idmessage_idasset_idbilling_txn_id
  • 不做一条持续几十分钟的巨大 Trace;跨异步边界传播上下文或 Span Link。

第 3 分钟:四类观测信号的边界

  • Metrics:聚合趋势和告警。
  • Logs:结构化上下文。
  • Traces:单次因果链和耗时拆解。
  • Business Events:状态变化和副作用事实。
  • 低基数放 Metrics,高基数放 Logs、Traces 和事件库。

第 4 分钟:核心指标

  • API RED。
  • Worker USE。
  • MQ:Depth、Oldest Age、Lag、Arrival、Completion、Retry、DLQ。
  • Provider:Submit Latency、429、5xx、Unknown Outcome、Job Duration、Callback Lag。
  • Output Fetch:TTFB、吞吐、URL 失效、checksum。
  • FFmpeg:实时因子、进度年龄、退出类、CPU/GPU、临时盘。
  • PostgreSQL 连接池等待和 wait event;Redis 延迟、超时和槽位差异。
  • Cost per Success 和对账差异。

第 5 分钟:如何定义“任务卡住”

  • 每阶段 entered_atexpected_deadlinehard_deadlinelast_progress_at
  • PostgreSQL 事实优先于 Redis 和前端显示。
  • Watchdog 按阶段扫描。
  • Queue Depth 与 Oldest Age 联合判断。
  • 进程存活不等于媒体任务有进展。

第 6 分钟:最危险的未知结果

  • Provider 已受理但本地超时。
  • UNKNOWNFAILED 必须分开。
  • 提交前创建 attempt 和幂等键。
  • 先查询、回调、账单对账,再决定是否重提。
  • 观察 unknown outcome 数量、比例和最长未决时间。

第 7 分钟:竞态、幂等与补偿

  • 回调和轮询同时完成:状态机条件更新 + 唯一事件。
  • 取消和完成竞态:按真实供应商状态和产品计费规则处理。
  • MQ 至少一次:Inbox/唯一约束保障业务幂等。
  • Redis 槽位租约 + Reconciler。
  • 输出下载和转码可重试,生成重提必须评估重复成本。

第 8 分钟:SLO 和告警

  • 端到端用户 SLO + 平台分层 SLO。
  • 区分平台可控时延和供应商执行时延。
  • Page 看用户症状和快速 Burn Rate。
  • 瞬时资源波动降为 Ticket。
  • 告警分组、抑制、持续时间和 Runbook。

第 9 分钟:容量和观测系统自身可靠性

  • 用任务到达率估算日志、Span 和事件速率。
  • Metrics Label 基数预算。
  • 日志热/冷分层和索引控制。
  • 错误与慢 Trace 100% 保留,成功任务动态采样。
  • Collector 多副本、有界队列、遥测丢弃指标。
  • 观测后端故障不能阻塞业务。

第 10 分钟:单任务排障闭环

  • 输入 task_id,展示事实状态、事件时间线、attempt、provider_job_id、MQ、Trace、日志、媒体和计费。
  • 根据阶段给出安全动作:查询供应商、刷新 URL、重试下载、重跑转码、补通知、计费对账。
  • 重新生成属于高风险动作,必须确认外部未受理或明确接受重复成本。
  • 所有人工操作有权限、原因和审计。

本章总结

AI 视频平台可观测性的最高价值,不是“图表足够多”,而是能在故障发生时快速回答五个问题:

1. 哪些用户和任务正在受影响?
2. 卡在哪个阶段,最后一个可信事实是什么?
3. 是否已经发生外部副作用或真实成本?
4. 哪个修复动作是幂等且不会制造重复生成?
5. 修复后如何验证数据、资源和计费重新一致?

面试中应反复强调:

Metrics 用于发现面,Logs 用于解释点,Traces 用于还原因果链,PostgreSQL 业务事件用于保存可恢复的事实。对于长耗时 AI 任务,task_id 串业务全程,attempt_id 隔离副作用尝试,trace_id 只描述一次执行链。任何自动恢复在执行前,都必须先判断它会不会造成重复生成或重复计费。