资讯详情

RPM数据库损坏修复:真伪判断、重建流程与根因排查

📅 2026/10/25 2:47:59 | 华诺云谱 👁 阅读
RPM数据库损坏修复:真伪判断、重建流程与根因排查
凌晨两点多收到告警值班同事连上去一看整台机器上跟包管理沾边的命令全瘫了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 blobName、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:rootSELinux 环境下还要把上下文恢复成正确的类型否则包管理器依然打不开。注意.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 --rebuilddb4.12 至 4.15BDB 或 ndb同上ndb 文件名有差异rpm --rebuilddb失败则导出重灌4.16sqlite可切换rpmdb.sqliterpmdb --rebuilddb4.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的变更我都在变更脚本开头加一行自动备份 rpmdbtar -C /var/lib -czf /backup/rpmdb-$(date %F-%H%M).tgz rpm 2/dev/null几百 MB 的空间换来的是一份随时能回退的快照这笔账怎么算都划算。6. 几条手册上不写的实操体会重建大库是个慢活这点要有心理预期。一台装了一千多个包的机器rpmdb --rebuilddb跑三五分钟很正常包多的跑十几分钟也不奇怪期间没有输出看着像卡死但其实是内存里在逐条解析 header。别中途 CtrlC 打断打断之后库的状态比之前更糟。要观察进度可以加-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变更后导出包清单两条都做完才敢关终端。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