PostgreSQL troubleshooting系列之八-处理硬软件故障
前言
本文的内容来源于电子书《Troubleshooting PostgreSQL》。那本书虽老,但是里边的内容依然很有参考价值。俺这里尝试对其简译,对某些地方进行修正和补充,以符合近年来新版本的要求。希望对学习和深入了解使用PG有帮助。
本文来自原书第9章:处理硬软件故障
本章将介绍与硬件和软件故障相关的问题。即使是昂贵的硬件也远非完美,它可能会不时出现故障。故障可能以内存故障、硬盘驱动器损坏、文件系统损坏、内核相关问题等形式出现。在本章中,将讨论一些最常见的问题。
校验和 - 避免隐性损坏
清除损坏的页面
处理坏掉的索引
转储单页数据
重置事务日志
断电相关问题
9、处理硬软件故障
9.1、校验和 - 避免隐性损坏
通常,在阅读有关高可用性和容错的书籍时,我总是有这样的印象:大多数系统都假定崩溃是原子性的。这是什么意思呢?假设有两台服务器;一个服务器崩溃了,另一个服务器接管了。不幸的是,崩溃在很多情况下都不是这样的。在现实世界中,崩溃通常远非原子性的。麻烦可能会逐渐累积,直到事情变得非常糟糕。考虑内存损坏; 当内存出错时,它可能不会立即导致崩溃,即使发生这种情况,系统也可能在故障再次出现之前重新启动而不会出现问题。
在很多情况下,一个默默积累的问题比一个简单的崩溃要危险得多。在隐性故障的情况下,主要的危险在于问题可能会在没有人注意到的情况下逐渐在系统中蔓延。
为了防止由内存消耗引起的隐性损坏问题,PostgreSQL提供了块级校验和。默认情况下,数据按原样写入,一次写入一个块。然而,启用块校验和后,PostgreSQL将在I/O上检查每个8k块,看看它是否仍然正常。
要启用数据校验和,可以使用-k选项运行initdb。这将为整个实例设置配置变量。
要检查数据库实例是否启用了数据校验和,可以执行以下命令(假设数据库实例位于/data目录下):
$ pg_controldata /data | grep -i checks
该命令将返回0或1,具体取决于该设置是打开还是关闭。
此时,不可能在initdb之后打开校验和。在生产环境中无法更改配置。
9.2、清除损坏的页面
有时,即使采取了所有预防措施,事情也会出错。文件系统可能会到处丢失一些块,或者磁盘可能只是丢失几个扇区。在这种情况下,PostgreSQL端可能会发生以下情况:
test=# SELECT count(*) FROM t_test;
ERROR: invalid page in block 535 of relation
base/16384/16436
如果一个块(或几个块)在文件系统中出现问题,PostgreSQL将出错并告诉最终用户查询不能再完成了。
在这里概述的场景中,可以确定一件事:存储系统中丢失了一些数据。需要指出的是,数据丢失实际上从来不是由PostgreSQL本身造成的。在大多数情况下,我们谈论的是损坏的硬件、损坏的文件系统或其他一些与内存相关的问题,这些问题已经波及到您的数据文件。
如果出现与存储相关的问题,一般规则是: “不要碰东西;把东西回退回去!” 在做任何可能使问题变得更糟之前,尝试创建文件系统快照、tar存档或任何类型的备份。在许多情况下,旧的“dd”可以做得很好,并且尝试在系统上创建底层设备的克隆绝对是值得的。
一旦系统上的所有内容都被救出,就可以执行下一步。PostgreSQL提供了一个特性,允许您将没有故障的块归零。在前面的示例中,查询无法完成,因为文件中包含一些坏数据。通过将zero_damaged_pages设置为on,可以告诉PostgreSQL将所有损坏的块归零。当然,这并不能挽救丢失的数据,但它至少可以帮助我们从系统中检索一些东西:
test=# SET zero_damaged_pages TO on;
SET
一旦这么设定了,就可以再次执行:
test=# SELECT count(*) FROM t_test;
WARNING: invalid page in block 535 of relation
base/16384/16436; zeroing out page
count
-----------
42342
(1 row)
这一次,每个损坏的块都会弹出警告。幸运的是,在这种情况下,只有一个块被破坏。它很快被一些二进制零替换,查询继续到下一个(适当的)块。
当然,zero_damaged_pages是危险的,因为它消除了坏数据。那你还有什么选择?当某些东西如此破碎时,重要的是至少要挽救它的存在。(让我想起了TPO)
9.3、处理坏掉的索引
偶尔,索引可能会暴涨。在大多数情况下,索引损坏是由硬件故障引起的。另外,在索引损坏的情况下,最可疑的是RAM。因此,要经常检查系统内存,以防索引出现故障。
一般来说,适用相同的规则: 首先拿到快照,然后简单地重新创建这些损坏的索引。REINDEX可以在这种情况下提供帮助:
test=# \h REINDEX
Command: REINDEX
Description: rebuild indexes
Syntax:
REINDEX { INDEX | TABLE | DATABASE | SYSTEM }
name [ FORCE ]
请记住,REINDEX需要一个SHARE锁,这确保不会发生写操作。在某些设置中,这可能会成为一个问题。为了避免锁定,转向CREATE INDEX concurrent是有意义的。并发索引构建比普通构建耗时更长。但是,它避免了阻塞写入的讨厌的表锁。它对许多应用都是非常有益的。
一般来说,与索引相关的问题通常没有表中的坏块那么严重。但是,请记住,索引本身不包含数据,并且不容易重新创建数据。
9.4、转储单页数据
高级用户如果真的想详细检查他们的损坏系统,可以将注意力集中在pageinspect上。它允许它们检查磁盘上的单个数据或索引页。这需要对PostgreSQL的内部工作有相当多的了解,但它可以提供有价值的见解,以了解一个系统可能出现的问题。
要使用该模块,首先必须安装它。它是这样工作的:
test=# CREATE EXTENSION pageinspect;
CREATE EXTENSION
模块就位后,可以检查表。为此,有几个函数可用。最重要的函数是get_raw_page。后续检查将需要:
get_raw_page(relname text, fork text, blkno int)
returns bytea
get_raw_page函数返回一个bytea字段,该字段包含目标页面中的内容。第一个参数告诉我们要检查的关系名称。第二个参数需要所谓的关系分叉。在PostgreSQL中,表不仅仅由数据文件组成。总的来说,一个表有三种类型的文件: main(表本身)、fsm(所谓的free space Map中关于空闲空间的信息)和vm(所谓的Visibility Map)。根据要检查的表的分支,必须将第二个参数设置为适当的值。最后是最后一个参数。它告诉PostgreSQL要转储哪个块。
如果您想提取pg_class(一个系统表)的第一页,请尝试以下操作:
test=# SELECT * FROM get_raw_page('pg_class',
'main', 0);
当然,这个函数的输出太长,无法一一列出。毕竟这是一个8k的块!
9.4.1、抽取目标页头
一旦页面被提取出来,就可以检查页面标题。要做到这一点,只需调用page_header:
test=# \x
Expanded display (expanded) is on.
test=# SELECT * FROM page_header(get_raw_page('pg_class', 'main', 0));
-[ RECORD 1 ]--------
lsn | 0/E6604D8
checksum | 0
flags
lower
upper
special
pagesize | 8192
version | 4
prune_xid | 0
-- pg14中的结果:
postgres=# SELECT * FROM page_header(get_raw_page('pg_class', 'main', 0)) \gx
-[ RECORD 1 ]---------
lsn | 0/2B00B0E8
checksum | 0
flags | 1
lower | 200
upper | 528
special | 8192
pagesize | 8192
version | 4
prune_xid | 0
这些字段的含义是什么?下面的清单包含一个概述:
•pd_lsn: 标识最后一次修改该页的xlog记录
•pd_checksum:如果设置了,这是页面校验和
•pd_flags:这是一组标志位
•pd_lower:这是一个空闲空间开始的偏移量
•pd_upper:这是到空闲空间结束的偏移量
•pd_special:这是一个特殊空间开始的偏移量
•pd_pagesize_version:以字节为单位给出页面大小和页面布局版本号
•pd_prune_xid:这是页面上可能无法编辑的元组中最古老的XID
请记住,并非所有这些信息都适用于每种类型的对象; 例如,pg_special不会给出任何对普通数据文件有用的东西,因为那里没有特殊的空间(通常认为它跟索引有关)。
如果这些数字显示出一些奇怪的东西,这是一个很好的迹象,表明出了问题。
在下一步中,可以检查单个元组(行)。为此,可以使用heap_page_items:
test=# SELECT *
FROM heap_page_items(
get_raw_page('pg_class', 'main', 0)
);
-[ RECORD1 ]------------------------------------
lp
lp_off
lp_flags
lp_len
t_xmin
t_xmax
t_field3
t_ctid
t_infomask2 | 29
t_infomask | 11019
t_hoff
t_bits
t_oid
| 32
| 11111 ... 0000
| 2619
在这个示例中,会有若干行数据返回。
这里再把我在pg14中得到的结果列一下看看:
postgres=# select * from heap_page_items(get_raw_page('pg_class', 'main', 0)) limit 1 \gx
-[ RECORD 1 ]------------------------------------------------------------------------------------------------
lp | 1
lp_off | 7968
lp_flags | 1
lp_len | 217
t_xmin | 916
t_xmax | 0
t_field3 | 6
t_ctid | (0,1)
t_infomask2 | 33
t_infomask | 11011
t_hoff | 32
t_bits | 1111111111111111111111111111111000000000
t_oid |
t_data | \xdf04000070675f747970650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b00000047000000000000000a0000000200000000000000000000000e000000004016440e0000004b10000001007072200000000000000000016e000000000094030000010000005b01000000000000000904000002000000010000000a0000000a0000007f000000000000000a00000002000000
关于pageinspect插件的一些稍深入的用法,我还有一篇文章:PostgreSQL中的B-tree索引如何逐层查看 有兴趣可以去试一下。
9.5、重置事务日志
在本节中,将讨论重置事务日志。在我们开始之前,我想发出一个个人警告: 不要掉以轻心。重置事务日志是一件残酷的事情。它几乎总是导致一些数据丢失,并且不能保证您的数据仍然是完全一致的。当出现问题时,重新设置xlog是最后要考虑的事情。
与我们之前讨论的相同的规则也适用于此:在使用pg_resetxlog之前,始终对文件系统进行快照或关闭数据库,并创建数据目录的二进制副本。让我强调一下我的观点。在我长达15年的PostgreSQL顾问生涯中,我只做过几次这样的事情。通常,这可以通过其他方式解决。
然而,如果PostgreSQL不再启动,因为xlog被破坏,pg_resetxlog可以来拯救你。下面是语法的工作原理:
$ pg_resetxlog --help
pg_resetxlog resets the PostgreSQL transaction log.
Usage:
pg_resetxlog [OPTION]... DATADIR
Options:
-e XIDEPOCH
-f
-l XLOGFILE
set next transaction ID epoch
force update to be done
force minimum WAL starting location
for new transaction log
-m MXID,MXID set next and oldest multitransaction
ID
be done (for testing)
-n
-o OID
-O OFFSET
-V, --version
-x XID
-?, --help
no update, just show what would
# directory
set next OID
set next multitransaction offset
output version information, then exit
set next transaction ID
show this help, then exit
这里有几个设置。最重要的是- 0,-x, -e, -m, -o和- 1。这些设置允许您转到某个事务ID,设置OID,等等。
在某些情况下,可能会发生pg_resetxlog抱怨pg_control文件无效的情况。控制文件非常重要,因为它包含关于块大小、检查点、吐司大小等的信息。要查看控件中有哪些数据,请尝试以下代码片段:
pg_controldata $PGDATA # or replace $PGDATA with # your PostgreSQL data directory
pg_controldata文件将向屏幕写入大量信息。如果pg_resetxlog找不到有效的控制文件,则必须使用-f。然后,即使控制文件已经消失或损坏,它也会尝试修复问题。
一旦执行了pg_resetxlog,通常可以再次启动数据库(除非它真的注定要失败)。确保立即进行备份,并检查数据是否正常。这包括验证所有外键是否正常,以及是否可以很好地转储或恢复数据。
【注意:】现在pg_resetxlog改名了,叫pg_resetwal。但是基本用法差不多。
9.6、断电相关问题
硬件并不总是一个问题。你可能很容易遇到停电这样简单的事情。一般来说,PostgreSQL很容易在断电后存活下来。事务日志处理所有与故障相关的问题,在xlog的帮助下,始终可以将数据库恢复到一致状态。拔掉插头并依赖PostgreSQL的质量是安全的。
然而,有一些恶劣的情况会导致更恶劣的麻烦。在提交时,PostgreSQL依赖于数据可以刷新到磁盘的事实。系统调用fsync用于确保所有相关的内容都被强制到磁盘上。但是,如果fsync不能完成它的工作怎么办?如果不能(或没有)将数据刷新到磁盘怎么办?如果您的系统已启动并运行,则一切正常。数据会在某个时刻保存到磁盘上,这样生活就美好了! 然而,如果发生崩溃怎么办?形势更加严峻。简而言之,任何事情都有可能发生。如果PostgreSQL不知道什么已经安全写入磁盘,什么没有,任何类型的损坏都可能发生。
为什么fsync会失败?如果硬件出现故障,事情总是会出问题。然而,这里有一个更常见的问题——虚拟化。默认情况下,并非所有虚拟化软件都会将数据刷新到磁盘。出于性能方面的考虑,一些系统,比如VirtualBox,会默认通过更懒惰的方式来优化fsync。结果可能是灾难性的,不仅对PostgreSQL,而且对整个虚拟机都是如此。
fsync的重要性怎么强调都不为过。如果对磁盘的刷新有些中断,那么整个数据库实例将由电力公司来处理。当然,不刷新磁盘要快得多,但这也可能意味着您的数据将更快地丢失。
如果在虚拟化设置中丢失数据,请始终尝试弄清楚fsync是否真的在底层硬件组件上执行它应该执行的操作。
总结
在本章中,讨论了最基本和最常见的问题。这包括硬件故障以及与索引损坏、xlog损坏等相关的问题。