☰
RPM数据库损坏修复:真伪判断、重建流程与根因排查
2026/10/25 2:47:47 网站建设 项目流程

凌晨两点多收到告警,值班同事连上去一看,整台机器上跟包管理沾边的命令全瘫了:rpm -qa卡在那里不返回,yum一敲就吐出一长串BDB0113和BDB1507的报错,连ls /var/lib/rpm都看得出不对劲。当时最直接的应急手段就是重建 RPM 数据库,也就是很多人笔记里随手记下的那一条"3.2.7、重建 RPM 数据库(修复损坏的 RPM 数据库)"。但真正做过的人都知道,这条笔记只有一行字,实际操作里的门道远不止敲一个--rebuilddb:数据库到底是哪一层坏了、该走哪条重建路径、重建前必须备份什么、重建后怎么确认没丢包、以及为什么有些机器的库会反复坏,这些才是能不能一次性把问题按死的关键。这篇内容面向日常跟 Linux 打交道、但对包管理底层结构没系统研究过的运维和开发,讲清楚 RPM 数据库的物理形态、真伪损坏的辨别方法、不同版本对应的重建套路,以及我自己踩过的那几个坑。

1. RPM 数据库的物理组成:由一组索引文件构成

1.1 打开 /var/lib/rpm 看一眼,比读十页文档管用

很多人对 RPM 数据库的印象停留在"一个索引文件",实际去/var/lib/rpm里ls -lh一下就知道这个认知偏差有多大。在 Berkeley DB 作为后端的年代,这个目录里躺着十几到几十个文件,Packages通常是体积最大的那个,动辄上百 MB,剩下的Name、Basenames、Dirnames、Providename、Requirename、Conflictname、Obsoletename、Triggername、Filetriggername等等,每个可能只有几百 KB 到几 MB。体积差异本身就说明了一件事:Packages里存的是真正的数据,其余那些文件都是围绕它建立的查询索引。

Packages存的是什么?是每一个已安装软件包的 header 结构体,里面包含了包名、版本、release、架构、依赖列表、文件清单、安装时间、校验和、脚本片段等全部元信息。你可以把它理解成一本字典的正文。而Name是"按包名查 header 位置"的目录页,Basenames和Dirnames配合起来是"按文件路径反查所属包"的目录页,Providename和Requirename是依赖解析用的交叉索引。这个分层关系直接决定了故障的可恢复上限:索引坏了能重建,正文坏了就只能抢救。

到了 sqlite 后端时代,整个结构被压进一个rpmdb.sqlite文件。表面上看是简化了,但内部逻辑没变,只是原本分离的那些文件变成了同一个数据库里的不同表:Packages表存 header blob,Name、Basenames等表存索引。我见过有人用sqlite3打开这个文件,看到一堆表名跟老版本的目录列表一一对应,一下子就明白了两代后端之间的继承关系。

1.2 索引与数据分离,决定了你最终能救回多少

把上面对应到修复场景,判断路径就很清晰了。假设一台机器上有 1200 个已安装包,/var/lib/rpm里的Packages是 130MB 左右,Name只有 400KB。如果故障只是索引文件写坏了或者锁文件残留,Packages完好无损,那重建就是无损操作,1200 个包的记录一个不少全回来。反过来,如果Packages本身出现了 header 级的 CRC 错误,重建过程会跳过那些解析失败的记录,重建完成后rpm -qa的数量会少几个,这时候你就知道有东西丢了。

再往下一层,如果Packages连打开都打不开,那重建命令根本跑不起来,必须先想办法把里面的记录导出成文本再重新灌回去。这套"先导出再重建"的操作,在老版本上用db_dump和db_load完成,在新版本上换成 sqlite 的.recover或者 rpm 自带的导出导入子命令。三代后端、三种抢救路径,如果一上来不判断是哪一层坏了,直接照搬某篇博客里的命令,很容易在错误的路上越走越远。

提示:判断之前先做一件事——把整个/var/lib/rpm目录原样复制一份到别的分区。复制动作本身不改动任何东西,但它是后面所有折腾的退路。

2. 真损坏还是假故障:三条命令划清界线

2.1 rpm --verifydb 能给出什么,又给不出什么

较新版本的 rpm 提供了一个专门的检查入口:

rpm --verifydb

正常情况下它什么都不输出,退出码为 0。一旦数据库有问题,它会报出具体的错误类型,比如页校验失败、记录长度异常、索引指向了不存在的位置。这个命令的好处是只读,不会改动数据库,跑一次没有副作用。配合下面这条一起看更有价值:

rpm -qa | wc -l

如果这条命令能正常返回一个数量,说明Packages主库至少还能被顺序读出来,问题大概率在索引层。如果它卡住不动超过几十秒,或者返回的数量明显少于你印象中的包数(比如一台跑了两年的业务机只报了 30 个包),那就要往主库方向怀疑了。

但--verifydb有它的局限。老版本的 rpm 上这个选项可能压根不存在,敲下去会直接告诉你 unknown option;还有些情况下它能返回成功,但索引其实已经跟主库不一致了,比如某个包被强行删了文件但索引没更新。所以我一般把它当作"第一道筛查"而不是"最终结论",真正的验证还是要靠后面的实际查询操作。

2.2 那些长得像数据库损坏、其实完全是别的问题

这一节是我最想强调的部分,因为按我处理过的案例统计,报错文本指向"数据库损坏"但根因不是数据库的情况,占比相当高。下面这几类最容易混淆:

第一类是锁文件残留。有人习惯用kill -9强杀yum或dnf,进程没了但锁没释放,/var/lib/rpm/.rpm.lock还挂着,后续所有 rpm 操作都在等这把锁,表现出来就是命令卡死。用fuser /var/lib/rpm/.rpm.lock或者lsof看一眼有没有残留持有者,如果有并且确认进程已经不在,清掉锁文件即可恢复。

第二类是磁盘空间不足。/var分区写满之后,sqlite 后端写 WAL 文件会失败,报错里出现database or disk is full,看起来特别像数据库坏了。df -h /var一查就清楚,跟损坏毫无关系。

第三类是权限和 SELinux 上下文被改坏。有些自动化脚本用chown -R扫过/var/lib,把/var/lib/rpm的所有者改成了别的用户,root 反而写不进去;或者restorecon没跑,上下文变成了default_t,包管理器被拦在门外。

第四类是并发冲突。两个包管理进程同时运行,或者dnf正在跑的时候有人手动敲rpm -i,两个进程对同一个库同时操作,sqlite 直接报database is locked。

现象更可能的真实原因先做的检查
命令卡住不返回,无报错锁文件残留或另一个进程在跑fuser /var/lib/rpm/.rpm.lock、ps aux | grep -E 'rpm|dnf|yum'
报database or disk is full/var分区空间耗尽df -h /var、df -i /var
报权限拒绝,即使 root 执行目录属主或 SELinux 上下文异常ls -ld /var/lib/rpm、ls -Z /var/lib/rpm
报database is locked并发写冲突找残留进程,不要直接删库
命令能跑但包数明显变少索引不一致或主库部分记录损坏rpm --verifydb、rpm -qa | wc -l

把这张表过一遍,能挡掉大半"误判为损坏"的情况。

2.3 新版本上的只读体检手法

sqlite 后端有一个额外好处:可以用sqlite3直接对文件做只读诊断,前提是一定要加只读参数,不要在系统运行中让它写文件。

sqlite3 -readonly /var/lib/rpm/rpmdb.sqlite "PRAGMA integrity_check;"

输出ok说明页结构完整。输出一串错误行说明文件有物理损坏。还可以看看表结构和记录数:

sqlite3 -readonly /var/lib/rpm/rpmdb.sqlite ".tables" sqlite3 -readonly /var/lib/rpm/rpmdb.sqlite "select count(*) from Packages;"

这两个命令能让你在动手之前就知道"主库记录数是多少",为后面的重建结果提供对比基准。我现在的习惯是每次上机排查,第一件事就是把这个数字记下来,重建完成后再对一次,少了多少一目了然。

3. 从备份到复核:一次完整重建的动作序列

3.1 备份这一步省掉的人,后面基本都在返工

重建命令本身只有一行,但真正决定成败的是它前面的准备。要做的第一件事是把整个目录打包备份,而且目标位置不能在同一个分区上:

mkdir -p /root/rpmdb-backup tar -C /var/lib -czf /root/rpmdb-backup/rpm-$(date +%Y%m%d-%H%M).tgz rpm

顺带把/var/lib/dnf也备一份。很多人不知道现代版本里 DNF 的事务历史存在/var/lib/dnf/history.sqlite,这是独立于 rpmdb 的另一套数据,重建 rpmdb 不会碰它,但如果事后需要追溯"这台机器上装过什么",这份历史就是救命稻草。同样值得一起备份的还有/var/log/dnf.log和/var/log/dnf.rpm.log,它们记录了每一次安装、升级、卸载的完整过程。

