数据库迁移
这里,我们将基于Pigsty沙箱环境,用一个实际的例子演示基于逻辑复制的Postgres数据库迁移。
我们有两套数据库集群pg-meta
与pg-test
,现在我们希望将pg-meta
的数据在线迁移至pg-test
中。
逻辑复制概述
逻辑复制通常可以用于跨大版本在线升级PostgreSQL,例如从10到13。
相比原地升级与pg_dump升级,逻辑复制的好处有:
- 在线:迁移可以在线进行,不需要或者只需要极小的停机窗口。
- 灵活:目标库的结构可以与源库不同,例如普通表改为分区表,加列等。可以跨越大版本使用。
- 安全:相比物理复制,目标库是可写的,因此在最终切换前,可以随意进行测试并重建。
逻辑复制的缺点包括:
设置较为繁琐,初始时刻拷贝数据较慢,对于单实例多DB的情况需要迁移多次。以及,LO与Sequence需要手动处理。
准备测试环境
首先准备用户,下面使用Pigsty默认沙箱用户。
PGSRC_ADMIN='postgres://dbuser_admin:DBUser.Admin@pg-meta/meta'
PGSRC_REPL='postgres://replicator:DBUser.Replicator@pg-meta/meta'
PGDST_ADMIN='postgres://dbuser_admin:DBUser.Admin@pg-test/test'
PGDST_REPL='postgres://replicator:DBUser.Replicator@pg-meta/meta'
在pg-meta
集群的meta
数据库中创建以下测试用表
psql ${PGSRC_ADMIN} <<-'EOF'
CREATE TABLE t_normal(id BIGSERIAL PRIMARY KEY,v TIMESTAMP); -- 常规表,带有主键
CREATE TABLE t_unique(id SERIAL UNIQUE,v TIMESTAMP); -- 没有主键,只有UNIQUE约束的表
CREATE TABLE t_tricky(id BIGINT,v TIMESTAMP); -- 无主键也无UNIQUE的表
EOF
然后使用以下命令,生成一些负载,模拟针对表的修改
# insert into common_table per second
while true; do psql ${PGSRC_ADMIN} -c 'INSERT INTO t_normal(v) VALUES(CURRENT_TIMESTAMP);'; sleep 1 ; done
迁移准备工作
修改配置
保证拥有一个具有REPLICATION
权限的用户,本例中我们使用Pigsty沙箱replicator
用户
必须在旧库上配置wal_level = 'logical'
修复特例
检查需要复制的表,是否都具有REPLICA IDENTITY
。具体来讲,就是这些表是不是都有主键,或者退一步,虽然没有主键,但是有UNIQUE NOT NULL
的索引(NOT NULL很重要,因为NULL != NULL,所以UNIQUE索引允许多个记录都带有“相同”的取值NULL)
用以下的查询检查是否存在没有主键的表。
SELECT name, CASE WHEN has_primary THEN 'good' WHEN NOT has_primary AND has_unique THEN 'check' ELSE 'bad' END AS status,
CASE relreplident WHEN 'd' THEN 'default' WHEN 'n' THEN 'nothing' WHEN 'f' THEN 'full' WHEN 'i' THEN 'index' END AS replica_identity,
has_primary, has_unique
FROM
(
SELECT quote_ident(nspname) || '.' || quote_ident(relname) AS name, relreplident,
EXISTS (SELECT * FROM pg_index WHERE indrelid = c.oid AND indisprimary AND indisvalid AND indisready AND indislive) AS has_primary,
EXISTS (SELECT * FROM pg_index WHERE indrelid = c.oid AND indisunique AND indisvalid AND indisready AND indislive) AS has_unique
FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema', 'monitor', 'repack', 'pg_toast')
ORDER BY 2,3
) res;
例如,这里标记为good
表示该表具有主键,bad
表示没有合适的复制身份,check
表示虽然没有主键但是有UNIQUE约束,最好检查一下。
name | status | replica_identity | has_primary | has_unique
------------------------+--------+------------------+-------------+------------
public.t_tricky | bad | default | f | f
public.t_unique | check | default | f | t
public.t_normal | good | default | t | t
如果无法为表创建主键
为没有主键的表配置复制身份:
ALTER TABLE t_unique REPLICA IDENTITY USING INDEX t_unique_id_key;
ALTER TABLE t_tricky REPLICA IDENTITY FULL;
检查设置之后的身份
name | status | replica_identity | has_primary | has_unique
------------------------+--------+------------------+-------------+------------
public.t_tricky | bad | full | f | f
public.t_unique | check | index | f | t
public.t_normal | good | default | t | t
同步模式
使用以下命令转储public
模式下的所有表结构,或者根据你自己的需求手工处理。
pg_dump ${PGSRC_ADMIN} --schema-only -n public | psql ${PGDST_ADMIN}
创建发布
CREATE PUBLICATION name
[ FOR TABLE [ ONLY ] table_name [ * ] [, ...]
| FOR ALL TABLES ]
[ WITH ( publication_parameter [= value] [, ... ] ) ]
PUBLICATION的名称不要用特殊字符,特别是-
,因为创建同名的复制槽时会有非法字符问题。
CREATE PUBLICATION "pg_meta_pub" FOR ALL TABLES;
创建订阅
CREATE SUBSCRIPTION subscription_name
CONNECTION 'conninfo'
PUBLICATION publication_name [, ...]
[ WITH ( subscription_parameter [= value] [, ... ] ) ]
创建订阅有一些参数:
copy_data
,默认为true
,当复制开始时,是否要复制全量数据。create_slot
,默认为true
,该订阅是否会在发布实例上创建复制槽。enabled
,默认为true
,是否立即开始订阅。connect
,默认为true
,是否连接至订阅实例,如果不连接,上面几个选项都会被重置为false
。
CREATE SUBSCRIPTION "pg_test_sub" CONNECTION 'host=10.10.10.10 user=replicator password=DBUser.Replicator dbname=meta' PUBLICATION "pg_meta_pub";
订阅创建后,会开始复制数据。
检查系统视图pg_subscription_rel
,如果所有的表都处于ready
状态,就说明复制就绪了。
迁移时刻
检查复制状态
On Src
CHECKPOINT;
CHECKPOINT;
On Dst
TABLE pg_subscription_rel;
暂停写入
修改Src主库Pgbouncer配置文件指向Dst,但先不应用。
停止原主库
同步序列
生成转储所有非空Sequence的SQL:
psql ${PGSRC_ADMIN} -qwAXt > seq.sql <<-'EOF'
SELECT $$SELECT setval('$$ || quote_ident(schemaname) || $$.$$ || quote_ident(sequencename) || $$', $$ || last_value || $$); $$ AS sql FROM pg_sequences WHERE last_value IS NOT NULL;
EOF
psql ${PGDST_ADMIN} -qwAxt -f seq.sql
导出的SQL结果为:
SELECT setval('public.t_normal_id_seq', 2);
比较两边数据库的Sequence区别
psql ${PGSRC_ADMIN} -qwAXtc "SELECT schemaname || '.' || sequencename AS name, last_value AS value FROM pg_sequences ORDER BY 1;" > seq-src.res
psql ${PGDST_ADMIN} -qwAXtc "SELECT schemaname || '.' || sequencename AS name, last_value AS value FROM pg_sequences ORDER BY 1;" > seq-dst.res
diff seq-src.res seq-dst.res
如果有区别,再重新执行一遍,或者检查哪里有错漏。
校验数据
切换写入
如果流量切换由DBA负载,则重载Pgbouncer配置,将其指向新实例。并通知业务方切换流量。
后事料理
DROP SUBSCRIPTION upgrade_sub;
ALTER SUBSCRIPTION upgrade_sub DISABLE ;
ALTER SUBSCRIPTION upgrade_sub SET (slot_name = NONE);
DROP SUBSCRIPTION upgrade_sub;
8) Time for some bubbly drinks
Note that if you won’t keep the old “publisher” accessible in read-only or normal primary mode (dangerous!) though, some extra steps are needed here before dropping:
SELECT table_schema || '.' || table_name
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
AND table_schema || '.' || table_name NOT IN (
select table_schema || '.' || table_name
from information_schema.table_constraints
WHERE constraint_type in ('PRIMARY KEY', 'UNIQUE')
)
AND table_schema NOT IN ('information_schema', 'pg_catalog');
逻辑复制基础知识
发布的性质
- 只能在逻辑复制的主库上创建
- 发布是一组表所构成的复制集合(Replication set),发布包含了这些表上产生的变更。
- 发布是属于数据库的(per database),而不是属于集簇的(not per cluster)
- 发布与表是一对多的关系,每个表可以加入到多个发布中。除非使用
FOR ALL TABLES
,每个表必须被显式加入到发布中。 - 发布与订阅是多对多的关系
复制身份
被发布到表必须具有复制身份(Replica Identity),只有这样,才可以在订阅者上定位UPDATE|DELETE
所操作的行。
订阅的性质
- 一个订阅(subscription)可以订阅另一个数据库实例上的一个或多个发布(publication)
- 订阅需要通过复制槽来接受变更,订阅在初始数据拷贝阶段会创建临时的复制槽。
- 逻辑订阅者可以作为 同步复制 的 standby,订阅名会作为
application_name
。 - 只有Superuser可以dump订阅,其他用户执行dump
发布 pg_publication
postgres@meta:5432/meta=# table pg_publication;
-[ RECORD 1 ]+------------
oid | 20453
pubname | pg_meta_pub
pubowner | 10
puballtables | t
pubinsert | t
pubupdate | t
pubdelete | t
pubtruncate | t
pubviaroot | f
puballtables
:是否包含所有的表pubinsert|update|delete|truncate
是否发布这些操作pubviaroot
:如果设置了该选项,任何分区表(叶表)都会使用最顶层的(被)分区表的复制身份。所以可以把整个分区表当成一个表,而不是一系列表进行发布。
发布表内容 系统视图 pg_publication_tables
由pg_publication
,pg_class
和pg_namespace
拼合而成的视图,记录了发布中包含的表信息。
postgres@meta:5432/meta=# table pg_publication_tables;
pubname | schemaname | tablename
-------------+------------+-----------------
pg_meta_pub | public | spatial_ref_sys
pg_meta_pub | public | t_normal
pg_meta_pub | public | t_unique
pg_meta_pub | public | t_tricky
使用pg_get_publication_tables
可以根据订阅的名字获取订阅表的OID
SELECT * FROM pg_get_publication_tables('pg_meta_pub');
SELECT p.pubname,
n.nspname AS schemaname,
c.relname AS tablename
FROM pg_publication p,
LATERAL pg_get_publication_tables(p.pubname::text) gpt(relid),
pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.oid = gpt.relid;
复制槽 pg_replication_slot
postgres@meta:5432/meta=# table pg_replication_slots ;
-[ RECORD 1 ]-------+------------
slot_name | pg_test_sub
plugin | pgoutput
slot_type | logical
datoid | 19355
database | meta
temporary | f
active | t
active_pid | 89367
xmin | NULL
catalog_xmin | 1524
restart_lsn | 0/2A08D40
confirmed_flush_lsn | 0/2A097F8
wal_status | reserved
safe_wal_size | NULL
SELECT pg_current_wal_lsn() - confirmed_flush_lsn AS lag,* FROM pg_replication_slots;
订阅 pg_subscription
每一个逻辑订阅都会有一条记录,注意这个视图是跨数据库集簇范畴的,每个数据库中都可以看到整个集簇中的订阅信息。
oid | 20421
subdbid | 19356
subname | pg_test_sub
subowner | 10
subenabled | t
subconninfo | host=10.10.10.10 user=replicator password=DBUser.Replicator dbname=meta
subslotname | pg_test_sub
subsynccommit | off
subpublications | {pg_meta_pub}
subenabled
:订阅是否启用subconninfo
:因为包含敏感信息,会针对普通用户进行隐藏。subslotname
:订阅使用的复制槽名称,也会被用作逻辑复制的源名称(Origin Name),用于除重。
订阅状态 pg_stat_subscription
每个活跃订阅都会在这个视图中有至少一条记,即Main Worker(负责应用Logical日志)。
Main Workker的relid = NULL
,如果有负责初始数据拷贝的进程,也会在这里有一行记录,relid
为负责拷贝数据的表。
subid | 20421
subname | pg_test_sub
pid | 5261
relid | NULL
received_lsn | 0/2A4F6B8
last_msg_send_time | 2021-02-22 17:05:06.578574+08
last_msg_receipt_time | 2021-02-22 17:05:06.583326+08
latest_end_lsn | 0/2A4F6B8
latest_end_time | 2021-02-22 17:05:06.578574+08
received_lsn
:最近收到的日志位置。lastest_end_lsn
:最后向WAL Sender回报的LSN位置,即主库上的confirmed_flush_lsn
。
局限性
-
不能复制DDL变更(通过手工
pg_dump --schema-only
解决) -
不能复制序列号(Sequence),(需要Failover到逻辑从库时,通过pg_dump解决)
-
如果逻辑从库上某张被外键引用的表被Truncate,但因为引用该表的表不在订阅集中(所以无法在不truncate该表的情况下继续,但在订阅集之外的表上执行truncate违反语义),那么就会出现冲突。
-
大对象无法复制。
-
只支持普通表的复制,包括分区表。不支持视图,物化视图,外部表。