如何在PostgreSQL故障切换后找回丢失的数据

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: 作者简介陈华军,苏宁易购云软件公司架构专家,主要负责数据库产品的相关设计工作。十年以上数据库相关工作经验。PostgreSQL中文社区核心组成员,主要负责PostgreSQL中文手册翻译项目的维护。1. 背景PostgreSQL的HA方案一般都基于其原生的流复制技术,支持同步复制和异步复制模式。 同步复制模式虽然可以最大程度保证数据不丢失,但通常需要至少部署三台机器,确保有两台以上的备节点。 因此很多一主一备HA集群,都是使用异步复制。在异步复制下,主库宕机,把备节点切换为新的主节点后,可能会丢失最近更新的少量数据。 如果这些丢失的数据对业务比较重要,那么,能不能从数据库里找回来呢?

作者简介
陈华军,苏宁易购云软件公司架构专家,主要负责数据库产品的相关设计工作。十年以上数据库相关工作经验。PostgreSQL中文社区核心组成员,主要负责PostgreSQL中文手册翻译项目的维护。

  1. 背景

PostgreSQL的HA方案一般都基于其原生的流复制技术,支持同步复制和异步复制模式。 同步复制模式虽然可以最大程度保证数据不丢失,但通常需要至少部署三台机器,确保有两台以上的备节点。 因此很多一主一备HA集群,都是使用异步复制。
在异步复制下,主库宕机,把备节点切换为新的主节点后,可能会丢失最近更新的少量数据。 如果这些丢失的数据对业务比较重要,那么,能不能从数据库里找回来呢?
下面就介绍找回这些数据的方法

  1. 原理

基本过程

  1. 备库被提升为新主后会产生一个新的时间线,这个新时间线的起点我们称之为分叉点。
  2. 旧主故障修复后,在旧主上从分叉点位置开始解析WAL文件,将所有已提交事务产生的数据变更解析成SQL。

前提是旧主磁盘没有损坏,能够正常启动。不过,生产最常见的故障是物理机宕机,一般重启机器就可以恢复。

  1. 业务拿到这些SQL,人工确认后,回补数据。

为了能从WAL记录解析出完整的SQL,最好wal_level设置成logical,并且表上有主键。 此时,对于我们关注的增删改DML语句,WAL记录中包含了足够的信息,能够把数据变更还原成SQL。 详细如下:
•INSERT
WAL记录中包含了完整的tuple数据,结合系统表中表定义可以还原出SQL。
•UPDATE
WAL记录中包含了完整的更新后的tuple数据,对于更新前的tuple,视以下情况而定。
–表设置了replica identity full属性
WAL记录中包含完整的更新前的tuple数据
–表包含replica identity key(或主键)且replica identity key的值发生了变更
WAL记录中包含了更新前的tuple的replica identity key(或主键)的字段值
–其他
WAL记录中不包含更新前的tuple数据
•DELETE
WAL记录中可能包含被删除的tuple信息,视以下情况而定。
–表设置了replica identity full属性
WAL记录中包含完整的被删除的tuple数据
–表包含replica identity key(或主键)
WAL记录中包含被删除的tuple的replica identity key(或主键)的字段值
–其他
WAL记录中不包含被删除的tuple数据
如果wal_level不是logical或表上没有主键,还可以从WAL中的历史FPI(FULL PAGE IANGE)中解析出变更前tuple。
因此,原理上,从WAL解析出SQL是完全可行的。并且也已经有开源工具可以支持这项工作了。

  1. 工具

使用改版的walminer工具解析WAL文件。
gitee。com/skykiker/XLogMiner
walminer是一款很不错的工具,可以从WAL文件中解析出原始SQL和undo SQL。 但是当前原生的walminer要支持这一场景还存在一些问题,并且解析WAL文件的速度非常慢。
改版的walminer分支增加了基于LSN位置的解析功能,同时修复了一些BUG,并优化了解析速度。其中的部分修改后续希望能合到walminer主分支里。

  1. 前提条件

