DBA表示惊呆了,流复制也不会同步序列?
前言
这两天,一位学员问了这样一个问题:"主备的序列不会进行同步吗?",逻辑复制场景下,我们知道序列并不会进行同步,而是直接复制的SQL语句,并没有真正的在订阅侧再次调用序列。
那么流复制呢?流复制的现象就比较诡异,在一定时间内,也不会进行复制,看似会打破流复制的复制行为,实则是序列的独特实现方式导致!让我们一探究竟~
复现
首先搭建一个异步主从
postgres=# create table t2(id serial,info text);
CREATE TABLE
postgres=# insert into t2(info) values('hello');
INSERT 0 1
postgres=# insert into t2(info) values('world');
INSERT 0 1
postgres=# select * from t2_id_seq ;
last_value | log_cnt | is_called
------------+---------+-----------
2 | 31 | t
(1 row)
然后让我们在备库上瞅瞅
postgres=# select pg_is_in_recovery();
pg_is_in_recovery
-------------------
t
(1 row)postgres=# select * from t2_id_seq ;
last_value | log_cnt | is_called
------------+---------+-----------
33 | 0 | t
(1 row)
确实如这位读者所说,在备库该序列还是一个"初始状态",并且在备库是不能调用 nextval 手动推进序列值的
postgres=# select nextval('t2_id_seq');
ERROR: cannot execute nextval() in a read-only transaction
说明这种行为是"写行为"。那么为什么会这样?既然是写行为,如果是一个强同步备库的话,备库挂了,序列还能正常调用吗?让我们验证一下:
postgres=# select sync_state,pid from pg_stat_replication;
sync_state | pid
------------+-------
sync | 23631
(1 row)
然后关闭强同步备库
postgres=# begin;
BEGIN
postgres=*# insert into t2(info) values('world');
INSERT 0 1
postgres=*# commit ;
^CCancel request sent
WARNING: canceling wait for synchronous replication due to user request
DETAIL: The transaction has already committed locally, but might not have been replicated to the standby.
COMMITpostgres=# begin;
BEGIN
postgres=*# select nextval('t2_id_seq');
nextval
---------
4
(1 row)
postgres=*# rollback ;
ROLLBACK
postgres=# begin;
BEGIN
postgres=*# select nextval('t2_id_seq');
nextval
---------
5
(1 row)
postgres=*# commit ;
COMMIT
奇怪的是,与刚刚的结论有点不符,虽然是写行为,但是 COMMIT 的时候,居然没有阻塞。
其实这和序列的 WAL 日志原理有关,我在之前文章中也提过
/*
* We don't want to log each fetching of a value from a sequence,
* so we pre-log a few fetches in advance. In the event of
* crash we can lose (skip over) as many values as we pre-logged.
*/
#define SEQ_LOG_VALS 32...
/*
* We don't log the current state of the tuple, but rather the state
* as it would appear after "log" more fetches. This lets us skip
* that many future WAL records, at the cost that we lose those
* sequence values if we crash.
*/
XLogBeginInsert();
XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT);
/* set values that will be saved in xlog */
seq->last_value = next;
seq->is_called = true;
seq->log_cnt = 0;
序列和其他数据库对象一样,发生了变更需要记录到WAL中,但是假如每变化一次都记录一次,会对性能产生冲击,所以预先记录了一些值,也就是说,序列每 32 个值记录一次 WAL。
那么当调用了 32 次之后,理论上再下一次调用便会卡住,让我们验证一下:
postgres=# select * from t2_id_seq ;
last_value | log_cnt | is_called
------------+---------+-----------
68 | 4 | t
(1 row)postgres=# select nextval('t2_id_seq');
nextval
---------
69
(1 row)
postgres=# select nextval('t2_id_seq');
nextval
---------
70
(1 row)
postgres=# select * from t2_id_seq ;
last_value | log_cnt | is_called
------------+---------+-----------
70 | 2 | t
(1 row)
postgres=# select nextval('t2_id_seq');
nextval
---------
71
(1 row)
postgres=# select nextval('t2_id_seq');
nextval
---------
72
(1 row)
postgres=# select nextval('t2_id_seq'); ---此处卡住
---夯住
^CCancel request sent
WARNING: canceling wait for synchronous replication due to user request
DETAIL: The transaction has already committed locally, but might not have been replicated to the standby.
这种机制主要为了提升性能,但是一旦数据库崩溃了,就会丢失部分值,丢失了 log_cnt 数量的序列。当然这个并没有什么大的危害,这是可能会对序列造成浪费,形成空洞。
[postgres@mypg ~]$ psql
psql (17devel)
Type "help" for help.postgres=# select * from myseq ;
last_value | log_cnt | is_called
------------+---------+-----------
231 | 0 | t
(1 row)
postgres=# select nextval('myseq');
nextval
---------
232
(1 row)
postgres=# \q
[postgres@mypg ~]$ pg_ctl -D 17data restart -mi
waiting for server to shut down.... done
server stopped
waiting for server to start....2024-09-10 21:21:12.521 CST [23943] LOG: redirecting log output to logging collector process
2024-09-10 21:21:12.521 CST [23943] HINT: Future log output will appear in directory "
log".
done
server started
[postgres@mypg ~]$ psql
psql (17devel)
Type "help" for help.postgres=# select * from myseq ;
last_value | log_cnt | is_called
------------+---------+-----------
264 | 0 | t
(1 row)
然后 log_cnt 置为 0,下一次调用又会记录一次 WAL。
另外一个值得注意的地方是,每次 checkpoint 之后第一次调用 nextval,也会重置 log_cnt 为 32。
postgres=# checkpoint ;
CHECKPOINT
postgres=# select nextval('t2_id_seq');
^CCancel request sent
WARNING: canceling wait for synchronous replication due to user request
DETAIL: The transaction has already committed locally, but might not have been replicated to the standby.
postgres=# checkpoint ;
CHECKPOINT
postgres=# select nextval('t2_id_seq');
^CCancel request sent
WARNING: canceling wait for synchronous replication due to user request
DETAIL: The transaction has already committed locally, but might not have been replicated to the standby.
这样的话,每次就需要记录一次 WAL,自然就需要阻塞等待了。
小结
序列为了提升性能,每 32 个值才会记录一次 WAL,这种行为会"违反"流复制下的复制行为,提交无需等待强同步备库 (因为实际上在 log_cnt 到达 32 之前压根就没有记录 WAL),因此,备库的序列值也会和主库不一样,主库每到达一次 log_cnt,就记录一次 WAL,然后备库的序列值就加 32,不断循环往复。再加上 cache 机制,提前将序列值缓存到内存中,如果主库有大量会话同时使用,更会加剧序列的空洞。
如果发生了切换,或者 crash 了,都会产生一些空洞,如果在某些场景中,需要严格依赖连续的值,可以考虑用 setval 重置一下,可以参照 https://stackoverflow.com/questions/244243/how-to-reset-postgres-primary-key-sequence-when-it-falls-out-of-sync。
参考
https://www.postgresql.org/message-id/[email protected]
https://stackoverflow.com/questions/244243/how-to-reset-postgres-primary-key-sequence-when-it-falls-out-of-sync