gongxiangp2/docs/视频转码配置说明.md
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

12 KiB
Raw Blame History

视频转码配置与运行说明

本文描述当前代码实现。数据库字段 videoPreview540Url 是历史命名,当前默认产物实际为 720P,不能按字段名推断分辨率。

当前结论

  • 上传的正式源视频优先保存在生产机 UPLOAD_DIR 对应的 uploads 目录;预览视频、短片和封面写入当前 S3 存储桶。
  • 当前编码器是代码中固定的 libx265(H.265/HEVC),不是可由 VIDEO_TRANSCODE_CODEC 切换的配置项。
  • 每个应用实例最多并行处理 1 条视频;数据库租约和 PostgreSQL 事务锁保证蓝绿容器之间同一素材只有一个有效执行者。
  • 应用启动时立即扫描未完成任务,之后每 60 秒重扫。Redis 用于上传会话和其它业务,不承担视频转码队列持久化。
  • 正式源视频在成功、失败、重试和部署中断后都必须保留。任务只允许清理自己创建的临时文件。
  • 转码产物从磁盘流式上传到 S3,不能先整体读入 Node.js 内存。
  • 本地 canonical 源视频直接只读使用,不复制到系统临时盘;远端兼容输入和任务产物只能写入 UPLOAD_DIR 所在的 NAS 工作目录,并在开始前按该挂载点空间 fail-closed。
  • FFmpeg 使用 spawn(..., shell: false) 直接启动;超时、租约丢失或 drain 时终止进程组,任意非零退出码都算失败。
  • 普通素材建档请求最多 128MiB;源文件达到 5MiB 必须走分片,单个分片最多 20MiB,逻辑源文件最多 10GiB。分片合并前必须确认 uploads/NAS 至少可容纳两份源文件并保留 2GiB 余量。

实际转码产物

产物 当前实现 数据库字段
主预览 默认高度 720、H.265、medium、AAC 128k videoPreview540Url(仅字段名保留 540)
首页短片 默认 10 秒、高度 360、H.265、ultrafast、无音轨 videoPreviewClipUrl
卡片封面 默认高度 720、JPEG;用户已上传封面时不重复截图 videoCoverUrl / previewUrl

只有三类必需产物均已生成并上传、当前数据库租约仍有效时,任务才可以写成 done。FFmpeg 任意非零退出码、输出缺失、上传失败或租约失效都不得伪装成完成。转码期间用户后补的手动封面或首页预览优先,完成事务只能填补当时仍为空的字段,不能用任务开始时的旧快照覆盖用户结果。

当前有效环境变量

变量 默认值 作用与边界
VIDEO_PREVIEW_RESOLUTION 720 主预览和自动封面的目标高度
VIDEO_CLIP_DURATION 10 首页短片秒数
VIDEO_PREVIEW_CRF 20 主预览 H.265 质量,越小画质越高、文件越大
VIDEO_CLIP_CRF 24 首页短片 H.265 质量
VIDEO_TRANSCODE_TIMEOUT 3600000 单个 FFmpeg 命令超时,单位毫秒
VIDEO_MAX_TEMP_FILE_SIZE 10737418240 临时输入最大字节数,默认 10GB
VIDEO_TRANSCODE_THREADS 1 FFmpeg/x265 线程上限;代码钳制在 1–2
VIDEO_TRANSCODE_MAX_CONCURRENT 1 共享应用容器的并发数;代码强制最高为 1
VIDEO_TRANSCODE_RUNTIME_DIR UPLOAD_DIR/.video-transcode-runtime 当前容器 capability、drain 和 active marker 目录

旧文档曾列出的 VIDEO_TRANSCODE_CODEC、VIDEO_TRANSCODE_PRESET、VIDEO_TRANSCODE_CRF、VIDEO_AUDIO_BITRATE 和“Redis 持久化视频队列”并不是当前视频链路的有效控制面,不得据此调整生产。

状态与恢复

合法状态为:

  • pending:已建档,等待领取或等待周期恢复。
  • processing:某个未过期数据库租约正在处理。
  • done:数据库已写入完整预览结果。
  • failed:已确认处理失败,需要用户明确重试或人工检查源文件。

应用通过 instrumentation.ts 启动恢复器;恢复器每 60 秒扫描:

  • 没有有效租约的 pending 视频;
  • 租约已过期的 processing 视频;
  • 历史版本留下且心跳已静默的 processing 视频。