1.分叉点之后的WAL日志文件未被清除
正常是足够的。也可以设置合理的wal_keep_segments参数,在pg_wal目录多保留一些WAL。比如:
wal_keep_segments=100
如果配置了WAL归档,也可以使用归档目录中的WAL。
2.WAL日志级别设置为logical
wal_level=logical
3.表有主键或设置了replica identity key/replica identity full
4.分叉点之后表定义没有发生变更
注:以上条件的2和3如果不满足其实也可以支持,但是需要保留并解析分叉点的前一个checkpint以后的所有WAL。

  1. 使用演示

5.1 环境准备
搭建好一主一备异步复制的HA集群
机器:

  • node1(主)
  • node2(备)

软件:

  • PostgreSQL 10,11或12

参数:
wal_level=logical
5.2 安装walminer插件
从以下位置下载改版walminer插件源码
<•gitee。com/skykiker/XLogMiner/>
在主备库分别安装walminer
cd walminer
make && make install
在主库创建walminer扩展
create extension walminer
5.3 创建测试表
create table tb1(id int primary key, c1 text);
insert into tb1 select id,'xxx' from generate_series(1,10000) id;
5.4 模拟业务负载
准备测试脚本
test.sql
\set id1 random(1,10000)
\set id2 random(1,10000)

insert into tb1 values(:id1,'yyy') on conflict (id)
do update set c1=excluded.c1;

delete from tb1 where id=:id2;
在主库执行测试脚本模拟业务负载
pgbench -c 8 -j 8 -T 1000 -f test.sql
5.5 模拟主库宕机
在主库强杀PG进程
killall -9 postgres
5.6 备库提升为新主
在备库执行提升操作
pg_ctl promote
查看切换时的时间线分叉点
[postgres@host2 ~]$tail -1 /pgsql/data10/pg_wal/00000002.history
1 0/EF76440 no recovery target specified
5.7 在旧主库找回丢失的数据
启动旧主库后调用wal2sql()函数,找回分叉点以后手游买号平台旧主库上已提交事务执行的所有SQL。
postgres=# select xid,timestamptz,op_text from wal2sql(NULL,'0/EF76440') ;
NOTICE: Get data dictionary from current database.
NOTICE: Wal file "/pgsql/data10/pg_wal/00000001000000000000000F" is not match with datadictionary.
NOTICE: Change Wal Segment To:/pgsql/data10/pg_wal/00000001000000000000000C
NOTICE: Change Wal Segment To:/pgsql/data10/pg_wal/00000001000000000000000D
NOTICE: Change Wal Segment To:/pgsql/data10/pg_wal/00000001000000000000000E

xid timestamptz op_text
938883 2020-03-31 17:12:10.331487+08 DELETE FROM "public"."tb1" WHERE "id"=7630;
938884 2020-03-31 17:12:10.33149+08 INSERT INTO "public"."tb1"("id", "c1") VALUES(5783, 'yyy');
938885 2020-03-31 17:12:10.331521+08 DELETE FROM "public"."tb1" WHERE "id"=3559;
938886 2020-03-31 17:12:10.331586+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=7585;
938887 2020-03-31 17:12:10.331615+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=973;
938888 2020-03-31 17:12:10.331718+08 INSERT INTO "public"."tb1"("id", "c1") VALUES(7930, 'yyy');
938889 2020-03-31 17:12:10.33173+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=1065;
938890 2020-03-31 17:12:10.331741+08 INSERT INTO "public"."tb1"("id", "c1") VALUES(2627, 'yyy');
938891 2020-03-31 17:12:10.331766+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=1012;
938892 2020-03-31 17:12:10.33178+08 INSERT INTO "public"."tb1"("id", "c1") VALUES(4740, 'yyy');
938893 2020-03-31 17:12:10.331814+08 DELETE FROM "public"."tb1" WHERE "id"=4275;
938894 2020-03-31 17:12:10.331892+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=8651;
938895 2020-03-31 17:12:10.33194+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=9313;
938896 2020-03-31 17:12:10.331967+08 DELETE FROM "public"."tb1" WHERE "id"=3251;
938897 2020-03-31 17:12:10.332001+08 DELETE FROM "public"."tb1" WHERE "id"=2968;
938898 2020-03-31 17:12:10.332025+08 INSERT INTO "public"."tb1"("id", "c1") VALUES(5331, 'yyy');
938899 2020-03-31 17:12:10.332042+08 UPDATE "public"."tb1" SET "c1" = 'yyy' WHERE "id"=3772;
938900 2020-03-31 17:12:10.332048+08 INSERT INTO "public"."tb1"("id", "c1") VALUES(94, 'yyy');

