Gitea Push Mirror 镜像同步卡住:磁盘写满导致 LevelDB 队列异常的排查与恢复

一、问题背景

在使用 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 页面能够正常访问,其他部分仓库的镜像同步也恢复正常,但个别仓库仍存在以下问题:

  1. 本地仓库持续产生新提交,但远端仓库没有更新。

  2. Push Mirror 页面没有显示任何同步错误。

  3. 最后成功同步时间一直没有变化。

  4. 点击手动同步,提示:

bash 复制代码
镜像同步正在进行中,请稍后再试。
  1. 多次重启 Gitea,问题仍然存在。

  2. 检查进程,没有发现正在执行的 Git Push 任务。

这种情况容易被误判为 Git 远端连接失败、认证异常或网络问题。

但与普通 Git Push 失败不同,Gitea 并没有记录明确的推送错误,表现更接近同步任务没有正常执行,或者任务状态没有正确释放。

三、排查过程

3.1 检查 Gitea 日志

首先查看容器日志:

bash 复制代码
docker logs --tail 300 gitea 2>&1 | grep -Ei 'mirror|queue|error|failed'

发现磁盘空间耗尽期间出现以下错误:

bash 复制代码
Unable to push sync request for to the queue
Error: write /data/gitea/queues/common/000184.log:
no space left on device

这说明磁盘写满时,Gitea 无法正常向 LevelDB 队列写入任务。

在释放磁盘空间后,又持续出现:

bash 复制代码
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 作为队列存储时,相关文件位于:

bash 复制代码
/data/gitea/queues/common

查看目录:

bash 复制代码
docker exec gitea ls -lah /data/gitea/queues/common

典型文件包括:

bash 复制代码
CURRENT
LOCK
LOG
MANIFEST-000193
000194.ldb
000192.log

进一步检查 LevelDB 日志时,发现:

bash 复制代码
journal@recovery recovering @184
journal@drop journal-184
"chunk length overflows block"

该日志表示 LevelDB 恢复时发现不完整或异常的日志记录。

这与之前发生的磁盘空间耗尽事件具有时间关联。

不过,LevelDB 能够重新打开,并不代表所有队列任务都已经正确恢复。

3.3 检查 Push Mirror 数据库记录

如果 Gitea 使用 SQLite,可以直接查看 Push Mirror 的持久化配置。

先检查表结构:

bash 复制代码
docker exec gitea sqlite3 \
  /data/gitea/gitea.db \
  "PRAGMA table_info(push_mirror);"

在 Gitea 1.21.10 中,主要字段包括:

bash 复制代码
id
repo_id
remote_name
remote_address
sync_on_commit
interval
created_unix
last_update
last_error

查询镜像状态:

bash 复制代码
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 的单位为纳秒。

例如:

bash 复制代码
28800000000000

换算为:

bash 复制代码
28800000000000 / 1000000000 / 3600 = 8 小时

因此,即使没有新的提交触发同步,周期性任务也应该按配置尝试执行。

另外,push_mirror 表没有持久化的 syncing 字段。

不要尝试通过修改 **last_update** 来解决同步卡住的问题。

该字段记录同步时间,并不是任务锁。

3.4 检查 Git Push 进程

执行:

bash 复制代码
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

bash 复制代码
docker stop -t 60 gitea

确认容器已经停止:

bash 复制代码
docker inspect -f '{{.State.Running}}' gitea

预期结果:

bash 复制代码
false

必须确认停止后再操作 LevelDB 文件。

4.3 备份旧队列并创建新目录

这里不需要知道宿主机的数据卷挂载位置。

通过 --volumes-from 让临时容器使用 Gitea 原有的数据卷。

bash 复制代码
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"
  '

操作后,容器内目录结构类似:

bash 复制代码
/data/gitea/queues/
├── common/
└── common.bak.20261026_172000/

其中:

  • common/:新建的空队列目录。

  • common.bak.*:原有 LevelDB 队列备份。

旧数据仍然保留,便于后续检查或回滚。

4.4 启动 Gitea

bash 复制代码
docker start gitea

检查日志:

bash 复制代码
docker logs --since 2m gitea 2>&1 |
grep -Ei 'queue|leveldb|mirror|error|failed'

正常情况下,Gitea 会重新初始化 LevelDB 队列。

五、恢复时遇到的权限问题

在实际恢复过程中,首次创建新队列目录后,Gitea 启动失败。

错误日志:

bash 复制代码
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 启动时需要创建或访问:

bash 复制代码
/data/gitea/queues/common/LOCK

如果新队列目录的属主或权限与 Gitea 实际运行用户不匹配,就会出现 permission denied。

特别是在使用临时容器维护数据卷时,新建目录可能由 root 创建,而 Gitea 服务以其他用户身份运行。

5.2 修复权限

先停止服务:

bash 复制代码
docker stop gitea

然后以旧队列目录为参照修复:

bash 复制代码
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 再次启动

bash 复制代码
docker start gitea

再次查看日志:

bash 复制代码
docker logs --since 2m gitea 2>&1 |
grep -Ei 'permission denied|queue|leveldb|fatal|error'

修复目录权限后,Gitea 能够正常启动。

相关推荐
shark_chili1 小时前
从需求到架构:我如何用 AI 写好design.md
后端
夕颜1111 小时前
一次跨 Mac 的单站点登录态迁移记录:从「能打开」到「重启后仍可用」
后端
武子康2 小时前
LingBot-VLA 2.0 怎么从 6B 权重走到真实机器人?55 维动作只是起点
人工智能·后端·agent
学长毕设2 小时前
基于springboot的社会志愿者管理系统-附源码
java·spring boot·后端
喵了几个咪2 小时前
GoWind CMS 拆解:给 NL2SQL 上四把锁——智能问数的安全边界
开发语言·后端·安全·微服务·golang·gowind
用户8356290780513 小时前
Python 操作 Excel:创建数组公式与 R1C1 公式
后端·python
乘风gg3 小时前
出事了,AI 提效后,老板让我裁员20%
前端·后端·ai编程
Rain的Java大神之路3 小时前
面了 50 个候选人,90% 都挂在 LeetCode 3 无重复字符的最长子串:滑动窗口的 5 个致命坑
java·后端·leetcode
狂师4 小时前
夯爆了!分享几个我在用的,基于业务场景接口串联测试Skill !
人工智能·后端·测试