备份完成之后,进维护窗口。如果条件允许,切到单用户或者至少停掉所有可能触发包管理的定时任务,systemd里那些自动更新单元、监控采集脚本里的rpm -q轮询,统统先停。我遇到过一次重建做到一半,某个巡检脚本又去调rpm -qa,直接把新库又锁住,白折腾一轮。

3.2 老后端的两条路径:先清环境文件,再考虑导出重灌

Berkeley DB 后端时代,最常见的故障是环境 region 文件不一致,也就是那堆__db.001到__db.005。典型报错是BDB0113 Thread/process failed和BDB1507 Thread died in Berkeley DB library。处理思路很直接:

# 先确认没有进程占用 fuser /var/lib/rpm/.rpm.lock # 清掉环境文件,注意不要动 Packages 和索引文件 rm -f /var/lib/rpm/__db.* # 重建索引 rpm --rebuilddb -v

-v参数会让它把处理过程打出来,能看到一个个包名滚过去,也方便判断是不是卡在某个特定包上。这一步能解决绝大多数索引不一致的问题,因为__db.*只是共享内存区域文件,删掉不影响任何持久化数据,下次启动会重新生成。

如果清完环境文件、重建依然报 BDB 层的错误,说明问题已经深入到Packages内部了,得走导出重灌:

mkdir -p /tmp/rpmnew cd /var/lib/rpm db_dump -p Packages | db_load /tmp/rpmnew/Packages

管道的前半段是从旧库逐条读出记录并转成文本格式,后半段是重新写成一个新的库文件。这个过程有个特点:如果旧库中间某条记录坏了,db_dump会在那个位置中断,后面的记录读不出来。所以管道跑完之后,一定要看看新库的体积是不是明显小于原库,如果是,说明丢了一部分包记录。把新库替换回去,再跑一次rpm --rebuilddb重建索引,能恢复多少算多少。

db_dump和db_load这两个工具在部分发行版上不是默认安装的,需要装libdb-utils或db-util这类软件包。临时找一台同版本机器拷贝二进制也可以,注意动态库依赖。

3.3 新后端的正确姿势:rpmdb 子命令与 sqlite 抢救

rpm 4.16 之后引入了独立的rpmdb命令行工具,做的事和rpm --rebuilddb一样,但语义更清晰,还多了导出导入的能力:

# 重建索引表 rpmdb --rebuilddb # 先导出全部记录,再初始化新库,再灌回去 rpmdb --exportdb > /root/rpmdb.export rpmdb --initdb rpmdb --importdb < /root/rpmdb.export

这一套流程的价值在于:导出文件是纯文本或准文本格式,可以人工检查、可以截断、可以只保留一部分。当库文件损坏到无法原地修复时,这是最可控的路径。

如果连rpmdb --exportdb都跑不起来,还有最后一道 sqlite 层面的抢救手段:

sqlite3 /var/lib/rpm/rpmdb.sqlite ".recover" > /tmp/recover.sql sqlite3 /tmp/rpmdb-fixed.sqlite < /tmp/recover.sql

.recover会尽量从损坏的文件里逐页扫描出可用的表和数据行,即使索引结构已毁,它也能把Packages表里的 blob 一条条捞出来。捞完之后把新文件放回原位,注意权限必须是644且属主为root:root,SELinux 环境下还要把上下文恢复成正确的类型,否则包管理器依然打不开。

注意:.recover需要 sqlite 3.29 及以上版本。老版本上只有.dump,遇到损坏文件时大概率会中途报错,能救出的数据量明显更少。

3.4 重建完成后的复核清单,别只看命令不报错

重建跑完不报错,很多人就收工了。我吃过这个亏——机器看起来正常,一周后某个依赖检查突然报错,查下去才发现重建时丢了两个关键包记录。所以现在固定跑一遍验证:

验证项命令期望结果
包总数rpm -qa | wc -l与备份中记录数一致,或差异可解释
核心包存在rpm -q rpm glibc kernel全部返回版本号,无 not installed
文件归属查询rpm -qf /bin/bash正确返回所属包
文件完整性rpm -V glibc无输出或仅有配置文件标记
依赖一致性dnf check无 broken dependency 提示
依赖解析rpm -q --whatrequires openssl-libs正常列出依赖方

