一、问题背景
在使用 Docker 部署 Gitea 时,配置了 Push Mirror(推送镜像),用于将本地 Gitea 仓库的提交自动同步到另一个 Git 服务。
某次服务器磁盘空间耗尽后,虽然清理磁盘并重启了 Gitea,但部分仓库的 Push Mirror 始终无法恢复正常同步。
本次故障环境:
| 组件 | 配置 |
|---|---|
| Gitea | 1.21.10 |
| 部署方式 | Docker |
| 数据库 | SQLite |
| 队列存储 | LevelDB |
| 镜像类型 | Push Mirror |
| 数据目录 | /data/gitea |
| 队列目录 | /data/gitea/queues/common |
本文记录完整的排查思路、队列重建过程,以及恢复时遇到的目录权限问题。
二、故障现象
磁盘空间恢复后,Gitea Web 页面能够正常访问,其他部分仓库的镜像同步也恢复正常,但个别仓库仍存在以下问题:
-
本地仓库持续产生新提交,但远端仓库没有更新。
-
Push Mirror 页面没有显示任何同步错误。
-
最后成功同步时间一直没有变化。
-
点击手动同步,提示:
镜像同步正在进行中,请稍后再试。
-
多次重启 Gitea,问题仍然存在。
-
检查进程,没有发现正在执行的 Git Push 任务。
这种情况容易被误判为 Git 远端连接失败、认证异常或网络问题。
但与普通 Git Push 失败不同,Gitea 并没有记录明确的推送错误,表现更接近同步任务没有正常执行,或者任务状态没有正确释放。
三、排查过程
3.1 检查 Gitea 日志
首先查看容器日志:
docker logs --tail 300 gitea 2>&1 | grep -Ei 'mirror|queue|error|failed'
发现磁盘空间耗尽期间出现以下错误:
Unable to push sync request for to the queue
Error: write /data/gitea/queues/common/000184.log:
no space left on device
这说明磁盘写满时,Gitea 无法正常向 LevelDB 队列写入任务。
在释放磁盘空间后,又持续出现:
Unable to push sync request for to the queue
for pull mirror repo[8].
Error: already in queue
already in queue 表示队列认为对应的同步请求已经存在,因此拒绝重复入队。
需要注意:
这条日志明确属于 Pull Mirror,并不能直接证明 Push Mirror 的任务卡住。
但结合磁盘写入失败、镜像同步长期停滞以及服务重启无效等现象,可以将队列状态异常列为重点排查方向。
3.2 检查 LevelDB 状态
Gitea 使用 LevelDB 作为队列存储时,相关文件位于:
/data/gitea/queues/common
查看目录:
docker exec gitea ls -lah /data/gitea/queues/common
典型文件包括:
CURRENT
LOCK
LOG
MANIFEST-000193
000194.ldb
000192.log
进一步检查 LevelDB 日志时,发现:
journal@recovery recovering @184
journal@drop journal-184
"chunk length overflows block"
该日志表示 LevelDB 恢复时发现不完整或异常的日志记录。
这与之前发生的磁盘空间耗尽事件具有时间关联。
不过,LevelDB 能够重新打开,并不代表所有队列任务都已经正确恢复。
3.3 检查 Push Mirror 数据库记录
如果 Gitea 使用 SQLite,可以直接查看 Push Mirror 的持久化配置。
先检查表结构:
docker exec gitea sqlite3 \
/data/gitea/gitea.db \
"PRAGMA table_info(push_mirror);"
在 Gitea 1.21.10 中,主要字段包括:
id
repo_id
remote_name
remote_address
sync_on_commit
interval
created_unix
last_update
last_error
查询镜像状态:
docker exec gitea sqlite3 \
/data/gitea/gitea.db \
"SELECT
id,
repo_id,
sync_on_commit,
interval,
datetime(last_update, 'unixepoch') AS last_sync,
last_error
FROM push_mirror;"
检查发现:
-
sync_on_commit = 1,提交后同步已启用。 -
interval配置正常。 -
last_update长时间未更新。 -
last_error为空。
这里需要注意,interval 的单位为纳秒。
例如:
28800000000000
换算为:
28800000000000 / 1000000000 / 3600 = 8 小时
因此,即使没有新的提交触发同步,周期性任务也应该按配置尝试执行。
另外,push_mirror 表没有持久化的 syncing 字段。
不要尝试通过修改 last_update****来解决同步卡住的问题。
该字段记录同步时间,并不是任务锁。
3.4 检查 Git Push 进程
执行:
docker exec gitea sh -c \
"ps -ef | grep -E '[g]it.*(push|remote-|send-pack)'"
没有发现相关进程。
需要说明的是,这只能证明检查时没有正在运行的匹配进程,不能单独证明任务锁没有释放。
综合以上信息,问题排查方向转向 LevelDB 队列。
四、解决方案:停机重建 LevelDB 队列
本次采用的方案是:
停止 Gitea,保留旧 LevelDB 队列目录,重新创建空队列,再启动服务。
相比直接删除队列文件,这种方式具有可回滚性。
4.1 操作前注意事项
Gitea 的 LevelDB common 目录可能由多个后台队列共享,包括:
-
镜像同步任务
-
通知任务
-
Webhook 任务
-
其他异步后台任务
因此,重建该目录不是只清理某一个仓库的镜像任务。
执行前必须确认:
-
磁盘空间已经恢复。
-
SQLite 数据库已经备份。
-
没有其他 Gitea 实例同时访问相同数据目录。
-
可以接受旧队列中尚未执行任务不再自动恢复。
-
当前没有必须保留的关键后台任务。
以下命令假设容器名称为 gitea。如果实际名称不同,需要替换。
4.2 停止 Gitea
docker stop -t 60 gitea
确认容器已经停止:
docker inspect -f '{{.State.Running}}' gitea
预期结果:
false
必须确认停止后再操作 LevelDB 文件。
4.3 备份旧队列并创建新目录
这里不需要知道宿主机的数据卷挂载位置。
通过 --volumes-from 让临时容器使用 Gitea 原有的数据卷。
docker run --rm \
--volumes-from gitea \
--entrypoint /bin/sh \
"$(docker inspect -f '{{.Config.Image}}' gitea)" \
-c '
set -eu
DIR=/data/gitea/queues
TS=$(date +%Y%m%d_%H%M%S)
# 确认旧队列存在
test -d "$DIR/common"
# 保留旧队列
mv "$DIR/common" "$DIR/common.bak.$TS"
# 创建新的队列目录
mkdir "$DIR/common"
OLD="$DIR/common.bak.$TS"
# 继承原目录的属主与权限
chown "$(stat -c %u:%g "$OLD")" "$DIR/common"
chmod "$(stat -c %a "$OLD")" "$DIR/common"
echo "Queue directory recreated."
echo "Backup: $OLD"
'
操作后,容器内目录结构类似:
/data/gitea/queues/
├── common/
└── common.bak.20261026_172000/
其中:
-
common/:新建的空队列目录。 -
common.bak.*:原有 LevelDB 队列备份。
旧数据仍然保留,便于后续检查或回滚。
4.4 启动 Gitea
docker start gitea
检查日志:
docker logs --since 2m gitea 2>&1 |
grep -Ei 'queue|leveldb|mirror|error|failed'
正常情况下,Gitea 会重新初始化 LevelDB 队列。
五、恢复时遇到的权限问题
在实际恢复过程中,首次创建新队列目录后,Gitea 启动失败。
错误日志:
Failed to create queue "notification-service":
unable to open level db at /data/gitea/queues/common:
open /data/gitea/queues/common/LOCK:
permission denied
随后服务退出。
5.1 问题原因
LevelDB 启动时需要创建或访问:
/data/gitea/queues/common/LOCK
如果新队列目录的属主或权限与 Gitea 实际运行用户不匹配,就会出现 permission denied。
特别是在使用临时容器维护数据卷时,新建目录可能由 root 创建,而 Gitea 服务以其他用户身份运行。
5.2 修复权限
先停止服务:
docker stop gitea
然后以旧队列目录为参照修复:
docker run --rm \
--volumes-from gitea \
--entrypoint /bin/sh \
"$(docker inspect -f '{{.Config.Image}}' gitea)" \
-c '
set -eu
DIR=/data/gitea/queues
OLD=$(ls -d "$DIR"/common.bak.* | sort | tail -n 1)
# 修复属主
chown -R "$(stat -c %u:%g "$OLD")" "$DIR/common"
# 修复目录权限
chmod "$(stat -c %a "$OLD")" "$DIR/common"
# 检查结果
ls -ldn "$OLD" "$DIR/common"
'
这里不直接使用固定的 1000:1000,而是读取旧目录的 UID/GID。
这样可以避免不同 Docker 镜像、用户映射或运行环境带来的差异。
5.3 再次启动
docker start gitea
再次查看日志:
docker logs --since 2m gitea 2>&1 |
grep -Ei 'permission denied|queue|leveldb|fatal|error'
修复目录权限后,Gitea 能够正常启动。