PostgreSQL的序列奇快无比的秘密

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 1. 引言 曾经有篇流传较广的文章Don’t Assume PostgreSQL is Slow 展示了PostgreSQL生成序列的速度不亚于redis的INCRs。而在此之前我就曾做过相关的测试(参考PostgreSQL的序列的性能验证),发现PG生成序列的速度远高于同类的关系数据库。根

1. 引言

曾经有篇流传较广的文章 Don’t Assume PostgreSQL is Slow 展示了PostgreSQL生成序列的速度不亚于redis的INCR s。而在此之前我就曾做过相关的测试(参考 PostgreSQL的序列的性能验证),发现PG生成序列的速度远高于同类的关系数据库。 根据 PostgreSQL的序列的性能验证 中测试结果,在没有启用序列cache的情况下, PG的每次调用nextval('seq1')的额外时间消耗大概是0.3us,也就是333w/s,所以即使做批量数据加载也不用担心序列拖后腿;而Oracle的nocache序列生成速度大概只有5w/s,当Oracle序列 cache了50以上时,速度才开始接近pg。
这个结果很惊人,但细一想,PG快得有点离谱。为什么这么说?因为当时测试的select nextval('seq1')在4核虚机上达到了7w/s的qps,而那个测试环境估计支撑不了这么高的iops,所以猜测PG一定对序列做了某种优化而不是每次刷盘。

2. 代码分析

关键代码见src/backend/commands/sequence.c的nextval_internal()函数,有个叫SEQ_LOG_VALS的常量,控制PG每产生32个序列值才记一次WAL。这相当于PG对序列做了全局缓存,而PG的create sequence语法上的cache是指每个进程(也就是连接)的本地cache。由于全局缓存优化的已经足够好了,所以一般不需要再启用本地cache。

src/backend/commands/sequence.c

点击(此处)折叠或打开

  1. fetch = cache = seq->cache_value;
  2. log = seq->log_cnt;
  3. ...
  4. /*
  5. * Decide whether we should emit a WAL log record. If so, force up the
  6. * fetch count to grab SEQ_LOG_VALS more values than we actually need to
  7. * cache. (These will then be usable without logging.)
  8. *
  9. * If this is the first nextval after a checkpoint, we must force a new
  10. * WAL record to be written anyway, else replay starting from the
  11. * checkpoint would fail to advance the sequence past the logged values.
  12. * In this case we may as well fetch extra values.
  13. */
  14. if (log < fetch || !seq->is_called)//此处fetch值为1.每次调nextval()log_cnt会递减,减到0时设置logit标志位
  15. {
  16. /* forced log to satisfy local demand for values */
  17. fetch = log = fetch + SEQ_LOG_VALS;
  18. logit = true;
  19. }
  20. else
  21. {
  22. XLogRecPtr redoptr = GetRedoRecPtr();

  23. if (PageGetLSN(page) <= redoptr)
  24. {
  25. /* last update of seq was before checkpoint */
  26. fetch = log = fetch + SEQ_LOG_VALS;
  27. logit = true;
  28. }
  29. }
  30. ...
  31. if (logit && RelationNeedsWAL(seqrel))
  32. {
  33. xl_seq_rec xlrec;
  34. XLogRecPtr recptr;

  35. /*
  36. * We don't log the current state of the tuple, but rather the state
  37. * as it would appear after "log" more fetches. This lets us skip
  38. * that many future WAL records, at the cost that we lose those
  39. * sequence values if we crash.
  40. */
  41. XLogBeginInsert();
  42. XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT);

  43. /* set values that will be saved in xlog */
  44. seq->last_value = next;//WAL中记录的last_value是下一轮的序列值,所以pg crash再通过WAL恢复后,新产生的序列会跳过几个值
  45. seq->is_called = true;
  46. seq->log_cnt = 0;

  47. xlrec.node = seqrel->rd_node;

  48. XLogRegisterData((char *) &xlrec, sizeof(xl_seq_rec));
  49. XLogRegisterData((char *) seqtuple.t_data, seqtuple.t_len);

  50. recptr = XLogInsert(RM_SEQ_ID, XLOG_SEQ_LOG);

  51. PageSetLSN(page, recptr);
  52. }

3. 实测验证

3.1 WAL写入时机

PG通过内部log_cnt计数器控制是否要记录序列更新的WAL,新建的序列,计数器初始值为0。
  1. postgres=# create sequence seq1;
  2. CREATE SEQUENCE
  3. postgres=# \d seq1
  4. Sequence "public.seq1"
  5. Column | Type | Value
  6. ---------------+---------+---------------------
  7. sequence_name | name | seq1
  8. last_value | bigint | 1
  9. start_value | bigint | 1
  10. increment_by | bigint | 1
  11. max_value | bigint | 9223372036854775807
  12. min_value | bigint | 1
  13. cache_value | bigint | 1
  14. log_cnt | bigint | 0
  15. is_cycled | boolean | f
  16. is_called | boolean | f

