Harbor删除仓库报500错误——PostgreSQL系统表pg_statistic TOAST数据损坏修复

Harbor删除仓库报500错误——PostgreSQL系统表pg_statistic TOAST数据损坏修复

记录时间:2026-07-26

环境:Harbor(Docker Compose 部署)/ PostgreSQL 13.14(harbor-db 容器)/ 数据目录挂载在虚拟机存储上

一、问题现象

最近在 Harbor 页面上删除一个 Repository 时,页面直接报 500。对应的 API 请求是:

DELETE /api/v2.0/projects/kuboard/repositories/etcd-host

Harbor Core 返回的错误:

unknown: ERROR: unexpected chunk number 0 (expected 1) for toast value 137202 in pg_toast_2619 (SQLSTATE XX001)

PostgreSQL 日志里也是同样的报错:

ERROR: unexpected chunk number 0 (expected 1) for toast value 137202 in pg_toast_2619

影响范围:

  • Repository 删除失败
  • Artifact 查询失败
  • 部分 Harbor API 返回 500,docker manifest inspect 拉取 manifest 也报 500

环境基本信息:

项目 信息
Harbor 部署方式 Docker Compose
数据库 PostgreSQL 13.14(harbor-db 容器)
镜像存储目录 /data/harbor_data/registry(约 132G)
数据库目录 /data/harbor_data/database(约 175M)

二、排查过程

2.1 初步判断

unexpected chunk number ... for toast value 这个报错属于 PostgreSQL 的 TOAST 数据异常(TOAST 是 PostgreSQL 存储超长字段的机制,大字段会被切成多个 chunk 存到 toast 表里,chunk 序号对不上说明数据页坏了)。

结合历史情况:这台 Harbor 虚拟机的底层存储之前出过异常,而 PostgreSQL 数据目录正好在挂载存储上,Registry 镜像数据看起来没问题。所以初步怀疑是存储异常导致 PostgreSQL 数据页损坏。

接下来要确认坏的到底是 Harbor 的业务表,还是别的什么表。这直接决定修复难度:业务表坏了可能要丢数据,系统表坏了则可能有更轻量的处理方式。

2.2 尝试 1(排除业务表):检查 artifact_reference 表

删除 Repository 的操作会涉及 artifact_reference 表,先怀疑是它坏了。进数据库:

docker exec -it harbor-db psql -U postgres registry

查这张表对应的 toast 表:

SELECT n.nspname, c.relname, c.reltoastrelid::regclass
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = 'artifact_reference';

结果:

public | artifact_reference | pg_toast.pg_toast_17081

它的 toast 表是 pg_toast_17081,跟报错里的 pg_toast_2619 对不上。再验证一下表数据本身:

SELECT COUNT(*) FROM artifact_reference;
-- 290,正常

SELECT id, parent_id, child_id FROM artifact_reference LIMIT 20;
-- 正常返回

表结构、数据行、主键都正常,说明业务表没坏,方向不对。

2.3 定位真正的损坏对象

报错里的 pg_toast_2619,toast 表命名规则是 pg_toast_<主表oid>,直接反查 oid 2619 是哪张表:

SELECT oid, relname, relnamespace
FROM pg_class
WHERE oid = 2619;

结果:

 oid  |   relname
------+--------------
 2619 | pg_statistic

坏的是 pg_statistic——PostgreSQL 的系统统计信息表,不是 Harbor 的业务表。这张表存的是 ANALYZE 收集的列统计信息,供查询优化器估算执行计划用。Harbor 一执行查询,优化器读统计信息就撞上损坏的 TOAST 数据,直接报 SQLSTATE XX001

三、根因分析

完整的因果链:

虚拟机底层存储异常(磁盘 I/O 异常)
        ↓
PostgreSQL 数据页损坏
        ↓
pg_statistic 的 TOAST 数据(pg_toast_2619)出现坏块
        ↓
Harbor 查询触发优化器读取统计信息
        ↓
unexpected chunk number(SQLSTATE XX001)→ Harbor API 500

影响范围确认:

  • 受影响:仅 PostgreSQL 的查询优化统计信息(可重建数据,无业务价值)
  • 未受影响:Harbor Registry 镜像文件(132G 数据完好)、Docker Layer、Artifact 实际数据、Repository 数据

