暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB-PG原理解读——Sequence 使用、原理全面解析(五)

PolarDB农夫山泉 2023-08-29
296

PolarDB PostgreSQL版(以下简称 PolarDB-PG)是一款阿里云自主研发的企业级数据库产品,采用计算存储分离架构,兼容 PostgreSQL 与 Oracle。PolarDB-PG 的存储与计算能力均可横向扩展,具有高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB-PG 具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载;还具有时空、向量、搜索、图谱等多模创新特性,可以满足企业对数据处理日新月异的新需求。

原理剖析

Sequence 在系统表与数据表中的描述

在 PostgreSQL 中有一张专门记录 Sequence 信息的系统表,即 pg_sequence。其表结构如下:

postgres=# \d pg_sequence Table "pg_catalog.pg_sequence" Column | Type | Collation | Nullable | Default --------------+---------+-----------+----------+--------- seqrelid | oid | | not null | seqtypid | oid | | not null | seqstart | bigint | | not null | seqincrement | bigint | | not null | seqmax | bigint | | not null | seqmin | bigint | | not null | seqcache | bigint | | not null | seqcycle | boolean | | not null | Indexes: "pg_sequence_seqrelid_index" PRIMARY KEY, btree (seqrelid)
复制

不难看出,pg_sequence 中记录了 Sequence 的全部的属性信息,该属性在 CREATE/ALTER SEQUENCE 中被设置,Sequence 的 nextval 以及 setval 要经常打开这张系统表,按照规则办事。

对于 Sequence 序列数据本身,其实现方式是基于 heap 表实现的,heap 表共计三个字段,其在表结构如下:

typedef struct FormData_pg_sequence_data { int64 last_value; int64 log_cnt; bool is_called; } FormData_pg_sequence_data;
复制
  • last_value 记录了 Sequence 的当前的序列值,我们称之为页面值(与后续的缓存值相区分)
  • log_cnt 记录了 Sequence 在 nextval 申请时,预先向 WAL 中额外申请的序列次数,这一部分我们放在序列申请机制剖析中详细介绍。
  • is_called 标记 Sequence 的 last_value 是否已经被申请过,例如 setval 可以设置 is_called 字段:
-- setval false postgres=# select setval('seq', 10, false); setval -------- 10 (1 row) postgres=# select * from seq; last_value | log_cnt | is_called ------------+---------+----------- 10 | 0 | f (1 row) postgres=# select nextval('seq'); nextval --------- 10 (1 row) -- setval true postgres=# select setval('seq', 10, true); setval -------- 10 (1 row) postgres=# select * from seq; last_value | log_cnt | is_called ------------+---------+----------- 10 | 0 | t (1 row) postgres=# select nextval('seq'); nextval --------- 11 (1 row)
复制

每当用户创建一个 Sequence 对象时,PostgreSQL 总是会创建出一张上面这种结构的 heap 表,来记录 Sequence 对象的数据信息。当 Sequence 对象因为 nextvalsetval 导致序列值变化时,PostgreSQL 就会通过原地更新的方式更新 heap 表中的这一行的三个字段。

setval 为例,下面的逻辑解释了其具体的原地更新过程。

static void do_setval(Oid relid, int64 next, bool iscalled) { /* 打开并对Sequence heap表进行加锁 */ init_sequence(relid, &elm, &seqrel); ... /* 对buffer进行加锁,同时提取tuple */ seq = read_seq_tuple(seqrel, &buf, &seqdatatuple); ... /* 原地更新tuple */ seq->last_value = next; /* last fetched number */ seq->is_called = iscalled; seq->log_cnt = 0; ... /* 释放buffer锁以及表锁 */ UnlockReleaseBuffer(buf); relation_close(seqrel, NoLock); }
复制

可见,do_setval 会直接去设置 Sequence heap 表中的这一行元组,而非普通 heap 表中的删除 + 插入的方式来完成元组更新,对于 nextval 而言,也是类似的过程,只不过 last_value 的值需要计算得出,而非用户设置。

序列申请机制剖析

讲清楚 Sequence 对象在内核中的存在形式之后,就需要讲清楚一个序列值是如何发出的,即 nextval 方法。其在内核的具体实现在 sequence.c 中的 nextval_internal 函数,其最核心的功能,就是计算 last_value 以及 log_cnt

last_valuelog_cnt 的具体关系如下图:

页面值与wal关系

其中 log_cnt 是一个预留的申请次数。默认值为 32,由下面的宏定义决定:

