如何损坏PostgreSQL数据库

当然,大多数人不想破坏他们的数据库。这些人将从避免本文中使用的技术中获益。但对于某些人来说,损坏数据库可能很有用,例如,如果您想测试将用于检测或修复数据损坏的工具或过程。

先决条件

我们需要一个包含一些数据的数据库,对于我们的一些实验,我们需要一些正在进行的活动。为此,我们可以使用内置的PostgreSQL基准pgbench。我们使用比例因子100,因此最大的表包含1000万行:

$ pgbench -q -i -s 100
dropping old tables...
creating tables...
generating data (client-side)...
10000000 of 10000000 tuples (100%) done (elapsed 7.44 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 10.12 s (drop tables 0.18 s, create tables 0.01 s, client-side generate 7.52 s, vacuum 0.14 s, primary keys 2.28 s).

负载将通过5个并发客户端会话生成:

$ pgbench -c 5 -T 3600

通过设置fsync=off创建损坏的数据库

让我们在postgresql中设置fsync=off。conf并在服务器负载时关闭其电源。

经过几次尝试,我们可以使用amcheck扩展检测数据损坏:

postgres=# CREATE EXTENSION amcheck;
CREATE EXTENSION
postgres=# SELECT bt_index_parent_check('pgbench_accounts_pkey', TRUE, TRUE);
WARNING:  concurrent delete in progress within table "pgbench_accounts"
ERROR:  could not access status of transaction 1949706
DETAIL:  Could not read from file "pg_subtrans/001D" at offset 196608: read too few bytes.
CONTEXT:  while checking uniqueness of tuple (131074,45) in relation "pgbench_accounts"

怎么搞的?数据不再以正确的顺序刷新到磁盘,因此数据修改可以在WAL之前到达磁盘。这会导致崩溃恢复期间的数据损坏。

从备份创建损坏的数据库

当pgbench运行时,我们创建一个基本备份:

$ psql
postgres=# SELECT pg_backup_start('test');
 pg_backup_start 
═════════════════
 1/47F8A130
(1 row)

请注意,因为我使用的是PostgreSQL v15,所以启动备份模式的函数是pg_backup_start(),而不是pg_ start_back()。这是因为自PostgreSQL 9.6以来一直被弃用的专用备份API最终在v15中被删除。要了解更多信息,请阅读我在链接中更新的帖子。

让我们计算出数据库的对象ID和pgbench_accounts的主键索引:

postgres=# SELECT relfilenode FROM pg_class
           WHERE relname = 'pgbench_accounts_pkey';
 relfilenode 
═════════════
       16430
(1 row)
 
postgres=# SELECT oid FROM pg_database
           WHERE datname = 'postgres';
 oid 
═════
   5
(1 row)

我们通过复制数据目录创建备份。之后,我们再次复制pgbench_accounts的主键索引和提交日志,以确保它们比其余的更新:

$ cp -r data backup
$ cp data/base/5/16430* backup/base/5
$ cp data/pg_xact/* backup/pg_xact/
$ rm backup/postmaster.pid

关键部分:不要创建backup_label

现在我们退出备份模式,但忽略从pg_backup_stop()返回的backup_ label文件的内容:

postgres=# SELECT labelfile FROM pg_backup_stop();
NOTICE:  WAL archiving is not enabled; you must ensure that all required WAL segments are copied through other means to complete the backup
                           labelfile                            
════════════════════════════════════════════════════════════════
 START WAL LOCATION: 1/47F8A130 (file 000000010000000100000047)↵
 CHECKPOINT LOCATION: 1/65CD24F0                               ↵
 BACKUP METHOD: streamed                                       ↵
 BACKUP FROM: primary                                          ↵
 START TIME: 2022-07-05 08:32:47 CEST                          ↵
 LABEL: test                                                   ↵
 START TIMELINE: 1                                             ↵
  
(1 row)

然后,让我们确保控制文件中的最后一个检查点不同:

$ pg_controldata -D backup | grep REDO
Latest checkpoint's REDO location:    1/890077D0
Latest checkpoint's REDO WAL file:    000000010000000100000089

太棒了,让我们启动服务器:

$ echo 'port = 5555' >> backup/postgresql.auto.conf
$ pg_ctl -D backup start
waiting for server to start..... done
server started

现在,对pgbench_accounts的索引扫描失败,因为索引包含比表更新的数据:

postgres=# SELECT * FROM pgbench_accounts ORDER BY aid;
ERROR:  could not read block 166818 in file "base/5/16422.1": read only 0 of 8192 bytes

通过从备份中省略backup_label文件,我们从错误的检查点恢复,因此表中的数据及其索引不再一致。请注意,我们可以在没有pg_backup_start()和pg_ backup_ stop()的情况下获得相同的效果,我只想强调backupUlabel的重要性。

使用pg_resetwal创建损坏的数据库

当数据库从pgbench加载时,我们使用

pg_ctl stop -m immediate -D data

然后我们运行pg_resetwal:

pg_resetwal -D data
The database server was not shut down cleanly.
Resetting the write-ahead log might cause data to be lost.
If you want to proceed anyway, use -f to force reset.
$ pg_resetwal -f -D data
Write-ahead log reset

然后,我们启动服务器,像之前一样使用amcheck检查索引的完整性:

postgres=# CREATE EXTENSION amcheck;
CREATE EXTENSION
postgres=# SELECT bt_index_parent_check('pgbench_accounts_pkey', TRUE, TRUE);
WARNING:  concurrent delete in progress within table "pgbench_accounts"
ERROR:  could not access status of transaction 51959
DETAIL:  Could not read from file "pg_subtrans/0000" at offset 204800: read too few bytes.
CONTEXT:  while checking uniqueness of tuple (1,1) in relation "pgbench_accounts"

pg_resetwal只能在完全关闭的集群上安全使用。选项-f旨在作为最后一搏,让损坏的服务器启动并挽救一些数据。只有专家才能使用它。

使用pg_upgrade–link创建损坏的数据库

我们使用initdb创建第二个集群:

$ initdb -E UTF8 --locale=C -U postgres data2

然后我们编辑postgresql。conf并选择其他端口号。关闭原始集群后,我们以链接模式运行“升级”:

$ pg_upgrade -d /home/laurenz/data -D /home/laurenz/data2 \
> -b /usr/pgsql-15/bin -B /usr/pgsql-15/bin -U postgres --link
Performing Consistency Checks
...
Performing Upgrade
...
Adding ".old" suffix to old global/pg_control               ok
 
If you want to start the old cluster, you will need to remove
the ".old" suffix from /home/laurenz/data/global/pg_control.old.
Because "link" mode was used, the old cluster cannot be safely
started once the new cluster has been started.
...
Upgrade Complete
----------------
Optimizer statistics are not transferred by pg_upgrade.
Once you start the new server, consider running:
    /usr/pgsql-15/bin/vacuumdb -U postgres --all --analyze-in-stages
 
Running this script will delete the old cluster's data files:
    ./delete_old_cluster.sh

pg_upgrade重命名了旧集群的控制文件,因此无法意外启动。我们将撤销:

mv /home/laurenz/data/global/pg_control.old \
>  /home/laurenz/data/global/pg_control

现在我们可以启动两个集群,并在两个集群上运行pgbench。很快,我们将看到如下错误消息

ERROR:  unexpected data beyond EOF in block 1 of relation base/5/16397
HINT:  This has been seen to occur with buggy kernels; consider updating your system.
 
ERROR:  duplicate key value violates unique constraint "pgbench_accounts_pkey"
DETAIL:  Key (aid)=(8040446) already exists.
 
WARNING:  could not write block 13 of base/5/16404
DETAIL:  Multiple failures --- write error might be permanent.
 
ERROR:  xlog flush request 0/98AEE3E0 is not satisfied --- flushed only to 0/648CDC58
CONTEXT:  writing block 13 of relation base/5/16404
 
ERROR:  could not access status of transaction 39798
DETAIL:  Could not read from file "pg_subtrans/0000" at offset 155648: read too few bytes.

由于两个集群共享相同的数据文件,因此我们成功地在相同数据文件上启动了两台服务器。这会导致数据损坏。

通过操纵数据文件创建损坏的数据库

为此,我们计算出属于表pgbench_accounts的文件名:

postgres=# SELECT relfilenode FROM pg_class
           WHERE relname = 'pgbench_accounts';
 relfilenode 
═════════════
       16396
(1 row)

现在我们停止服务器并将一些垃圾写入第一个数据块:

yes 'this is garbage' | dd of=data/base/5/16396 bs=1024 seek=2 count=1 conv=notrunc
0+1 records in
0+1 records out
1024 bytes (1.0 kB, 1.0 KiB) copied, 0.00031255 s, 3.3 MB/s

然后我们启动服务器并尝试从表中选择:

postgres=# TABLE pgbench_accounts ;
ERROR:  compressed pglz data is corrupt

我们篡改了数据文件,因此表被损坏也就不足为奇了。

创建带有目录修改的损坏数据库

谁需要ALTER TABLE来删除表列?我们可以简单地运行

DELETE FROM pg_attribute
WHERE attrelid = 'pgbench_accounts'::regclass
  AND attname = 'bid';

之后,尝试查询表将导致错误:

ERROR:  pg_attribute catalog is missing 1 attribute(s) for relation OID 16396

我们忽略了在pg_attribute中删除一列会将attisDroped设置为TRUE,而不是实际删除该条目。此外,我们没有检查pg_depend中的依赖关系,也没有正确地锁定表以防止并发访问。不支持修改目录表,如果它破坏了数据库,则可以保留这两部分。

结论

我们已经看到了许多破坏PostgreSQL数据库的方法。其中有些是显而易见的,有些可能会让初学者感到惊讶。如果您不希望数据库损坏,

  • 不要弄乱系统目录

  • 切勿修改数据目录中的任何内容(配置文件除外)

  • 不要在fsync=off的情况下运行

  • 不要在崩溃的服务器上调用pg_resetwal-f

  • 使用pg_upgrade–link升级后删除旧集群

  • 不要删除或省略backup_label

  • 运行受支持的PostgreSQL版本,以避免已知的软件错误

  • 在可靠的硬件上运行

我希望你能用这些信息保存一些数据库!如果您想了解有关PostgreSQL性能故障排除的更多信息,请阅读我关于加入策略的文章。

作者相关

原文标题:HOW TO CORRUPT YOUR POSTGRESQL DATABASE
原文作者:Laurenz Albe
原文链接:https://www.cybertec-postgresql.com/en/how-to-corrupt-your-postgresql-database/


免责声明:

1、本站资源由自动抓取工具收集整理于网络。

2、本站不承担由于内容的合法性及真实性所引起的一切争议和法律责任。

3、电子书、小说等仅供网友预览使用,书籍版权归作者或出版社所有。

4、如作者、出版社认为资源涉及侵权,请联系本站,本站将在收到通知书后尽快删除您认为侵权的作品。

5、如果您喜欢本资源,请您支持作者,购买正版内容。

6、资源失效,请下方留言,欢迎分享资源链接

文章评论

0条评论