这几条里,rpm -qf和rpm -q --whatrequires特别能说明问题,因为它们分别走的是文件索引和依赖索引。如果重建只重建了部分表,这两条命令会立刻暴露出来。dnf check则是从上层做一次全局依赖闭包核对,能看到更隐蔽的断裂。

4. 三代后端并存:先认版本,再选命令

4.1 用三条命令确认你手上是哪一代

网上教程互相打架的根本原因,是它们面向的 rpm 版本不一样。动手之前先把版本信息摸清楚:

rpm --version rpm --eval '%{_db_backend}' rpm --eval '%{_dbpath}' ls -l /var/lib/rpm

第一行给出 rpm 主版本号,第二行在支持该宏的版本上会返回bdb、ndb或sqlite,第三行告诉数据库路径是不是默认的/var/lib/rpm(有些系统为了容器场景改过)。第四行最直观:目录里如果有一堆散文件和__db.*,那就是 BDB 或 ndb 后端;如果只看到一个rpmdb.sqlite加-wal、-shm,那就是 sqlite。

rpm 版本区间常见后端典型文件形态推荐重建命令
4.11 及以前BDBPackages+ 多个索引 +__db.*rm -f __db.*后rpm --rebuilddb
4.12 至 4.15BDB 或 ndb同上,ndb 文件名有差异rpm --rebuilddb,失败则导出重灌
4.16sqlite(可切换)rpmdb.sqliterpmdb --rebuilddb
4.17 及以上sqliterpmdb.sqlite+-wal/-shmrpmdb --rebuilddb或rpm --rebuilddb

具体行为以你手上发行版的发布说明为准,别拿这张表当绝对标准,但用来判断"该走老路还是新路"够用了。

4.2 新后端最麻烦的场景:库彻底没了怎么办

老后端有个隐藏好处:Packages和其他索引文件是分开的,即使索引全毁,Packages还在,重建就有的救。新后端把所有东西塞进一个 sqlite 文件,这个文件彻底损坏且.recover也救不回来的时候,情况就比较棘手,因为系统里没有第二份"已安装包清单"的权威副本。

我处理过一例,最后的办法是从日志反推。/var/log/dnf.rpm.log里逐条记录了Installed:、Upgraded:、Removed:事件,把这些事件按时间顺序回放一遍,能还原出最终的包集合。写成脚本大概是这样:

grep -E 'Installed:|Upgraded:|Removed:' /var/log/dnf.rpm.log \ | sed -E 's/^[0-9T:.-]+ //' \ | awk '{print $1, $2}' \ | sort | uniq

得到候选清单之后人工核对,把已卸载的排除、把升级过的只留最终版本,然后rpmdb --initdb建一个空库,用dnf install把这批包重新装一遍。这个过程很笨,但比重装系统强,而且dnf会跳过已经存在于文件系统上且版本一致的包,实际耗时比想象中短。

另一条路是从同批次部署的机器上拷贝rpmdb.sqlite。前提是两台机器的包集合完全一致,只要有几十个包的差异,拷过来的库就会导致依赖查询给出错误答案,比没有库还危险。所以这条路我只在同一批自动化部署、配置完全对齐的机器之间用过,并且拷完立刻跑dnf check复核。

4.3 换后端不是随手能做的事

有人看到 sqlite 后端性能更好,就想着用RPMDB_BACKEND环境变量把老库切过去。这个操作的风险在于:转换过程如果中途失败,新旧两份库都可能处于不完整状态,而这时候你手上已经没有可回退的干净副本了。真要做,就在克隆出来的测试机上先跑一遍完整流程,确认转换后rpm -qa数量一致、dnf check干净、文件查询正常,再考虑上生产。而且转换后原来的Packages文件不会自动删除,部分工具可能还是会去读它,造成新旧数据打架,这一点要有心理准备。

5. 重建只是临时补救:找到反复损坏的根因

5.1 硬件与文件系统层,占了我遇到案例的一半以上

同一台机器半年内重建三次以上,基本可以断定不是软件层面的偶发问题。我排查的顺序一般是先看内核日志里有没有 IO 相关的异常:

dmesg -T | grep -i -E 'i/o error|medium error|ata[0-9]|nvme[0-9]|scsi'

出现medium error、unrecovered read error这类字样,说明磁盘该换了。接着看 SMART 信息,重点盯Reallocated_Sector_Ct、Pending_Sector_Ct、UDMA_CRC_Error_Count这几个,前两个非零就是明确的坏道信号。

文件系统层也要看。ext4 的日志回放如果没走完,/var/lib/rpm下的文件可能处在半写状态;XFS 的 log 损坏会导致部分元数据丢失。这种场景下用只读方式检查:

