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 和通知网关。
生产系统必须能回答以下问题:
-
任务是否真的卡住了?
- 只是前端通知断开,还是后端状态没有推进?
- 是排队、供应商处理、回源下载还是转码阶段超时?
- Redis 缓存是否过期,而 PostgreSQL 中的事实状态其实已经完成?
-
第三方供应商是否退化?
- 是提交接口 429 上升,还是已经受理但完成耗时变长?
- 是某个模型、地区、账号配额或输入类型异常,还是供应商全局故障?
- 回调变慢时,轮询补偿是否仍然有效?
-
是否发生了不可逆或付费副作用?
- Provider Submit 超时后,对方是否已经创建任务?
- MQ 重复投递是否触发了第二次生成?
- 用户是否被重复结算,平台是否承担了重复供应商成本?
-
媒体数据面是否健康?
- 第三方临时 URL 是否即将失效?
- 下载吞吐是否异常、checksum 是否一致、ffprobe 是否通过?
- FFmpeg 是 CPU 饱和、GPU 槽位不足、临时盘打满,还是输入文件损坏?
-
用户体验是否达标?
- 创建任务接口是否可用?
- 排队时间、生成完成时间、首个可播放时间和最终导出时间是否满足目标?
- 用户看到的进度是否新鲜,而不是一个长期停留在 99% 的伪进度?
-
告警是否能驱动行动?
- 值班人员收到告警后,是否知道影响范围、责任阶段、止损动作和回滚条件?
- 告警是用户症状,还是一个无关紧要的基础设施波动?
- 同一事故是否触发几百条重复告警?
本章的最终产物不是一堆 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_id与attempt_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.name、service.version、deployment.environment、cloud.region。 - 将
trace_id、span_id、task_id、attempt_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 可以分两层:
-
节点或边车层
- 接收本机应用 OTLP。
- 批量、限流和短时缓冲。
- 补充节点、Pod、区域等资源属性。
-
网关层
- 多副本部署。
- 做路由、过滤、采样和后端导出。
- 对错误 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_total | Counter | service、operation、status_class |
api_request_duration_seconds | Histogram | service、operation |
api_inflight_requests | Gauge | service、operation |
api_rejected_total | Counter | reason、tenant_tier |
Worker USE
| 指标 | 含义 |
|---|---|
worker_slot_utilization_ratio | 槽位利用率 |
worker_queue_wait_seconds | 等待 Worker 槽位时长 |
worker_saturation_ratio | 待执行数与可用槽位关系 |
worker_errors_total | 按稳定错误类统计 |
worker_heartbeat_age_seconds | Worker 心跳新鲜度 |
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_total | 429、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_total | URL 失效次数 |
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_ratio | CPU 使用率 |
worker_gpu_utilization_ratio | GPU 使用率 |
worker_gpu_memory_utilization_ratio | GPU 显存使用率 |
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_seconds | SQL 延迟 |
db_lock_wait_seconds | 锁等待 |
db_deadlocks_total | 死锁次数 |
db_replication_lag_seconds | 副本延迟 |
Go 的 database/sql 应同时导出 DB.Stats() 中的连接使用和等待指标,并与 PostgreSQL pg_stat_activity、wait_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
如果立即重试,可能产生两个真实生成任务和两份成本。
正确处理:
- 将 attempt 标记为
UNKNOWN。 - 使用客户端幂等键、provider request ID 或供应商查询接口对账。
- 暂时不释放已预占额度,也不直接重复结算。
- 达到 reconcile deadline 后进入人工或规则化决策。
- 指标单独统计
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_age、consumer_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_id、user_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 个问题
- 为什么已经有
trace_id,还需要task_id和attempt_id? - 如何判断一个任务真的“卡住”,而不是通知或缓存延迟?
- 为什么不能把
task_id作为 Prometheus Label?单任务又如何查询? - Queue Depth 和 Oldest Message Age 各自说明什么?告警应该看哪个?
- Provider Submit 已经发出但本地超时,如何观测和恢复?
- 第三方供应商耗时不受平台控制,端到端 SLO 应该怎样定义?
- 如何设计告警,既能快速发现事故,又不造成告警疲劳?
- 日志、Trace 或 Metrics 平台挂了,业务系统是否还能安全运行?
- 单任务全链路排障页面应该展示什么,哪些修复动作可以开放?
- 如何估算可观测系统的存储成本,并决定 Trace 采样策略?
22.14 每个追问的资深回答
问题 1:为什么已经有 trace_id,还需要 task_id 和 attempt_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_at、expected_deadline、hard_deadline 和 last_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_id、message_id、asset_id、billing_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_at、expected_deadline、hard_deadline、last_progress_at。 - PostgreSQL 事实优先于 Redis 和前端显示。
- Watchdog 按阶段扫描。
- Queue Depth 与 Oldest Age 联合判断。
- 进程存活不等于媒体任务有进展。
第 6 分钟:最危险的未知结果
- Provider 已受理但本地超时。
UNKNOWN与FAILED必须分开。- 提交前创建 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只描述一次执行链。任何自动恢复在执行前,都必须先判断它会不会造成重复生成或重复计费。