恢复扫描失败只记录错误,下一轮继续;扫描之间不重叠。不得使用“批量把所有 processing 改回 pending”的裸 SQL,这会绕过租约、源文件核验和在途 FFmpeg,可能造成重复转码或覆盖结果。

上传入口的内存与磁盘边界

  • /api/materials/upload 必须在解析 multipart 前校验 Content-Type、Content-Encoding 和 Content-Length;缺失长度返回 411,超过 128MiB 返回 413。
  • 直传 zipFile 必须小于 5MiB。达到 5MiB 后走 init → chunk → complete → existingFilePath → 建档,大文件网络传输仍使用分片并发,不会被普通建档的内存处理槽串行化。
  • 普通建档/图片处理每个应用实例最多同时处理 1 个请求,另保留 8 个短队列位置;繁忙时返回带 Retry-After 的 503,避免多个大 multipart 与 Sharp 同时占满 Node.js 内存。
  • 单分片最多 20MiB;初始化会话记录固定分片布局,每个位置的期望字节数必须精确匹配。断点续传复用原会话布局;服务器边读边计数,超限立即取消并删除半文件,不能靠大量“各自合法”的分片突破会话总大小。
  • 服务器在 multipart 解析前进入最多 8 路的分片槽,并在每块落盘前再次确认 NAS 至少保留 2GiB 安全余量;每次请求先写唯一 partial,完整校验后原子替换 chunk-N。这与浏览器最高并发一致,可继续利用 300M 带宽,同时避免解析内存和重复请求写盘竞态。
  • init 拒绝超过 10GiB 的逻辑源文件;无法确认 uploads/NAS 剩余空间时保守返回 503,空间不足返回 507,不得继续写盘。complete 全局只合并 1 个文件,合并前再次要求可用空间至少为文件大小加 2GiB;写入唯一 partial、同步并原子改名,异常必清半成品。
  • complete 必须幂等:同一用户的成功结果即使在公网响应途中丢失,也能从受控元数据恢复归属并返回同一个 merged 文件,不得因分片已清理而要求整文件重传。
  • 买家秀和小红书批次逐条提交,避免多个图片 multipart 同时解码;小红书附加图片必须进入 displayImages,不能静默丢弃。
  • 本机稳定代理对主上传精确限制为 129m、分片精确限制为 21m、其余上传 JSON 子路由限制为 2m,相关上传路由关闭 proxy_request_buffering。这样活动上传会持续建立到应用容器的连接,蓝绿 drain 不会把仍在 Nginx 缓冲但尚未转发的请求误判为空闲。

API 权限与用途

查询单条状态

GET /api/materials/transcode?materialId=<素材ID>
  • 必须登录并拥有该素材的查看权限。
  • 普通用户看不到其他素材 ID、全队列详情或后端错误全文。
  • 首页只做只读轮询,不会因为看到队列暂时为空就静默发起强制重试。

明确启动或重试

POST /api/materials/transcode
Content-Type: application/json

{
  "materialId": "<素材ID>",
  "forceRetry": true
}
  • 普通用户只能操作自己的素材;系统管理员可按权限处理其它素材。
  • 正在处理的素材返回 409,客户端不能绕过有效租约并行重试。
  • 自动恢复由后台调度器负责,不依赖管理员打开这个接口。

发布与数据安全门禁

生产发布前必须:

  1. 先构建并固定同一 40 位 Git SHA 的 runner、migrator、镜像 digest 与生产机 image ID;此时不启动候选、不改数据库。
  2. 第一次从不支持新 drain 协议的旧镜像升级必须使用独立的 scripts/legacy-worker-bootstrap-deploy.ps1。先执行只读 Mode=Audit;只有同时提供 -Apply -AllowLegacyDrainBootstrap 才能进入已证明的 503 维护交接,不能删除门禁强推。
  3. 只有旧容器已停止并保留、503 维护入口已验证后,才在同一维护窗执行生产备份、隔离恢复验证及四份 additive migration;部署脚本不会偷偷迁移数据库,也不能以“当前没人使用”代替维护门禁。
  4. 候选容器切流前只读确认租约列、索引、品牌默认值、uploadId 幂等索引和全部 migration checksum 精确匹配。
  5. 常态发布时旧容器进入 drain 后,等待等待队列和 active marker 同时归零;状态未知时拒绝停止旧容器。
  6. 保留旧容器、旧镜像、数据库备份和正式源视频,直到 canary 全链路通过。
  7. 候选容器必须带 drain 启动,在数据库、品牌资源、能力和切流门禁全部通过后才恢复后台任务;切流提交后旧槽继续保持 drain。
  8. 未完成独立 uploads/runtime/Redis 隔离前,通用增量脚本拒绝 Target=shadow;正式发布发现运行中的影子容器也必须先人工确认无共享 FFmpeg 风险。
  9. 通用脚本禁止自动执行历史 direct 容器到稳定代理的拓扑迁移,也禁止用未验证旧镜像做 direct 回滚;检测到该现场时必须在任何切流或停容器前 fail-closed,改走单独受控 SOP。

