暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

PITR玩砸了,看DBA如何删库跑路

原创 NickYoung 2025-08-04
323

问题现象

使用备份集恢复实例后无法登录业务db,报错如下:

postgres=# \c test0703 connection to server on socket "/tmp/.s.PGSQL.5403" failed: FATAL: cannot connect to invalid database "test0703" HINT: Use DROP DATABASE to drop invalid databases. Previous connection kept postgres=#

报错很明确,test0703是一个invalid database无法登录,建议我们删除该库。
我的目的就是使用备份集恢复这个数据库,结果恢复出一个无效数据库?命中BUG?

报错分析

不要慌,先看下为什么报错。
报错位于InitPostgres函数,由于database_is_invalid_form(datform)为true进入了报错逻辑。

void InitPostgres(const char *in_dbname, Oid dboid, const char *username, Oid useroid, bits32 flags, char *out_dbname) { /* 省略 */ if (database_is_invalid_form(datform)) { ereport(FATAL, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot connect to invalid database \"%s\"", dbname), errhint("Use DROP DATABASE to drop invalid databases.")); } /* 省略 */ }

也就是说由于pg_database中查询到该db的datconnlimit为DATCONNLIMIT_INVALID_DB即-2,则登录报错。

#define DATCONNLIMIT_INVALID_DB -2 bool database_is_invalid_form(Form_pg_database datform) { return datform->datconnlimit == DATCONNLIMIT_INVALID_DB; }

查看pg_database确实如此。所以PG中定义datconnlimit为-2就是invalid database无法登录。

postgres=# select datname,datconnlimit from pg_database where datname='test0703'; datname | datconnlimit ----------+-------------- test0703 | -2 (1 row) postgres=#

那么为什么datconnlimit会等于-2?

代码走读

从源代码得知只有在drop database的过程中才会设置datconnlimit为-2

把drop database的主要过程整理为流程图方便大家理解,当然这不是完整的流程,只是把我认为关键的部分呈现出来。
d48c823a300f3fb4ebea18cc77d1ec00.png

我们都知道drop database是一个不可逆的操作[1] ,理论上要么不删除,要么删除成功。

在exec_simple_query中start_xact_command隐式开启事务,看到这你可能会质疑,不是说不可逆吗,怎么还会放到事务中?别着急,继续往下看。

由于drop database是一个Utility statement,因此不会生成执行计划,是直接调用到对应的逻辑里执行的。流程图中绿色部分从dropdb函数开始就是drop database语句的核心逻辑。

step1:
在dropdb函数里,首先做了一些ACLcheck及login校验后对pg_database加RowExclusiveLock

step2:
使用inplace_update去修改当前 drop database的datconnlimit这个过程稍微详细的描述下。

systable_inplace_update_begin里对我们要修改的这条记录使用heap_inplace_lock即调用Lock_buffer给当前buffer_content加LWlock LW_EXCLUSIVE模式锁,这样其他进程都无法访问这条内容。
然后执行修改datform->datconnlimit = !!#ff0000 DATCONNLIMIT_INVALID_DB!!;
改完之后systable_inplace_update_finish释放锁LWlock释放,这样修改就“生效”了。什么?生效了?不是在事务里?事务还没提交,怎么生效的?

大家应该注意到了这里是inplace update,而并不是PG默认的多版本标记更新,默认的标记更新只有等当前事务提交,才对外可见。为什么这么设计呢?

我们先串完整个过程,后续会有答案。

然后进行XLogFlush把WAL及时刷下去。

step3:
CatalogTupleDelete删除pg_database当前database这条记录,这里是标准的heap_delete,目前在事务里,得等到commit后才“生效”。

所以事务其实保护的就是这里,我们某个database可不可见主要取决于pg_database里对应的记录可不可见。那么这部分是可逆的,在这个步骤事务提交就这条记录清理,事务回滚这条记录依然可见。

step4:
ok,下来就到了不可逆的操作了,DropDatabaseBuffers丢弃这个database对应修改过的buffer内容,并且触发一次checkpoint。

remove_dbtablespaces调用unlink删除该database目录下物理文件。

处理完毕table_close释放pg_database加的锁。

step5:
finish_xact_command提交事务,操作完毕,这个时候要drop 的database在pg_database中记录就看不到了。

流程走读完毕,那么看起来就是drop database具体的流程中有部分可逆的步骤,有大部分不可逆的步骤。其实也好理解,当一个操作有一个不可逆的步骤,那么这个操做本身就是不可逆的,这些不可逆的其实也就是无法支持事务的。

再回到drop database datname这个语句本身,执行结果就只有两种,要么删除了,要么不删除,我们肯定不接受删除了一半,或者删除了部分数据这种中间状态,这种情况下database也是无法使用的。

所以在一开始执行drop database,就使用了inplace update将database修改为invalid禁止登录。即便你在drop database中间状态备份了数据,使用这个备份恢复数据,或者访问数据库都无法访问。似乎是保证了drop database的“原子性”。

所以回到这个case,不是什么BUG,是PITR选择的时间点不对,应该再往前,选用更早备份结合增量WAL重新恢复。当前恢复的时间点,这个database正在被删除。

如果我的描述不太清晰,可以参考源代码[2] ,篇幅问题,这里我只粘贴一部分注释吧。

    /*
	 * Except for the deletion of the catalog row, subsequent actions are not
	 * transactional (consider DropDatabaseBuffers() discarding modified
	 * buffers). But we might crash or get interrupted below. To prevent
	 * accesses to a database with invalid contents, mark the database as
	 * invalid using an in-place update.
	 *
	 * We need to flush the WAL before continuing, to guarantee the
	 * modification is durable before performing irreversible filesystem
	 * operations.
	 */

代码跟踪

好人做到底,把debug关键过程截图下,不用麻烦大家自行验证了,有兴趣的也可以自行验证(切勿操作生产库,搞出异常和本文无关,博主表示接不住)

修改之前datconnlimit是-1,修改后为-2
c1da0c7061cb03b26ef9c9f4a19b9441.png

其他会话查询也是一致。
87a800cfa3073626922c8a21cef5ce6a.png

执行CatalogTupleDelete删除这条信息时,由于还在事务中,其他会话还可以查询到这条记录。
813b1d6a44108d8145b3ddc610c176b5.png
a7fb933f4bbd707c1506d49d20e1fd66.png

当finish_xact_command提交事务后,其他会话也就查不到这条记录了。
8d1af48dff2ced7853df06978488feca.png
0a44bb12c17b907df6e76715e7be9823.png

结论

这不是什么BUG,是为了保证drop database动作的“原子性”,所以当PITR恢复的实例datconnlimit为-2时,说明恢复的时间点不正确,当前database正在被删除,需要选用更早备份结合增量WAL重新恢复。

当然datconnlimit可以从-2改为-1,直接修改pg_database即可(allow_alter_system必须为on,17版本default为on),但我不建议这么做,因为这样操作恢复的数据是不可靠的。

Reference:

[1] https://www.postgresql.org/docs/17/sql-dropdatabase.html
[2] https://github.com/postgres/postgres/blob/REL_17_STABLE/src/backend/commands/dbcommands.c#L1634C20-L1634C26

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论