/* * 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
复制

每当将 last_value 增加一个 increment 的长度时,log_cnt 就会递减 1。

页面值递增

log_cnt 为 0,或者发生 checkpoint 以后,就会触发一次 WAL 日志写入,按下面的公式设置 WAL 日志中的页面值,并重新将 log_cnt 设置为 SEQ_LOG_VALS

wal_value=last_value+incrementSEQ_LOG_VALSwal\_value = last\_value+increment*SEQ\_LOG\_VALS

通过这种方式,PostgreSQL 每次通过 nextval 修改页面中的 last_value 后,不需要每次都写入 WAL 日志。这意味着:如果 nextval 每次都需要修改页面值的话,这种优化将会使得写 WAL 的频率降低 32 倍。其代价就是,在发生 crash 前如果没有及时进行 checkpoint,那么会丢失一段序列。如下面所示:

postgres=# create sequence seq; CREATE SEQUENCE postgres=# select nextval('seq'); nextval --------- 1 (1 row) postgres=# select * from seq; last_value | log_cnt | is_called ------------+---------+----------- 1 | 32 | t (1 row) -- crash and restart postgres=# select * from seq; last_value | log_cnt | is_called ------------+---------+----------- 33 | 0 | t (1 row) postgres=# select nextval('seq'); nextval --------- 34 (1 row)
复制

显然,crash 以后,Sequence 对象产生了 2-33 这段空洞,但这个代价是可以被接受的,因为 Sequence 并没有违背唯一性原则。同时,在特定场景下极大地降低了写 WAL 的频率。

Sequence 缓存机制

通过上述描述,不难发现 Sequence 每次发生序列申请,都需要通过加入 buffer 锁的方式来修改页面,这意味着 Sequence 的并发性能是比较差的。

针对这个问题,PostgreSQL 使用对 Sequence 使用了 Session Cache 来提前缓存一段序列,来提高并发性能。如下图所示:

Session Cache

Sequence Session Cache 的实现是一个 entry 数量固定为 16 的哈希表,以 Sequence 的 OID 为 key 去检索已经缓存好的 Sequence 序列,其缓存的 value 结构如下:

typedef struct SeqTableData { Oid relid; /* Sequence OID(hash key) */ int64 last; /* value last returned by nextval */ int64 cached; /* last value already cached for nextval */ int64 increment; /* copy of sequence's increment field */ } SeqTableData;
复制

其中 last 即为 Sequence 在 Session 中的当前值,即 current_value,cached 为 Sequence 在 Session 中的缓存值,即 cached_value,increment 记录了步长,有了这三个值即可满足 Sequence 缓存的基本条件。

对于 Sequence Session Cache 与页面值之间的关系,如下图所示:

cache与页面关系

类似于 log_cntcache_cnt 即为用户在定义 Sequence 时,设置的 Cache 大小,最小为 1。只有当 cache domain 中的序列用完以后,才会去对 buffer 加锁,修改页中的 Sequence 页面值。调整过程如下所示:

cache申请

例如,如果 CACHE 设置的值为 20,那么当 cache 使用完以后,就会尝试对 buffer 加锁来调整页面值,并重新申请 20 个 increment 至 cache 中。对于上图而言,有如下关系:

cached_value=NEW current_valuecached\_value = NEW\ current\_value

NEW current_value+20×INC=NEW cached_valueNEW\ current\_value+20\times INC=NEW\ cached\_value

NEW last_value=NEW cached_valueNEW\ last\_value = NEW\ cached\_value

在 Sequence Session Cache 的加持下,nextval 方法的并发性能得到了极大的提升,以下是通过 pgbench 进行压测的结果对比。

性能对比

总结

Sequence 在 PostgreSQL 中是一类特殊的表级对象,提供了简单而又丰富的 SQL 接口,使得用户可以更加方便的创建、使用定制化的序列对象。不仅如此,Sequence 在内核中也具有丰富的组合使用场景,其使用场景也得到了极大地扩展。

本文详细介绍了 Sequence 对象在 PostgreSQL 内核中的具体设计,从对象的元数据描述、对象的数据描述出发,介绍了 Sequence 对象的组成。本文随后介绍了 Sequence 最为核心的 SQL 接口——nextval,从 nextval 的序列值计算、原地更新、降低 WAL 日志写入三个方面进行了详细阐述。最后,本文介绍了 Sequence Session Cache 的相关原理,描述了引入 Cache 以后,序列值在 Cache 中,以及页面中的计算方法以及对齐关系,并对比了引入 Cache 前后,nextval 方法在单序列和多序列并发场景下的对比情况。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

文章被以下合辑收录

评论