从「一个一个来」到「三路并发」
最开始是这样的:一个小项目用 CNB 云构建处理视频文件,处理器的工作方式是单路串行——一个视频处理完,下一个才能开始。对着三千多个文件的队列算了一下,按这个速度要跑好几天。
需求很简单:让处理变快。但在这个「变快」的过程中,踩了七个不同的坑。每个坑都很小,但串在一起能把一个简单的并发调整拖成整夜。
CNB 的并发模型
CNB 的核心配置文件是 .cnb.yml,通过 api_trigger 声明式的定义构建触发。在 YAML 中设置 CI_VIDEO_CONCURRENT 环境变量来控制并发路数:
master:
api_trigger_gpu:
- name: process-video-gpu
env:
CI_VIDEO_CONCURRENT: "3"
CI_BATCH_SIZE: "32"
stages:
- name: process
timeout: 43200s
script: |
python3 process_video.py
参数演进是从 concurrent=1, batch=8 开始的,目标是推到 3 路并行。看起来就改个数字的事,但每个数字背后都有故事。

坑 1:GPU OOM — 并发不是越多越好
第一次调优:直接把 concurrent 从 1 改成 5,5 路并行跑起来。十几分钟后进程全部被 kill。
根因:每个并发任务加载 YOLO 模型大约占 1.5GB 显存。5 路就是 7.5GB,虽然 T4 是 16GB,但模型运行时还有其他内存开销(图像缓冲、中间计算),合计超过可用显存。
修复:concurrent 回退到 3。为了弥补单路吞吐,把 CI_BATCH_SIZE 从 16 提升到 32——每批处理更多帧,让 GPU 利用率更高而不是靠更多路数。

教训:并发路数不是线性扩展的,上限由显存决定。先算出单路占用,再反推上限,比「先试 5 路看看」靠谱。
坑 2:无输出超时 — 构建跑到一半被截断
concurrent 调好后,几个长时间处理的构建中途被终止。查 CNB 日志发现是无输出超时(默认 10 分钟)。YOLO 推理的批次间隔在某些视频上超过 10 分钟,被当作「无输出」截断了。
修复:每个 process stage 加 timeout: 43200s:
stages:
- name: process
timeout: 43200s
script: |
python3 process_video.py --timeout 43200
CNB 有一个特性:声明 timeout 后,无输出超时也会同步为这个值。设 43200s(12小时)后,最长不会因为处理慢而被截断。
坑 3:Workspace 和 CI 的超时是两套配置
项目有两种运行模式:CI(api_trigger 触发构建)和 Workspace(面板上点「云原生开发」启动长驻环境)。超时配置在两个地方位置不同:
- CI 场景:timeout 在
stages级别 - Workspace 场景:timeout 在
$.vscode作业级别
$:
vscode:
- name: gpu-processor
timeout: 43200s
script: |
while true; do
python3 process_video.py
sleep 60
done
如果只改了 CI 的 timeout 没改 Workspace,长驻处理仍然会超时。
坑 4:API 触发路径用错了
通过 OpenAPI 触发构建,一开始用的 CLI 命令返回 404。查文档发现正确路径是:
curl -X POST "https://api.cnb.cool/{owner}/{repo}/-/build/start" \
-H "Authorization: Bearer $(cat ~/.cnb/token | python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])')" \
-H "Accept: application/vnd.cnb.api+json" \
-H "Content-Type: application/json" \
-d '{"branch":"master","name":"process-video-gpu","sync":"false"}'
有三个注意点:
1. 路径是 /-/build/start,不是某些 CLI 命令构造的路径
2. sync 必须是字符串 "false",不是布尔值(CNB API 的参数类型校验有 bug)
3. Accept header 必须指定(否则部分 API 返回 406,见下一个坑)
坑 5:Accept Header 缺失 → 406
调用 CNB 删除 asset 的 API 时,一直返回 406 Not Acceptable。根因是请求头缺少 Accept: application/vnd.cnb.api+json。
这不是每次都触发——某些 API 不需要这个 header 也能返回正确结果,但 delete asset 等操作会校验。修复很简单:所有 CNB API 调用统一加这个 header,不再区分。
坑 6:镜像路径变量不展开
配置中本来用 ${CNB_REPO_SLUG_LOWERCASE} 变量来构造镜像路径:
docker:
image: docker.cnb.cool/${CNB_REPO_SLUG_LOWERCASE}/{cpu,gpu}-latest
结果在跨仓库触发时变量不展开,镜像拉取失败。修复方法:直接硬编码完整路径。
docker:
image: docker.cnb.cool/username/cam-filter:gpu-latest
这个变量在当前仓库的 push 触发中能正常工作,但 api_trigger 跨仓库调用时不展开。
防重复认领:Upstash Redis 解决并发冲突

3 路并行意味着 3 个处理实例同时从任务池取任务,不能重复处理同一个文件。解决方案是 Upstash Redis 的 SETNX 原子操作:
import requests
def claim_file(file_id: str, upstash_url: str) -> bool:
"""返回 True 表示认领成功,False 表示别人在处理"""
resp = requests.post(upstash_url, json=["SETNX", f"claim:{file_id}", "1"])
result = resp.json()
return result.get("result") == 1
每个文件在 Redis 中有一个 key,SETNX 保证只有一个处理实例能设置成功。失败的实例跳过这个文件取下一个。
这个方案的好处是无状态、零额外基础设施(Upstash 免费实例 50 万命令/月),并且通过 HTTP 协议访问,不需要 Redis 客户端库。
总结:并发调优的三个原则
从单路串行到三路并行的过程,总结出三条原则:
- 并发上限由显存决定,不是 CPU 核心数。GPU 场景一定要先算单路显存占用,留 20% 余量
- 超时配置是隐形成本。默认值可能不适合你的场景,每个 stage 显式声明 timeout
- API 的每个 header 和参数类型都值得较真。字符串和布尔值不一样,少一个 header 就是 406
最终配置的核心结构:
master:
api_trigger_gpu:
- name: process-video-gpu
timeout: 43200s
env:
CI_VIDEO_CONCURRENT: "3"
CI_BATCH_SIZE: "32"
CI_YOLO_INTERVAL: "2"
runner:
tags: cnb:arch:amd64:gpu
stages:
- name: process
timeout: 43200s
script: |
python3 process_video.py --timeout 43200
这套配置处理完三千多个文件只用了不到四分之一的时间。七个坑,每个都小,但填平之后就是稳定的生产级并行。