算是不幸中的万幸,pg_statistic 里的数据全部可以通过 ANALYZE 重新生成,清掉重建即可,不涉及业务数据丢失。

四、解决方案

4.1 停止 Harbor Core

先停掉 Harbor Core,避免修复期间持续有查询打到数据库:

docker stop harbor-core

4.2 清理损坏的统计信息并重建

进数据库:

docker exec -it harbor-db psql -U postgres registry

pg_statistic 是系统表,默认不允许直接改,需要先打开系统表修改权限:

-- 仅当前会话生效,允许修改系统表
SET allow_system_table_mods = on;

-- 清空损坏的统计信息表(连同其 toast 表一起清空)
TRUNCATE pg_catalog.pg_statistic;

-- 重新收集全库统计信息
ANALYZE;

三条都正常返回(SET / TRUNCATE TABLE / ANALYZE)。

注意allow_system_table_mods 是高危参数,只在这种修复场景下临时开启,且用 SET 只对当前会话生效即可,不要写进配置文件持久化TRUNCATE pg_statistic 之所以安全,是因为这张表的内容完全可由 ANALYZE 重建;换成其他系统表切勿照搬这个操作。

4.3 恢复 Harbor Core

docker start harbor-core
docker logs harbor-core

日志中不再出现 SQLSTATE XX001 / unexpected chunk number

五、验证

5.1 数据库层验证

SELECT * FROM artifact_reference LIMIT 1;

正常返回,不再报 unexpected chunk number

 id | parent_id | child_id
----+-----------+----------
  1 |       401 |      397

5.2 Registry 层验证

修复前 docker manifest inspect 报 500,修复后再试:

docker manifest inspect reg-hub.gzeport.com/cicd/jdk/debian-openjdk11:20260725

正常返回多架构 manifest:

{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      { "platform": { "architecture": "amd64", "os": "linux" } },
      { "platform": { "architecture": "arm64", "os": "linux" } }
   ]
}

页面上删除 Repository 的操作也恢复正常。修复后确认:Harbor API 正常、Repository 操作正常、manifest 查询正常、Registry 镜像数据完整。

六、注意事项 / 后续整改

6.1 PostgreSQL 数据目录不要放在网络存储上

PostgreSQL 对 fsync、顺序写和 I/O 延迟非常敏感,数据目录放在 NAS/NFS 这类网络文件系统上,一旦存储抖动就容易产生坏页。建议的存放策略:

数据 建议存储 原因
PostgreSQL 数据目录 本地 SSD / 本地虚拟磁盘 对 fsync 和 I/O 延迟敏感,坏页代价高
Harbor Registry 镜像数据 网络存储可接受 大文件顺序读写为主,容量需求大

6.2 增加数据库定期备份

Harbor 的数据库很小(本例才 175M),备份成本极低,但这次要是坏的不是 pg_statistic 而是业务表,没备份就麻烦了。已经写了一个备份脚本(见本目录 harbor-db-backup.sh),每天执行一次、gzip 压缩、保留最近 15 天。

这里有个管道备份的常见问题需要注意:docker exec ... pg_dump | gzip > 备份文件 这种写法,只要重定向发生,gzip 就会先把目标文件创建出来。如果 pg_dump 中途失败,目录里会留下一个空的或半截的 .sql.gz,文件名看起来和正常备份一模一样,恢复时才发现是废的。所以脚本采用先写 .tmp 临时文件、全部校验通过后再改名为正式文件名的方式,并用 trap 保证异常退出时清理临时文件,备份目录里凡是 .sql.gz 结尾的文件一定完整可用:

#!/bin/bash
# Harbor PostgreSQL 数据库定时备份脚本
# 用途:每天备份 harbor-db 容器中的 registry 库,gzip 压缩落盘,保留最近 15 天

# 任一环节出错立即退出;管道中 pg_dump 失败也视为整体失败(pipefail)
set -euo pipefail

