前阵子接到项目组的求助:CVAT任务和项目无法创建的报错反复出现,标注小组一到要开新一批任务就卡壳。页面转圈几秒后弹出一句“系统错误”,换浏览器、清缓存、重启容器都试过,问题依旧。老实说,这种“系统错误”在CVAT的日常运维里出现频率非常高,但绝大多数人一开始都会像我一样,在错误的层面浪费掉大量时间。
后来我把创建链路完整拆了一遍,发现这类报错几乎都指向同一个“责任层”——不是前端界面坏了,而是后端的数据库、存储或后台任务服务出了问题。这篇文章就照着我的排查路径来写,从原理到实操、从定位到修复,把“任务和项目无法创建(系统错误)”这个经典问题彻底说清楚,希望能帮到正在被这个报错折磨的人。
1. CVAT创建任务/项目失败时,报错链路上谁是真正的“责任人”
1.1 一条“新建任务”请求到底经过哪些环节
很多刚接触CVAT的人会误以为“创建任务”就是点个按钮、填个名字、传几张图这么简单。实际上,从你点击“Submit”到页面显示“任务创建成功”,背后至少要走完这样一串流程:
- 前端页面收集任务参数(名称、项目归属、标注类别、文件列表等),发起POST请求;
- 请求到达后端API层(cvat_server容器内的Django应用),先做登录态和权限校验;
- 校验通过后,后端开始向数据库写入Task记录,同时在存储后端分配任务数据目录(可能是本地磁盘目录,也可能是对象存储的bucket);
- 随后后端会把“导入数据”这类重活抛给后台任务队列(由rq-worker容器处理),由它负责解压文件、解析图片、生成标注Job;
- 所有步骤完成后,前端才能拿到成功的响应。
这个链路里的任何一环掉链子,前端UI都会统一弹出一个“System error”的红色提示。也就是说,这句话本身只是一个兜底信息,它不告诉你具体哪里出了问题,真正的问题隐藏在服务器端日志里。
1.2 “系统错误”这句话翻译过来是什么
从HTTP状态码角度理解会更清楚。CVAT前端把非2xx的响应都归为“系统错误”,最常见的是三种情况:
- 500 Internal Server Error:后端代码执行中抛出未捕获异常,比如数据库写入失败、文件系统权限不足;
- 502 / 504 Bad Gateway / Gateway Timeout:反向代理(如Nginx)无法从后端拿到及时响应,多半是后端进程卡死或数据库锁等待超时;
- 409 Conflict:资源冲突,例如同名任务已存在、存储后端状态不一致。
这三种状态码的修复方向完全不同。所以排查的第一步,永远不是去服务器“重启一下试试”,而是先在浏览器开发者工具里把真正的HTTP状态码和响应体捞出来。这一步看似简单,却能让你少走一个小时的弯路。
2. 最常见的四种根因:日志表、连接池、存储后端、迁移遗留
2.1 日志表爆炸:最隐蔽的“隐形杀手”
我处理过的模拟项目X里,出现这个报错频率最高的原因,是数据库里某张日志流水表异常膨胀。
CVAT本身带了一些任务操作日志和状态变更记录。在长期运行、日均标注操作量很大的部署环境里,这些流水表会以惊人的速度增长。某次现场排查时,我进入数据库看了一眼,最大的那张日志表已经占了几十个GB,同一个磁盘分区上还运行着数据库的数据文件。当磁盘剩余空间逼近临界值的时候,数据库写入任何一条新记录都会变得异常缓慢,甚至直接报错。创建任务恰恰是需要往多张表插入新数据的操作,于是表现为“系统错误”。
这类根因最迷惑人的一点在于:页面可能还能正常浏览、任务列表还能加载,只有创建新任务和新项目时才会失败,因为只有写操作才真正触发了资源瓶颈。如果你发现报错集中在“新建”类操作上,优先怀疑数据库写入链路。
2.2 连接池被蘸干:并发标注后的“连接悬空”
第二种高频根因是数据库连接数被耗尽了。
CVAT的后端服务使用数据库连接池,但连接池不是一个无限大的池子。当标注小组连续导入多批大数据集、后台同时在跑多个自动标注任务时,SQLAlchemy持有的连接可能会因为长事务、慢查询或者异常退出而没被及时释放。一旦数据库活跃连接数达到max_connections上限,新的创建请求就再也拿不到连接,只能等超时后抛出异常。
这个问题的诡异之处在于:报错是间歇性的,过一阵子可能又能创建成功了——因为某些后台任务跑完后连接被释放了。很多团队在这种“时好时坏”的状态里反复重启服务,其实只是不断打断正常的任务队列,并没有触达根因。
2.3 存储后端与权限初始化失败
第三种根因跟任务数据的“存放位置”有关。
CVAT创建任务时,会为任务分配一个存储空间。如果你的部署环境配置了对象存储(比如某个S3兼容服务),而后端存储的bucket被误删、AccessKey失效、或者存储服务端改了权限策略,任务创建时一旦尝试访问不存在的bucket,就会在API层抛异常。
对应的报错日志里通常会出现这类关键词:unable to create bucket、AccessDenied、NoSuchBucket。还有一种情况是配置文件里挂着旧的存储参数,实际后端存储已经换掉了,服务器端写数据时拿着旧配置去访问不存在的位置,自然创建失败。
2.4 版本升级后的迁移遗留
最后一个容易被忽略的根因:CVAT版本升级后没有正确执行数据库迁移。
CVAT的数据库结构会随着版本变化,新版本可能新增了字段、表或者修改了约束。如果你直接从旧版本升级到新版本,但没有跑迁移命令(或者迁移过程中断、报错被忽略),那么代码里创建Task记录时要写入的某个字段在数据库里根本不存在。此时每次创建任务都会触发数据库异常。
这类根因的排查往往滞后,因为报错时间点距离升级时间可能已经隔了很久,很少有人会把“一周前做的升级”和“今天创建任务失败”联系起来。但如果你翻看服务器日志,会发现异常堆栈里几乎都是数据库字段层面的错误消息。
2.5 四个根因速查对照表
给一张对照表,方便你快速把现场现象映射到最可能的根因上,接着再进入详细排查:
| 根因 | 典型现象 | 日志关键词 | 定位命令方向 |
|---|---|---|---|
| 数据库日志表膨胀 | 磁盘告警,所有写操作变慢,只有创建类操作失败 | No space left on device、database is full | 查磁盘占用、查最大的表 |
| 数据库连接池耗尽 | 间歇性报错,连续导入或自动标注多时触发 | connection limit exceeded、too many connections | 查连接数、查闲置连接 |
| 存储后端异常 | 换过存储配置或删除过资源后出现 | AccessDenied、NoSuchBucket | 查对象存储配置、查bucket状态 |
| 升级迁移遗留 | 最近升过级,之后新功能创建都报错 | column does not exist、relation does not exist | 查迁移状态、检查schema |
3. 一次完整的排查链路:从浏览器报错到数据库内部
3.1 第一步:在浏览器开发者工具里拿到第一手失败信息
碰到“系统错误”优先打开浏览器F12,切到Network面板,然后重新点击创建任务。找出发起请求的接口(一般是路径包含tasks或projects的POST请求),点击查看Status Code和Response内容。
这一步会让你快速区分两种情况:
- 如果响应体里直接包含
psycopg2.OperationalError之类的异常字符串,说明是数据库层问题; - 如果响应体是类似
{"non_field_errors": ["..."]}的通用消息,就需要结合服务器端日志继续查。
举个例子,我碰到过一例响应体是connection limit exceeded for user "cvat",当时几乎就能锁定连接池问题了。花30秒看Network面板,比盲目重启几个容器要高效得多。
3.2 第二步:顺着cvat_server日志往下挖
浏览器里的信息只是冰山一角,完整的异常堆栈要进服务器看cvat_server容器的日志:
docker compose logs --tail=200 cvat_server如果日志量太大,可以先看最近20分钟内的内容:
docker compose logs --since=20m cvat_server | grep -i error常见的关键词包括:
No space left on device→ 磁盘满了,需要清理或扩容;connection limit exceeded for user "cvat"→ 连接池问题;column xxx does not exist→ 数据库迁移问题;NoSuchBucket/AccessDenied→ 存储后端问题。
拿到异常类型后,后面的排查方向就非常清晰了。
3.3 第三步:查数据库内部状态
如果日志指向数据库层,下一步进数据库容器做状态检查。CVAT默认使用PostgreSQL,容器名一般是cvat_db:
docker exec -it cvat_db psql -U cvat -d cvat先看数据库当前连接数是否已经逼近上限:
SELECT count(*) FROM pg_stat_activity WHERE datname = 'cvat';再看服务端允许的最大连接数:
SHOW max_connections;如果当前连接数已经非常接近最大连接数,连接池问题就基本实锤了。接着查一查哪些连接占着位置不干活:
SELECT pid, usename, state, now() - state_change AS idle_for FROM pg_stat_activity WHERE datname = 'cvat' ORDER BY idle_for DESC;这时候你会看到一长串状态为idle的连接,它们很可能属于之前某次任务导入或自动标注遗留的“连接悬空”。
3.4 第四步:检查磁盘与容器健康状态
数据库层检查完,回到宿主机上看一眼磁盘:
df -h重点关注根分区和/var/lib/docker所在分区的剩余空间。我遇到过有的部署环境docker镜像和数据堆积占满了整个分区,剩余空间只有几百MB,数据库写新记录时根本写不进去。
顺带检查所有关键容器是否处于healthy状态:
docker compose ps如果cvat_redis或rq-worker显示异常,也可能影响任务创建流程,因为任务创建后的数据导入依赖后台任务队列来执行。
4. 对应根因的解决实操:清理、瘦身、修复与迁移
4.1 日志表清理与数据库瘦身实操
确认是日志表膨胀导致的磁盘吃紧后,需要先把最大的表捞出来看清楚是什么表。在PostgreSQL里执行:
SELECT schemaname, tablename, pg_total_relation_size(schemaname || '.' || tablename) / 1024 / 1024 AS size_mb FROM pg_tables WHERE schemaname = 'public' ORDER BY size_mb DESC LIMIT 10;你会清晰地看到哪些表是“重量级选手”。如果这张表确认是历史日志/操作流水类数据,并且业务上允许清理,可以按时间窗口删除老数据,或者直接清空重建:
-- 先备份确认过再执行,别手滑 BEGIN; DELETE FROM log_record_table WHERE created_at < NOW() - INTERVAL '30 days'; COMMIT; ANALYZE log_record_table;如果表里的数据已经完全不需要了,也可以直接:
TRUNCATE TABLE log_record_table;注意:清理前务必确认表名对应的数据确实可清理。CVAT里任务相关的业务数据不能随便清,否则会破坏整个标注系统的数据完整性。清理后记得重新执行
ANALYZE,让数据库统计信息更新,否则后续查询性能不会立刻回升。
清理完几十GB的日志表后,磁盘剩余空间恢复正常,创建任务通常马上就能用了。这套操作我至少帮三四个团队解过燃眉之急。
4.2 连接池瘦身与参数调整
连接数耗尽时,最快恢复在线的方式是先手动清理掉那些长期闲置的连接:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'cvat' AND state = 'idle' AND now() - state_change > INTERVAL '10 minutes';执行完再查一次连接数,确认释放出一批空闲连接,任务创建就能先恢复。
但要根治这个问题,还得调整连接池参数。在CVAT的部署配置中,后端连接数据库时使用的连接串里可以增加连接池限制参数:比如pool_size和pool_recycle。写一个常见配置示例(实际参数值要根据部署规模调整):
DATABASE_CONNECTION_MAX_AGE=600 DATABASE_CONNECTION_POOL_SIZE=20同时,数据库侧的max_connections也可以适当调大,但要注意宿主机内存上限,不是越大越好。PostgreSQL每个连接都会占用固定内存,无脑拉高很可能拖垮整个实例。
提示:改完连接池参数后,需要重启cvat_server容器才能生效。重启前把
pg_stat_activity快照留下,方便重启后对比连接回收情况。另外,排查一下为什么会出现“连接悬空”——大概率是某些长任务异常退出导致连接没归还,这类问题光调参数不治本,后续得关注后台任务队列的健康度。
4.3 存储后端修复与迁移执行
存储后端类的报错,需要分两种场景处理。
如果使用的是对象存储,且bucket被删或权限失效,最快的方式是在存储服务端重建同名bucket,并确保AccessKey具备读写权限。如果bucket已经在界面上无法恢复,就需要在CVAT的服务配置里把默认存储切回本地存储,或者换一个新的bucket并更新环境变量,然后重启服务。
如果改动了CVAT的存储相关配置,验证方式很简单:重新尝试上传一张小图片并创建带单个文件的测试任务。只要能创建成功,再逐步增加数据量。
4.4 版本升级后的迁移修复
对于迁移遗留导致的报错,先确认当前迁移状态:
docker compose run --rm cvat_server python manage.py migrate --check如果提示有未执行的迁移,直接执行:
docker compose run --rm cvat_server python manage.py migrate执行成功后重启所有服务,再验证任务创建。这里给个小提醒:升级CVAT版本前,无论如何先备份数据库,迁移失败时可以快速回滚,不要裸奔升级。
5. 防再犯:巡检脚本与容量规划
5.1 一套低成本的每日巡检方案
处理完故障之后,如果不做预防,这类问题大概率还会在几个月后卷土重来。我的建议是部署一个简单的巡检脚本,每天检查五个核心指标:
- 磁盘剩余空间;
- 数据库当前连接数;
- 数据库最大表的Top 5;
- 关键容器运行状态;
- 最近半小时内cvat_server日志有没有新增异常。
一个参考脚本框架(bash)如下:
#!/bin/bash echo "=== DISK ===" df -h | grep -E '(/|docker)' echo "=== PG CONNECTIONS ===" docker exec cvat_db psql -U cvat -d cvat -c "SELECT count(*) FROM pg_stat_activity WHERE datname='cvat';" echo "=== TOP TABLES ===" docker exec cvat_db psql -U cvat -d cvat -c "SELECT schemaname, tablename, pg_total_relation_size(schemaname || '.' || tablename) / 1024 / 1024 AS size_mb FROM pg_tables WHERE schemaname='public' ORDER BY size_mb DESC LIMIT 5;" echo "=== CONTAINERS ===" docker compose ps echo "=== SERVER LOG ERRORS ===" docker compose logs --since=30m cvat_server | grep -i error | tail -20把这个脚本加到cron里,每天执行一次,输出重定向一份到独立文件。就算哪天又出问题,你手里也有一份从故障前几天开始的状态趋势,复盘定位会快得多。
5.2 日志数据自动清理与容量规划建议
对于日志流水类的膨胀问题,我强烈建议在数据库侧设置定时清理任务,保留合理时间窗口内的数据,其余定期删除。比如可以写一个每日执行的清理函数,删除30天前的日志记录,并顺手更新统计信息。
磁盘容量规划方面,按照我的经验,一个持续运营的CVAT实例,除了数据文件本身,至少要预留20%以上的余量给日志、临时文件和docker镜像。很多“系统错误”的隐患在磁盘用到70%以上时就开始冒头,等剩几百MB才处理就太被动。
最后再分享一条个人经验:遇到“任务和项目无法创建(系统错误)”,第一反应不要是“重启大法”。你先去浏览器Network里看响应状态码,再对着cvat_server日志找异常堆栈,这两个动作覆盖了约80%的排查路径。真正花了时间定位之后,你会发现这四个根因里至少有一个能对上你现场看到的现象。处理完一轮之后,再把巡检脚本安排上,这类问题基本就能从“半夜被叫醒”变成“早上看报告”。