删库重建还是修?一个 WASM 数据库的恢复决策

2026-07-28

最近我的 GBrain 知识库挂了。
不算意外——用 PGLite(WASM 嵌入式 PostgreSQL)跑了几个月,终于撞上了那个传说中的 Aborted()

但有趣的是这次解决问题的过程:我没有花时间修数据库,而是直接删了重建。事后复盘发现,这个”不修”的决策在大多数 PGLite 损坏场景下反而是对的。

这篇文章不教你”怎么修 PGLite 数据库”——因为很多时候根本修不了。我整理了 6 种真实损坏场景的判别逻辑和恢复策略,以及当 rm -rf 反而是最优解时的判断依据。

PGLite 是什么(30 秒速览)

PGLite(@electric-sql/pglite)是一个 WASM 版的 PostgreSQL 17.5,不需要安装 Postgres 服务,直接在进程里跑。典型用法:

const db = new PGLite('./path/to/pgdata')  // 持久化到文件系统
  • 纯 WASM 运行,3MB gzipped
  • 单连接设计——一次只能有一个进程打开数据库
  • 支持 pgvector、pg_trgm 扩展
  • 数据存在文件目录(~/.gbrain/brain.pglite/

我用它做 GBrain 知识库的默认存储引擎。GBrain 之前还有另一篇文章写过迁移到 PostgreSQL 的过程,其中 PGLite 的单连接限制是主要迁移原因——但那是另一个故事了。

真实案例:2026-04-30 数据库丢了

问题现场
- gbrain servegbrain exportgbrain querygbrain health——全部报 Aborted()
- MCP 服务也无法连接
- 报错信息没有具体指示,就是一行 RuntimeError: Aborted()

排查过程
1. 怀疑 PGLite 进程还在跑 → ps aux | grep gbrain → 无异常进程
2. 检查数据库目录 → ls -la ~/.gbrain/brain.pglite/目录不存在
3. 确认文件已丢失(可能是之前某次操作异常中断导致)

决策结果:不用修,直接重建。

rm -rf ~/.gbrain/brain.pglite
gbrain init --pglite
gbrain import ~/wiki    # 从 wiki 重新导入 36 个页面
gbrain embed --all      # 重新生成 embedding(512维,HNSW索引)

重建耗时约 10 分钟。加上恢复时间不到 15 分钟。

为什么选择删除重建而不是尝试修复
1. 数据目录已丢失 → 没有可修复的目标
2. 原始数据在 wiki 目录中完好 → 重建成本几乎为零
3. PGLite 的数据库是 WASM 内部状态和目录文件的复合体,没有 SQLite 的 .recover 命令
4. 重建顺便升级了 schema(vector(512) + HNSW)

核心原则:先确认 ls -la 文件是否存在,再决定是修复还是重建。

六种 PGLite 损坏场景

“PGLite 挂了” 这件事在不同场景下意味着完全不同的东西。下面是最常见的六种:

# 场景 症状 根因 恢复策略
1 数据目录丢失 Aborted(),目录不存在 kill -9、系统异常重启 删除重建
2 WASM 运行时崩溃 Aborted(),但目录还在 WASM 兼容性问题/版本升级 升级/降级 PGLite 版本
3 单连接锁冲突 MCP 跑着时 CLI 报错 PGLite 架构限制,非损坏 等锁释放或关 MCP
4 Embedding 数据损坏 搜索异常,非数字向量 写入中断 gbrain embed --stale
5 Schema 版本不匹配 升级后打不开数据库 GBrain 升级后 schema 变化 gbrain doctor + apply-migrations
6 分支 migration 回不来 跑了分支版本 migration 后 master 不认 开发分支 schema 版本冲突 删除重建或回退版本

关键认知:场景 1~3 在用户看来都是 Aborted(),但处理方式完全不同。目录丢失(场景 1)直接重建比尝试修复快 10 倍,WASM 崩溃(场景 2)升级版本可能数据完好无损,锁冲突(场景 3)根本就是假损坏,等几秒就好。

Embedding 损坏的特殊性(场景 4)

这是唯一”修比删划算”的场景。GBrain 的 embedding 列是 JSONB 类型,部分损坏不会阻塞整个数据库——tryParseEmbedding() 函数会逐行尝试解析,遇到损坏行 WARN 跳过而不是崩溃。

// src/core/utils.ts — 简化
function tryParseEmbedding(row: any): number[] | null {
  try {
    return JSON.parse(row.embedding)
  } catch {
    console.warn(`损坏的 embedding,跳过: ${row.id}`)
    return null
  }
}

这种设计让修复成本变得极低:gbrain embed --stale 只重新生成损坏的 embedding。

决策树:删还是修?

                      ┌─────────────────────┐
                      │ PGLite 报错/异常     │
                      └──────────┬──────────┘
                                 │
                    ┌────────────┴────────────┐
                    │ 检查文件是否存在           │
                    │ ls -la ~/.gbrain/brain   │
                    │         .pglite/         │
                    └────┬──────────────┬──────┘
                         │              │
                    目录丢失         目录存在
                         │              │
                   ┌─────┴─────┐  ┌─────┴─────┐
                   │ 删除重建   │  │ 检查错误   │
                   │ 成本低     │  │ 类型       │
                   │ 原始数据   │  └─────┬─────┘
                   │ 在别处     │        │
                   └───────────┘  ┌──────┴─────────┐
                                 │                │
                           WASM 崩溃        锁冲突/数据损坏
                                 │                │
                        ┌────────┴───┐    ┌──────┴──────┐
                        │ 升级版本    │    │ gbrain doctor│
                        │ 或降级重试  │    │ 诊断再修     │
                        └────────────┘    └──────┬──────┘
                                                 │
                                     ┌───────────┴───────────┐
                                     │                       │
                                 可修复                 不可修复
                                     │                       │
                              ┌──────┴───────┐     ┌────────┴───────┐
                              │ gbrain       │     │ 数据在别处     │
                              │ repair-jsonb │     │ → 删除重建     │
                              │ embed --stale│     │ 数据不在别处   │
                              │ 等工具        │     │ → 尝试 pg_dump│
                              └──────────────┘     │   然后重建     │
                                                   └────────────────┘

决策维度对比

维度 删除重建 修复
适用场景 目录丢失、数据可重导、schema 大版本变化 WASM 崩溃、embedding 损坏、小版本迁移
恢复时间 取决于数据量(需重新 embedding) 分钟级(仅修损坏部分)
风险 无(原始数据在别处) 低(只读诊断)
操作复杂度 低(rm → init → import → embed) 中等(需定位具体问题)
是否需要备份 不需要(依赖原始数据源) 建议事前备份

四条关键原则

  1. 数据目录缺失 → 不要尝试修复,直接重建
    PGLite 的数据库文件是 WASM 运行时状态和文件目录的复合体。目录不存在意味着一切都没了,修复工具没有操作目标。

  2. WASM 运行时崩溃 → 先升级/降级版本,数据可能还在
    PostgreSQL 的数据文件格式很稳定。换一个版本 PGLite,同版本的数据库文件可能直接可用。

  3. 部分数据损坏 → 先诊断再修,不要全局重建
    只有 embedding 损坏的场景下,gbrain embed --stale 比重建快得多。但需要先确认这不是全局损坏。

  4. 锁冲突误判为损坏 → 等几秒重试或停 MCP
    这是 PGLite 单连接设计的天生限制,不是数据问题。GBrain 有 pglite-lock.ts 文件锁机制防止误判。

社区最新动态:PGLite 正在变好

写这篇文章时发现的几个有趣的社区动向,说明 PGLite 的损坏恢复正在从”全手工”走向”半自动化”:

PR #994 — 自动 WAL 恢复(未合并,2026-05)
yestheboxer 提交了一个 PR,在 PGLite 启动时检测是否因为 WAL/checkpoint 损坏导致崩溃。如果是,自动重置 WAL 并重试一次。关键设计:
- 只针对 PostgreSQL 17 的文件布局
- dataDirRepair: 'none' 可完全退出
- 恢复成功后实例暴露 repairedDataDir 字段
- 有趣的是维护者的回复:”我们觉得这个应该做成独立工具”

PR #892 — NodeFS 文件锁(未合并,2026-02)
reisepass 的 PR 解决了一个真实痛:同时在两个终端跑 PGLite 导致文件损坏。方案很简单——加一个锁文件,第二个进程直接拒绝打开。同步提交者评论称这”应该在 PGLite 的 dev 模式默认开启”。

Issue #1012 — Fatal error, can’t start instance anymore
pglite@0.4.6 + Debian 13 + node v24 的崩溃报告,目前还没有修复方案。如果你在用这个组合,建议锁定版本。

Issue #223 — macOS 26.3 兼容性
经典的 WASM 版本兼容问题。修复方法:降级到 @electric-sql/pglite@0.4.3

备份策略

PGLite 没有 SQLite 的 .backup 命令,也不支持标准 PostgreSQL 的 pg_dump。但有几种务实的方案:

方案 操作 适合场景
数据源备份 保持 wiki/markdown 目录作为唯一可信源 GBrain 模式(数据在 markdown 文件中)
文件目录备份 cp -a ~/.gbrain/brain.pglite/ backup/ 快速快照,定期执行
迁移到 PostgreSQL gbrain migrate --to supabase 数据重要时迁移到标准 PG
版本锁定 锁定 @electric-sql/pglite 版本 防止 WASM 兼容性崩溃

而我学到的(来自 2026-04-30 教训):

重要数据不要只存在 PGLite 里。 它能跑但不适合做唯一存储。如果原始数据在别处(markdown 文件、git 仓库),重建就是几分钟的事。如果数据只存在于 PGLite 的二进制目录里,丢了才是真·灾难。

GBrain 命令速查

# 第一步:诊断(别急着动手)
gbrain doctor                          # 全面健康检查
gbrain doctor --json                   # JSON 输出(便于 agent 处理)
gbrain integrity check                 # 只读报告(放心跑)
gbrain integrity auto                  # 自动修复(三档置信度)

# 修复(确认是局部问题再调用)
gbrain repair-jsonb [--dry-run]        # 修复 JSONB 双编码
gbrain apply-migrations --yes          # 强制应用迁移
gbrain embed --stale                   # 重新生成损坏的 embedding
gbrain embed --all                     # 全量重新生成

# 重建(原始数据在别处时最快)
rm -rf ~/.gbrain/brain.pglite          # 删除数据库
gbrain init --pglite                   # 重新初始化
gbrain import ~/wiki                   # 重新导入数据
gbrain embed --all                     # 重新生成 embedding

# 迁移(终极方案——换掉 PGLite)
gbrain migrate --to supabase           # 迁移到 PostgreSQL

不止适用于 PGLite

这篇文章虽然以 PGLite + GBrain 为案例,但核心决策逻辑适用于所有嵌入式数据库(SQLite、LevelDB、RocksDB):

如果数据源在别处 → 删了重建最快。 这也是为什么很多系统设计成”数据源 + 可重建索引”的模式——不是为了让数据库随时可以丢,而是让”丢了也不怕”。

如果数据库是唯一存储 → 备份是第一优先级,修复是最后手段。 花时间研究修复工具,不如花时间做一个可靠的备份策略。

回到最初的问题:删库重建还是修?

如果你的原始数据还在,且重建成本可以接受——删了重建永远比修快。这个结论听起来反直觉,但在用了半年的嵌入式数据库上,它是对的。