(18 rows)

Time: 2043.380 ms (00:02.043)
上面wal2sql()的输出结果是按事务在WAL中提交的顺序排序的。可以把这些SQL导到文件里提供给业务修单。
5.8 恢复旧主
可以通过pg_rewind快速回退旧主多出的数据,然后作为新主的备库重建复制关系,恢复HA。

  1. 小结

借助改版的walminer,可以方便快速地在PostgreSQL故障切换后找回丢失的数据。
walminer除了能生成正向SQL,还可以生成逆向的undo SQL,也就是我们熟知的闪回功能。 undo SQL的生成方法和使用限制可以参考开源项目文档。
然而,在作为闪回功能使用时,walminer的解析速度还是需要注意的。 因为从WAL记录中完整解析undo SQL需要开启replica identity full,而很多系统可能不会打开或者不会为每个表都打开replica identity full设置。 在没有replica identity full的前提下,生成undo SQL就必须要依赖历史FPI。
如果面对几十GB的WAL文件,解析并收集所有历史FPI,资源和时间消耗还是很大的。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
1月前
|
关系型数据库 MySQL Java
【MySQL+java+jpa】MySQL数据返回项目的感悟
【MySQL+java+jpa】MySQL数据返回项目的感悟
41 1
|
1月前
|
SQL 监控 关系型数据库
MySQL怎么全局把一张表的数据回滚
MySQL怎么全局把一张表的数据回滚
96 2
|
1月前
|
存储 SQL 关系型数据库
MySQL批量添加数据并取外表的某个字段值
MySQL批量添加数据并取外表的某个字段值
60 1
|
18天前
|
安全 关系型数据库 MySQL
如何将数据从MySQL同步到其他系统
【10月更文挑战第17天】如何将数据从MySQL同步到其他系统
110 0
|
24天前
|
SQL 前端开发 关系型数据库
全表数据核对 ,行数据核对,列数据核对,Mysql 8.0 实例(sample database classicmodels _No.3 )
全表数据核对 ,行数据核对,列数据核对,Mysql 8.0 实例(sample database classicmodels _No.3 )
42 0
全表数据核对 ,行数据核对,列数据核对,Mysql 8.0 实例(sample database classicmodels _No.3 )
|
30天前
|
关系型数据库 MySQL 数据库
mysql 里创建表并插入数据
【10月更文挑战第5天】
110 1
|
1月前
|
分布式计算 关系型数据库 MySQL
大数据-88 Spark 集群 案例学习 Spark Scala 案例 SuperWordCount 计算结果数据写入MySQL
大数据-88 Spark 集群 案例学习 Spark Scala 案例 SuperWordCount 计算结果数据写入MySQL
48 3
|
3天前
|
SQL 前端开发 关系型数据库
SpringBoot使用mysql查询昨天、今天、过去一周、过去半年、过去一年数据
SpringBoot使用mysql查询昨天、今天、过去一周、过去半年、过去一年数据
31 9
|
15天前
|
SQL Java 关系型数据库
java连接mysql查询数据(基础版,无框架)
【10月更文挑战第12天】该示例展示了如何使用Java通过JDBC连接MySQL数据库并查询数据。首先在项目中引入`mysql-connector-java`依赖,然后通过`JdbcUtil`类中的`main`方法实现数据库连接、执行SQL查询及结果处理,最后关闭相关资源。
|
11天前
|
SQL 关系型数据库 MySQL
定时任务频繁插入数据导致锁表问题 -> 查询mysql进程
定时任务频繁插入数据导致锁表问题 -> 查询mysql进程
27 1
下一篇
无影云桌面