BACKUP_DIR="/backup/harbor-db"   # 备份存放目录,建议放在与数据库不同的磁盘/存储上
CONTAINER="harbor-db"            # Harbor 数据库容器名
DB_USER="postgres"
DB_NAME="registry"
KEEP_DAYS=15                     # 保留天数
DATE=$(date +%F)
BACKUP_FILE="${BACKUP_DIR}/registry_${DATE}.sql.gz"
# 先写临时文件,全部校验通过后才改名为正式文件名,
# 保证备份目录里凡是 .sql.gz 结尾的文件一定是完整可用的备份
TMP_FILE="${BACKUP_FILE}.tmp"

# 无论脚本因何种原因退出(pg_dump 失败、校验失败、被信号中断),
# 都清理临时文件,避免半截备份残留在目录里被误当作正常备份
trap 'rm -f "$TMP_FILE"' EXIT

mkdir -p "$BACKUP_DIR"

# 容器未运行时直接报错退出,避免生成空备份文件误导恢复
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
    echo "[$(date '+%F %T')] ERROR: 容器 ${CONTAINER} 未运行,备份中止"
    exit 1
fi

echo "[$(date '+%F %T')] 开始备份 ${DB_NAME} -> ${BACKUP_FILE}"
# pipefail 保证管道左侧 pg_dump 失败时整个管道返回非零;
# 用 if ! 显式接住错误并打日志,而不是依赖 set -e 静默退出
if ! docker exec "$CONTAINER" pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$TMP_FILE"; then
    echo "[$(date '+%F %T')] ERROR: pg_dump 或 gzip 执行失败,本次备份中止(临时文件由 trap 清理)"
    exit 1
fi

# 双重校验:文件大小兜底(空库 dump 压缩后也远超 1K)+ gzip 完整性检查,
# 防止磁盘写满或管道中断产生截断的备份文件
if [ "$(stat -c %s "$TMP_FILE")" -lt 1024 ] || ! gzip -t "$TMP_FILE" 2>/dev/null; then
    echo "[$(date '+%F %T')] ERROR: 备份文件校验失败,本次备份中止(临时文件由 trap 清理)"
    exit 1
fi

# 校验全部通过,原子改名为正式备份文件
mv "$TMP_FILE" "$BACKUP_FILE"

# 清理旧备份:-mtime +14 匹配修改时间超过 14 天的文件,
# 即删除第 15 天之前的备份,目录中始终保留最近 15 天(15 份)
find "$BACKUP_DIR" -name "registry_*.sql.gz" -mtime +$((KEEP_DAYS - 1)) -delete

echo "[$(date '+%F %T')] 备份完成,当前保留文件:"
ls -lh "$BACKUP_DIR"/registry_*.sql.gz

部署方式:

# 复制到 Harbor 服务器并赋执行权限
cp harbor-db-backup.sh /opt/scripts/harbor-db-backup.sh
chmod +x /opt/scripts/harbor-db-backup.sh

# 手动执行一次,确认备份文件正常生成
/opt/scripts/harbor-db-backup.sh

# 配置 crontab,每天凌晨 02:30 执行(错开业务低峰即可)
crontab -e
# 添加:
30 2 * * * /opt/scripts/harbor-db-backup.sh >> /var/log/harbor-db-backup.log 2>&1

注意BACKUP_DIR 建议放在与数据库数据目录不同的磁盘/存储上。这次的根因就是存储异常,备份和数据放同一块存储等于没备。恢复时用 gunzip -c registry_YYYY-MM-DD.sql.gz | docker exec -i harbor-db psql -U postgres registry 导回。

6.3 增加存储健康巡检

虚拟机层(Linux):

dmesg | egrep -i "error|fail|io error|disk"

宿主层(ESXi)重点关注:APD(All Paths Down,存储路径全断)、PDL(Permanent Device Loss,设备永久丢失)、datastore latency、storage reset 事件。

6.4 同类报错的判断思路

再遇到 pg_toast_<oid> 相关报错,先用 SELECT relname FROM pg_class WHERE oid = <oid> 反查主表:

  • 坏的是 pg_statistic 这类可重建的系统表 → 按本文方式清理重建即可
  • 坏的是业务表 → 优先从备份恢复;没有备份再考虑 zero_damaged_pages 等有损手段,且操作前必须先冷备整个数据目录

七、参考资料

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