Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
12 KiB
12 KiB
视频转码配置与运行说明
本文描述当前代码实现。数据库字段
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,客户端不能绕过有效租约并行重试。 - 自动恢复由后台调度器负责,不依赖管理员打开这个接口。
发布与数据安全门禁
生产发布前必须:
- 先构建并固定同一 40 位 Git SHA 的 runner、migrator、镜像 digest 与生产机 image ID;此时不启动候选、不改数据库。
- 第一次从不支持新 drain 协议的旧镜像升级必须使用独立的
scripts/legacy-worker-bootstrap-deploy.ps1。先执行只读Mode=Audit;只有同时提供-Apply -AllowLegacyDrainBootstrap才能进入已证明的 503 维护交接,不能删除门禁强推。 - 只有旧容器已停止并保留、503 维护入口已验证后,才在同一维护窗执行生产备份、隔离恢复验证及四份 additive migration;部署脚本不会偷偷迁移数据库,也不能以“当前没人使用”代替维护门禁。
- 候选容器切流前只读确认租约列、索引、品牌默认值、uploadId 幂等索引和全部 migration checksum 精确匹配。
- 常态发布时旧容器进入 drain 后,等待等待队列和 active marker 同时归零;状态未知时拒绝停止旧容器。
- 保留旧容器、旧镜像、数据库备份和正式源视频,直到 canary 全链路通过。
- 候选容器必须带 drain 启动,在数据库、品牌资源、能力和切流门禁全部通过后才恢复后台任务;切流提交后旧槽继续保持 drain。
- 未完成独立 uploads/runtime/Redis 隔离前,通用增量脚本拒绝
Target=shadow;正式发布发现运行中的影子容器也必须先人工确认无共享 FFmpeg 风险。 - 通用脚本禁止自动执行历史 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、任务详情 jobqueued/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 进程;
- 部署过程中有活动转码时,旧容器没有被提前停止。
故障排查顺序
- 查素材的
videoProcessingStatus、租约 token/过期时间、三阶段进度和transcodeLastError。 - 核对 canonical 源文件是否存在、大小是否与数据库一致,并用 FFprobe 验证可解析。
- 查当前容器 active marker、drain 状态、FFmpeg 进程和启动恢复日志。
- 查三个预览对象是否存在;孤立对象不能在数据库引用不明时直接删除。
- 只有源文件安全、没有有效租约且没有在途 FFmpeg 时,才允许按精确素材 ID 重试。
详细事故原因、生产证据和首次上线步骤见《视频预览任务中断事故复盘(2026-08-11)》。