当然,大多数人不想破坏他们的数据库。这些人将从避免本文中使用的技术中获益。但对于某些人来说,损坏数据库可能很有用,例如,如果您想测试将用于检测或修复数据损坏的工具或过程。
先决条件
我们需要一个包含一些数据的数据库,对于我们的一些实验,我们需要一些正在进行的活动。为此,我们可以使用内置的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性能故障排除的更多信息,请阅读我关于加入策略的文章。
作者相关
Laurenz Albe是CYBERTEC的高级顾问和支持工程师。自2006年以来,他一直与PostgreSQL合作并为其做出贡献。
原文标题:HOW TO CORRUPT YOUR POSTGRESQL DATABASE
原文作者:Laurenz Albe
原文链接:https://www.cybertec-postgresql.com/en/how-to-corrupt-your-postgresql-database/