取得第一个序列值后,log_cnt变成32。
  1. postgres=# select nextval('seq1');
  2. nextval
  3. ---------
  4. 1
  5. (1 row)

  6. postgres=# \d seq1
  7. Sequence "public.seq1"
  8. Column | Type | Value
  9. ---------------+---------+---------------------
  10. sequence_name | name | seq1
  11. last_value | bigint | 1
  12. start_value | bigint | 1
  13. increment_by | bigint | 1
  14. max_value | bigint | 9223372036854775807
  15. min_value | bigint | 1
  16. cache_value | bigint | 1
  17. log_cnt | bigint | 32
  18. is_cycled | boolean | f
  19. is_called | boolean | t

这个过程中,通过strace监视"wal writer process"进程,可以发现发生了WAL写入和刷盘。
  1. [root@localhost ~]# strace -efsync,write,fdatasync -p 2997
  2. Process 2997 attached
  3. --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=3209, si_uid=1001} ---
  4. write(11, "\0", 1) = 1
  5. write(3, "\207\320\5\0\1\0\0\0\0\300\273\t\0\0\0\0I\10\0\0\0\0\0\0\n\0\0004-\0\0\0"..., 8192) = 8192
  6. fdatasync(3) = 0

以后每次获取序列值,log_cnt会减1,但只有log_cnt减到0,并从0重新跳到32的时候,strace中才能看到WAL写入。
  1. postgres=# select nextval('seq1');
  2. nextval
  3. ---------
  4. 2
  5. (1 row)

  6. postgres=# \d seq1
  7. Sequence "public.seq1"
  8. Column | Type | Value
  9. ---------------+---------+---------------------
  10. sequence_name | name | seq1
  11. last_value | bigint | 2
  12. start_value | bigint | 1
  13. increment_by | bigint | 1
  14. max_value | bigint | 9223372036854775807
  15. min_value | bigint | 1
  16. cache_value | bigint | 1
  17. log_cnt | bigint | 31
  18. is_cycled | boolean | f
  19. is_called | boolean | t


3.2 PG crash后的序列值

PG在记录序列的WAL时,记录的是当前值+32。所以如果PG crash再恢复后,将跳过一部分从未使用的序列值。这样做避免了产生重复序列的可能,但不能保证序列的连续,这是优化WAL写入而付出的必要代价。
下面是使用kill -9杀PG进程的情况。

点击(此处)折叠或打开

  1. postgres=# select nextval('seq1');
  2. nextval
  3. ---------
  4. 20
  5. (1 row)

  6. postgres=# \d seq1
  7. Sequence "public.seq1"
  8. Column | Type | Value
  9. ---------------+---------+---------------------
  10. sequence_name | name | seq1
  11. last_value | bigint | 20
  12. start_value | bigint | 1
  13. increment_by | bigint | 1
  14. max_value | bigint | 9223372036854775807
  15. min_value | bigint | 1
  16. cache_value | bigint | 1
  17. log_cnt | bigint | 28
  18. is_cycled | boolean | f
  19. is_called | boolean | t

  20. postgres=# select nextval('seq1');
  21. server closed the connection unexpectedly
  22. This probably means the server terminated abnormally
  23. before or while processing the request.
  24. The connection to the server was lost. Attempting reset: Succeeded.
  25. postgres=# select nextval('seq1');
  26. nextval
  27. ---------
  28. 49
  29. (1 row)
不过,正常关闭或重启PG是不会出现这种问题的。
相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
6月前
|
缓存 关系型数据库 MySQL
postgresql|数据库|序列Sequence的创建和管理
postgresql|数据库|序列Sequence的创建和管理
126 0
|
关系型数据库 PostgreSQL
『PostgreSQL』PGSQL手动创建Sequence序列
📣读完这篇文章里你能收获到 - 在PostgreSQL中对Sequence的管理及使用
704 0
『PostgreSQL』PGSQL手动创建Sequence序列
|
存储 关系型数据库 数据库
分布式 PostgreSQL 集群(Citus)官方示例 - 时间序列数据
分布式 PostgreSQL 集群(Citus)官方示例 - 时间序列数据
259 0
分布式 PostgreSQL 集群(Citus)官方示例 - 时间序列数据
|
SQL 关系型数据库 PostgreSQL
PostgreSQL 10.1 手册_部分 II. SQL 语言_第 9 章 函数和操作符_9.16. 序列操作函数
9.16. 序列操作函数 本节描述用于操作序列对象的函数,序列对象也被称为序列生成器或者就是序列。序列对象都是用CREATE SEQUENCE创建的特殊的单行表。序列对象通常用于为表的行生成唯一的标识符。
1226 0
|
存储 关系型数据库 PostgreSQL
|
关系型数据库 测试技术 PostgreSQL
PostgreSQL的序列的性能验证
前段时间有同事咨询PostgreSQL相关的问题,发现他们用了一个自动生成的32字节的字符串作为唯一键,而这张表的数据量相当大,建议他们改用序列,可减少存储空间。但用序列有一点不一样,就是序列必须顺序产生,那么高并发访问时会不会成为性能瓶颈呢?下面做个测试验证一下。
982 0
|
SQL Cloud Native 关系型数据库
ADBPG(AnalyticDB for PostgreSQL)是阿里云提供的一种云原生的大数据分析型数据库
ADBPG(AnalyticDB for PostgreSQL)是阿里云提供的一种云原生的大数据分析型数据库
1258 1