首次旧镜像过渡的职责边界

一次性 bootstrap 只负责旧直连现场到稳定 503 维护入口的安全交接,不负责候选发布:

  • 必须固定旧容器完整 ID、旧 image ID/RepoDigest、旧容器网络 upstream、完整 docker port 端点集合、uploads ZFS source/target/statfs,以及目标镜像 digest/image ID/40 位 Git SHA。
  • 在 deployment flock 与 proxy reconciliation flock 内重复核验全部 pins;发现已有蓝绿槽、稳定代理、影子容器或状态歧义立即退出。
  • 通过 IPv4/IPv6 规则阻断新连接,让已建立连接自然退出;同时检查宿主 ss 与通过宿主 nsenter 进入固定旧容器 network namespace 后的服务端口 3000,两侧都必须连续采样两次 ESTABLISHED=0。不能只看宿主到容器 IP 的连接,因为 Docker DNAT 且未启用 userland-proxy 时可能漏掉仍在上传的公网连接。
  • 冻结前后都只读核对视频 pending/processing=0、任务详情 job queued/processing=0,并列出和拒绝任何 taskStatus=uploading 素材;同时核对旧容器和整台宿主机没有 FFmpeg。
  • 先预置候选蓝槽的两类 drain marker,但绝不写 capability;真实 capability 只能由目标容器用当前实例 ID 发布。
  • docker pause 后再次用固定目标镜像的 Node 单次入口查库;确认 idle 后只能用 docker stop --signal SIGKILL --time 0 停止,避免普通 stop 为发送优雅退出信号而恢复 paused 容器、让旧用户代码短暂重跑。停止状态必须精确为 exited,并再次用固定目标镜像查数据库、检查宿主机无 FFmpeg,全部通过后才改名保留旧容器。不得删除旧容器或旧镜像。
  • 成功只表示入口处于明确 503 维护状态。随后仍需运行普通 git-incremental-deploy.ps1 -Target production -Commit <同一SHA>;所有候选门禁照常执行。
  • 自动恢复只允许在入口仍是 legacy-bootstrap-maintenance 且没有运行中的蓝绿容器时进行。候选可能接收过流量时保持维护状态并人工判定,不能盲启旧容器。

完整取证命令、PowerShell 参数模板和恢复命令见《视频预览任务中断事故复盘(2026-08-11)》的“首次生产过渡门禁”。

验收一条小视频时需要同时确认:

  • 数据库状态为 done,三类预览 URL 均非空;
  • 三个 S3 对象实际 HEAD 200、类型和大小正确,浏览器可解码;
  • 卡片在 15 天保护期内仍显示处理状态,并在完成后自动出现封面;
  • uploads 中 canonical 源视频仍存在,字节数未变化;
  • 当前只有一个有效租约和一个 FFmpeg 进程;
  • 部署过程中有活动转码时,旧容器没有被提前停止。

故障排查顺序

  1. 查素材的 videoProcessingStatus、租约 token/过期时间、三阶段进度和 transcodeLastError。
  2. 核对 canonical 源文件是否存在、大小是否与数据库一致,并用 FFprobe 验证可解析。
  3. 查当前容器 active marker、drain 状态、FFmpeg 进程和启动恢复日志。
  4. 查三个预览对象是否存在;孤立对象不能在数据库引用不明时直接删除。
  5. 只有源文件安全、没有有效租约且没有在途 FFmpeg 时,才允许按精确素材 ID 重试。

详细事故原因、生产证据和首次上线步骤见《视频预览任务中断事故复盘(2026-08-11)》。