首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PITR玩砸了,看DBA如何删库跑路

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

作者头像
NickYoung
发布2026-07-09 16:24:18
发布2026-07-09 16:24:18
80
举报

你真的了解删库的正确姿势吗?DBA删库后,PITR失败准备跑路,且看老杨如何挽救DBA小姐姐

DBA小姐姐着急忙慌地找到老杨说需要紧急救火,误删了一个库,而且已经尝试PITR恢复了,但恢复的数据有问题,DB无法访问。并且强调还是相对重要的数据,恢复不了的话,可能不是跑路那么简单。


1 问题现象

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

代码语言:javascript
复制
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?买机票开溜?

2 报错分析

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

代码语言:javascript
复制
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,则登录报错。

代码语言:javascript
复制
#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无法登录。

代码语言:javascript
复制
postgres=# select datname,datconnlimit from pg_database where datname='test0703';
 datname  | datconnlimit 
----------+--------------
 test0703 |           -2
(1 row)

postgres=#

为什么datconnlimit会等于-2?

3 代码走读

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

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

我们都知道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 = 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] ,篇幅问题,这里我只粘贴一部分注释吧。

代码语言:javascript
复制
 /*
  * 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.
  */

4 代码跟踪

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

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

其他会话查询也是一致。

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

当finish_xact_command提交事务后,其他会话也就查不到这条记录了。

5 小结

这不是什么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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PostgreSQL运维之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 问题现象
  • 2 报错分析
  • 3 代码走读
  • 4 代码跟踪
  • 5 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档