我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

我想与大家分享我第一次将 Postgres 数据库恢复到完整功能的成功经验。 半年前我开始接触Postgres DBMS,在此之前我完全没有数据库管理的经验。

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

我在一家大型 IT 公司担任半 DevOps 工程师。 我们公司开发高负载服务的软件,我负责性能、维护和部署。 我收到了一项标准任务:更新一台服务器上的应用程序。 该应用程序是用 Django 编写的,在执行更新迁移期间(数据库结构的更改),在此过程之前,我们通过标准 pg_dump 程序进行完整的数据库转储,以防万一。

进行转储时发生意外错误(Postgres 版本 9.5):

pg_dump: Oumping the contents of table “ws_log_smevlog” failed: PQgetResult() failed.
pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989
pg_dump: The command was: COPY public.ws_log_smevlog [...]
pg_dunp: [parallel archtver] a worker process dled unexpectedly

问题 “块中的页面无效” 谈到文件系统级别的问题,这是非常糟糕的。 在各种论坛上建议这样做 全真空 有选项 零损坏页数 来解决这个问题。 好吧,让我们尝试一下...

准备恢复

警告! 在尝试恢复数据库之前,请务必先进行 Postgres 备份。 如果您有虚拟机,请停止数据库并拍摄快照。 如果无法拍摄快照,请停止数据库并将 Postgres 目录的内容(包括 wal 文件)复制到安全位置。 我们业务的主要任务不是让事情变得更糟。 读 .

由于数据库通常适合我,因此我将自己限制为常规数据库转储,但排除了数据损坏的表(选项 -T,--排除表=表 在 pg_dump 中)。

服务器是物理的,不可能拍摄快照。 备份已删除,让我们继续。

文件系统检查

在尝试恢复数据库之前,我们需要确保文件系统本身一切正常。 如果出现错误,请纠正它们,因为否则只会让事情变得更糟。

就我而言,带有数据库的文件系统安装在 “/srv” 并且类型是ext4。

停止数据库: systemctl 停止 [电子邮件保护] 并检查文件系统是否未被任何人使用并且可以使用命令卸载 :
lsof +D /srv

我还必须停止 redis 数据库,因为它也在使用 “/srv”。 接下来我卸载了 / srv (卸载)。

使用实用程序检查文件系统 e2fsck 使用开关 -f (即使文件系统被标记为干净也强制检查):

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

接下来,使用该实用程序 转储2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep 检查)您可以验证检查是否实际执行:

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

e2fsck 表示在 ext4 文件系统级别没有发现问题,这意味着您可以继续尝试恢复数据库,或者更确切地说返回 真空满 (当然,需要重新挂载文件系统并启动数据库)。

如果您有物理服务器,请务必检查磁盘的状态(通过 smartctl -a /dev/XXX)或 RAID 控制器,以确保问题不在硬件级别。 就我而言,RAID 原来是“硬件”,所以我要求本地管理员检查 RAID 的状态(服务器距离我几百公里)。 他说没有任何错误,这意味着我们肯定可以开始修复。

尝试 1:zero_damaging_pages

我们使用具有超级用户权限的帐户通过 psql 连接到数据库。 我们需要一个超级用户,因为...... 选项 零损坏页数 只有他可以改变。 就我而言,它是 postgres:

psql -h 127.0.0.1 -U postgres -s [数据库名称]

选项 零损坏页数 需要忽略读取错误(来自 postgrespro 网站):

当 PostgreSQL 检测到损坏的页头时,它通常会报告错误并中止当前事务。 如果启用了zero_damaging_pages,系统会发出警告,将内存中损坏的页面清零,然后继续处理。 此行为会破坏数据,即损坏页中的所有行。

我们启用该选项并尝试对表进行完全清理:

VACUUM FULL VERBOSE

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)
不幸的是,运气不好。

我们遇到了类似的错误:

INFO: vacuuming "“public.ws_log_smevlog”
WARNING: invalid page in block 4123007 of relation base/16400/21396989; zeroing out page
ERROR: unexpected chunk number 573 (expected 565) for toast value 21648541 in pg_toast_106070

pg_toast – 一种在 Poetgres 中存储“长数据”的机制,如果它不适合一页(默认为 8kb)。

尝试 2:重新索引

谷歌的第一个建议没有帮助。 经过几分钟的搜索,我找到了第二个技巧 - 制作 重新索引 损坏的桌子。 我在很多地方看到过这样的建议,但并没有激发信心。 让我们重新索引:

reindex table ws_log_smevlog

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

重新索引 毫无问题地完成。

然而,这并没有帮助, 真空满 因类似错误而崩溃。 由于我习惯了失败,所以我开始在互联网上进一步寻找建议,并发现了一个相当有趣的方法 文章.

尝试 3:选择、限制、偏移

上面的文章建议逐行查看表格并删除有问题的数据。 首先我们需要查看所有行:

for ((i=0; i<"Number_of_rows_in_nodes"; i++ )); do psql -U "Username" "Database Name" -c "SELECT * FROM nodes LIMIT 1 offset $i" >/dev/null || echo $i; done

就我而言,该表包含 1 628 991 线! 需要好好照顾 数据分区,但这是一个单独讨论的主题。 那是星期六,我在 tmux 中运行了这个命令然后上床睡觉:

for ((i=0; i<1628991; i++ )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog LIMIT 1 offset $i" >/dev/null || echo $i; done

到了早上,我决定检查一下事情进展如何。 让我惊讶的是,我发现20个小时后,只有2%的数据被扫描了! 我不想等50天。 又一个彻底的失败。

但我没有放弃。 我想知道为什么扫描花了这么长时间。 从文档(再次在 postgrespro 上)我发现:

OFFSET 指定在开始输出行之前跳过指定的行数。
如果同时指定了 OFFSET 和 LIMIT,系统首先跳过 OFFSET 行,然后开始计算 LIMIT 约束的行数。

使用 LIMIT 时,还必须使用 ORDER BY 子句,以便按特定顺序返回结果行。 否则,将返回不可预测的行子集。

显然,上面的命令是错误的:首先,没有 订购,结果可能是错误的。 其次,Postgres 首先必须扫描并跳过 OFFSET 行,并且随着增加 OFFSET 生产力将进一步下降。

尝试 4:以文本形式转储

然后我想到了一个看似绝妙的主意:以文本形式转储并分析最后记录的行。

但首先,让我们看一下表的结构。 ws_log_smevlog:

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

在我们的例子中,我们有一个专栏 “ID”,其中包含行的唯一标识符(计数器)。 计划是这样的:

  1. 我们开始以文本形式进行转储(以sql命令的形式)
  2. 在某个时间点,转储会因错误而中断,但文本文件仍会保存在磁盘上
  3. 我们查看文本文件的末尾,从而找到成功删除的最后一行的标识符(id)

我开始以文本形式转储:

pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dump

正如预期的那样,转储因相同的错误而中断:

pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989

进一步通过 尾巴 我看着转储的尽头(尾部-5./my_dump.dump)发现dump在id为的线路上被中断 186 525。 “所以问题出在id 186 526的行上,它坏了,需要删除!” - 我想。 但是,对数据库进行查询:
«从 ws_log_smevlog 选择 *,其中 id=186529“事实证明,这条线一切都很好......索引为 186 - 530 的行也可以正常工作。 另一个“绝妙想法”失败了。 后来我明白了为什么会发生这种情况:当从表中删除和更改数据时,它们并没有被物理删除,而是被标记为“死元组”,然后出现 自动真空 并将这些行标记为已删除并允许重复使用这些行。 要理解,如果表中的数据发生变化并且启用了autovacuum,那么它就不是顺序存储的。

尝试 5:SELECT、FROM、WHERE id=

失败让我们更坚强。 你不应该放弃,你需要坚持到底并相信自己和自己的能力。 所以我决定尝试另一种选择:逐条查看数据库中的所有记录。 了解我的表的结构(见上文),我们有一个唯一的 id 字段(主键)。 表中有 1 行, id 是按顺序排列的,这意味着我们可以一一浏览它们:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

如果有人不明白,该命令的工作原理如下:它逐行扫描表并将标准输出发送到 的/ dev / null的,但如果 SELECT 命令失败,则会打印错误文本(stderr 发送到控制台)并打印包含错误的行(感谢 ||,这意味着 select 有问题(命令的返回码不为 0))。

我很幸运,我在现场创建了索引 id:

我第一次在失败后恢复 Postgres 数据库的经历(relatton base/4123007 的块 16490 中的无效页面)

这意味着找到具有所需 id 的行不会花费太多时间。 理论上它应该有效。 好吧,让我们运行命令 TMUX 我们去睡觉吧。

到早上,我发现大约有 90 个条目被浏览,略高于 000%。 与之前的方法 (5%) 相比,结果非常好! 但我不想等2天...

尝试 6:SELECT、FROM、WHERE id >= 和 id

客户拥有专用于数据库的优秀服务器:双处理器 英特尔至强E5-2697 v2,我们所在的位置有多达 48 个线程! 服务器上的负载是平均的;我们可以毫无问题地下载大约 20 个线程。 RAM 也足够:高达 384 GB!

因此,该命令需要并行化:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

在这里可以编写一个漂亮而优雅的脚本,但我选择了最快的并行化方法:手动将范围 0-1628991 分割为 100 条记录的间隔,并分别运行以下形式的 000 个命令:

for ((i=N; i<M; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

但这还不是全部。 理论上,连接数据库也需要一些时间和系统资源。 您会同意,连接 1 并不是很明智。 因此,让我们检索 628 行而不是一对一连接。 结果,团队变成了这样:

for ((i=N; i<M; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

在 tmux 会话中打开 16 个窗口并运行命令:

1) for ((i=0; i<100000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
2) for ((i=100000; i<200000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
…
15) for ((i=1400000; i<1500000; i=$((i+1000)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
16) for ((i=1500000; i<1628991; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

一天后,我收到了第一个结果! 即(不再保留值XXX和ZZZ):

ERROR:  missing chunk number 0 for toast value 37837571 in pg_toast_106070
829000
ERROR:  missing chunk number 0 for toast value XXX in pg_toast_106070
829000
ERROR:  missing chunk number 0 for toast value ZZZ in pg_toast_106070
146000

这意味着三行包含错误。 第一条和第二条问题记录的id在829到000之间,第三条问题记录的id在830到000之间。接下来,我们只需找到问题记录的准确id值即可。 为此,我们以步骤 146 的方式查看有问题记录的范围并识别 ID:

for ((i=829000; i<830000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number 2 (expected 0) for toast value 37837843 in pg_toast_106070
829449
for ((i=146000; i<147000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number ZZZ (expected 0) for toast value XXX in pg_toast_106070
146911

美好的结局

我们发现了有问题的线路。 我们通过 psql 进入数据库并尝试删除它们:

my_database=# delete from ws_log_smevlog where id=829417;
DELETE 1
my_database=# delete from ws_log_smevlog where id=829449;
DELETE 1
my_database=# delete from ws_log_smevlog where id=146911;
DELETE 1

令我惊讶的是,即使没有选项,条目也被毫无问题地删除了 零损坏页数.

然后我连接到数据库,做了 真空满 (我认为没有必要这样做),最后我使用成功删除了备份 pg_dump。 转储已完成,没有任何错误! 问题就这样被解决了。 喜悦之情溢于言表,在经历了如此多的失败之后,我们终于找到了解决方案!

致谢和结论

这就是我第一次恢复真正的 Postgres 数据库的经历。 我会记住这段经历很长一段时间。

最后,我要感谢 PostgresPro 将文档翻译成俄语并感谢 完全免费的在线课程,这对分析问题有很大帮助。

来源: habr.com

添加评论