xfs_repair -n /dev/mapper/vg-var

或者 ext4 上跑e2fsck -n,-n表示只检查不修改,安全。如果检查出问题,就得进救援模式实际修复了。

虚拟化环境还有一个容易被忽略的点:宿主机异常断电时,客户机的写缓存可能没真正落盘,sqlite 的 WAL 文件处在不一致状态,下次启动就直接报损坏。这种场景光重建没用,得从虚拟化层配置上减少写缓存,或者给关键机器配 UPS。

5.2 并发写入与人为越界,是最隐蔽的一类

除了硬件,我自己遇到过两次"重建后一个月又坏",最后都追到了并发操作上。

第一次是定时任务和手动操作撞车。某台机器上配了每天凌晨的自动更新,运维同事白天手动装包时正好赶上一次dnf makecache的后台任务,两个进程都去碰 rpmdb,虽然 dnf 有锁机制,但有人为了"快点装完"直接kill -9了后台任务,锁没释放干净,后续操作就在半锁状态下把索引写花了。定位方法是查/var/log/dnf.log里那段时间的事件顺序,能看到两条记录的起始时间重叠,中间夹杂着一次非正常退出。

第二次更隐蔽,是一套备份恢复流程造成的。备份工具把/var/lib/rpm拍进了快照,某次误操作恢复了这个快照,但文件系统上的实际包已经被升级过,结果库里的记录和磁盘上的文件对不上,rpm -V报出一大片"文件缺失",看着像数据库坏了,实际上是数据和现实脱节。这种问题重建解决不了,只能靠重装相关包把人造差异抹平。

还有一类在容器场景里出现得多:把宿主机的/var/lib/rpm通过绑定挂载塞进多个容器,几个容器各自跑包管理命令。这种情况几乎必然出事,因为 rpmdb 本身不支持多进程跨命名空间并发写。正确的做法是每个容器有自己的库,宿主机的库只读挂载或者干脆不挂。

5.3 建立基线,别等它坏了才发现

反复坏的机器,根本问题是缺少早期预警。我现在给所有长期运行的机器加了两条最基础的监控:一是每天定时记录rpm -qa | wc -l的输出到日志,突变超过阈值就告警;二是监控/var/lib/rpm目录的总大小,如果某天突然缩小了一半,说明有东西被删了或者索引大面积失效。这两个指标实现成本极低,但能提前几天发现问题。

另外,凡是要做批量包操作、系统升级、磁盘扩容这类可能影响/var/lib的变更,我都在变更脚本开头加一行自动备份 rpmdb:

tar -C /var/lib -czf /backup/rpmdb-$(date +%F-%H%M).tgz rpm 2>/dev/null

几百 MB 的空间换来的是一份随时能回退的快照,这笔账怎么算都划算。

6. 几条手册上不写的实操体会

重建大库是个慢活,这点要有心理预期。一台装了一千多个包的机器,rpmdb --rebuilddb跑三五分钟很正常,包多的跑十几分钟也不奇怪,期间没有输出,看着像卡死但其实是内存里在逐条解析 header。别中途 Ctrl+C 打断,打断之后库的状态比之前更糟。要观察进度可以加-v,或者另开一个终端看/proc/<pid>/io的读取量有没有在涨。

重建期间的内存占用别低估。header 解析是内存密集型的,小内存机器上重建大库有触发 OOM 的风险,我见过一台 1GB 内存的测试机在重建时把 sshd 都挤掉了。目标机器内存小于 2GB 的话,考虑把rpmdb.sqlite拷到另一台机器上,用--dbpath指向拷贝过来的目录完成重建,再把结果搬回去,这样对原机的冲击最小。

备份恢复之后别忘 SELinux 上下文。用tar打包再解开的文件,属主和权限通常没问题,但上下文可能带上user_tmp_t之类的不正确标签,导致包管理器打不开。解压后跑一次restorecon -Rv /var/lib/rpm就好。我因为漏了这一步多排查了半个多小时,报错信息指向权限,但ls -l看权限完全正常,只有ls -Z才能看出端倪。

最后,任何一次重建之后,都建议立刻做一次全量配置留存,把rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort的输出存成文件扔进版本库。下次再出问题,这份清单就是你判断"丢了哪些包"的黄金基准。踩过几次之后我现在的固定动作是:变更前备份 rpmdb,变更后导出包清单,两条都做完才敢关终端。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询