Here are some tips for using omnigraffle

Draw curve

  1. select pen tool: draw custome sharps
  2. Click one point on the canvas, then click another point — a straight line will appear.
  3. Select the line → double-click the middle of the line → drag the new point: a curved effect will appear.

Flexible Control Over Line Arrows

When the endpoint of a line (especially an endpoint with an arrow) is connected to a shape, the default mode is “Connected Line” (automatic snapping). In this mode, the endpoint is “locked” to the shape’s boundary or center, and cannot be freely dragged.

Solution

  1. Turn on Magnets
    • Select the target shape.
    • Menu BarEditMagnetsShow Magnets
    • You will see small red dots (magnets) appear around the shape.
    • These are the connectable positions.
    • Drag the endpoint of a line near the red dot, and it will snap/attach to that point.

What is Magnet?

Magnet (magnetic points) are those positions on the shape (Shape) that can attract the end points of the lines.
When you draw a line with the Line Tool, endpoints that are close to these magnetic points will “snap” to it.

Examples

page

配置checkpoint

checkpoint持续时间(更准确地说,是将脏缓冲区写入磁盘所需的时间)由参数 checkpoint_completion_target 决定。该参数的值表示checkpoint 期间的 I/O 分布目标,是一个比例。避免将该参数设置为 1:否则可能会导致下一个checkpoint启动时,上一个checkpoint尚未完成。虽然不会发生灾难性的后果(因为同一时间只能执行一个checkpoint),但正常运行可能仍会受到干扰。

在配置其他参数时,我们可以采用以下方法。首先,确定在两个相邻checkpoint之间应存储的 WAL 文件的合理体积。这个体积越大,系统开销就越小,但它仍然会受到可用磁盘空间和可接受的恢复时间的限制。

为了估算在正常负载下生成这一体积所需的时间,你需要记录初始的 insert LSN,并定期检查它与当前 insert 位置之间的差值

我们将前面计算出的数值视为checkpoint之间的典型间隔,因此将其用作 checkpoint_timeout 参数的值。默认设置往往偏小,通常会将其增加,例如设置为 30 分钟。

但也很可能(甚至可以说是很常见)负载会在某些时候升高,从而导致在这个时间间隔内生成的 WAL 文件体积过大。在这种情况下,必须更频繁地执行checkpoint。为了设置这样的触发机制,我们通过 max_wal_size 参数来限制恢复时所需的 WAL 文件总量。当超过这个阈值时,服务器会额外执行一次checkpoint。

恢复所需的 WAL 文件包含了上一个已完成checkpoint和当前尚未完成检查点的所有记录。因此,为了估算这些文件的总体积,应将计算出的检查点之间的 WAL 大小乘以
1 + checkpoint_completion_target。

在 PostgreSQL 11 版本之前,系统会保留两个已完成checkpoint所对应的 WAL 文件,因此估算恢复所需 WAL 总体积的倍数是:2 + checkpoint_completion_target

按照这种方式,大多数checkpoint会按计划执行,即按照 checkpoint_timeout 设置的时间间隔执行一次;但如果系统负载增加,导致生成的 WAL 文件大小超过 max_wal_size 的设定值,就会提前触发一次checkpoint。

系统还会定期将实际进度与预期数值进行比较,以监控写入进度是否达标。

实际进度由已处理的缓存页所占的比例来定义。

按时间计算的预期进度是指已过去的时间占比,其前提假设是checkpoint必须在 checkpoint_timeout × checkpoint_completion_target 的时间内完成。

按大小计算的预期进度是指已写入的 WAL 文件所占的比例,其总量根据 max_wal_size × checkpoint_completion_target 来估算。

如果脏页提前写入磁盘,checkpointer 进程会暂停一段时间;如果在时间或数据大小的任何一个参数上出现延迟,它会尽快赶上进度。由于同时考虑了时间和数据大小,PostgreSQL 能够用同一套机制来管理定时checkpoint和按需checkpoint。

一旦checkpoint完成,不再需要用于恢复的 WAL 文件将被删除;不过,系统会保留若干文件(总大小不超过 min_wal_size),用于重复利用,并通过重命名的方式保留。

这种重命名机制减少了频繁创建和删除文件所带来的开销;如果你不需要这个功能,可以通过参数 wal_recycle 将其关闭。

下图展示了在正常情况下,磁盘上 WAL 文件大小的变化趋势

wal

需要注意的是,磁盘上实际的 WAL 文件大小可能会超过 max_wal_size 的设定值,原因包括:

  • max_wal_size 参数只是一个期望目标值,而不是严格限制。如果负载突然上升,写入速度可能会落后于计划,导致 WAL 文件积压。
  • 尚未被复制或归档的 WAL 文件,服务器无权删除。如果启用了流复制或持续归档功能,必须持续监控这些机制,否则非常容易导致磁盘空间耗尽。
  • 你可以通过配置 wal_keep_size 参数来预留一定的磁盘空间用于存储 WAL 文件,以避免这些问题带来的风险。

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

当数据库服务正常运行时,wal文件持续不断的写入磁盘。这种写入时顺序的(sequential):几乎没有随机访问,所以即便时hdd硬盘也能处理这样的任务。由于这种负载和典型的数据文件访问非常不一样,值得为WAL文件设置单独的物理存储,并通过符号链接替换PGDATA/PG_WAL目录,该目录链接到mount的文件系统中的目录。

在某些情况下,需要同时写和读取WAL文件。第一种情况是崩溃恢复。第二个是流复制。 Walsender流程直接从文件中读取WALENTRIES。因此,如果必需的页面仍位于主服务器的OS缓冲区中(不在shared_buffer中),replica未接收WAL条目,则必须从磁盘中读取数据。但是访问仍然是顺序而不是随机的。

wal日志条目写入有以下两种模式:

  • 同步模式:在事务提交之前,必须把所有相关的 WAL 记录写入磁盘,否则不允许继续执行后续操作。
  • 异步模式:事务提交会立刻返回成功,相关的 WAL 记录则由后台进程稍后再写入磁盘。

当前使用的模式由参数:synchronous_commit 确定

同步模式:为了可靠地记录一次提交,仅仅将WAL条目传递给操作系统是不够的;你必须确保磁盘同步已经成功完成。由于同步涉及实际的I/O操作(这相当慢),因此最好尽可能少地执行它。

为此,完成事务并将WAL条目写入磁盘的后端可以进行一次小的暂停,该暂停由commit_delay参数定义。但是,只有当系统中至少有commit_siblings个活跃事务时,这种情况才会发生:在这次暂停期间,其中一些事务可能会完成,而服务器将设法一次性同步所有WAL条目。这很像为某人赶进来而按住电梯门。

默认情况下,没有暂停。仅针对执行大量短时OLTP事务的数据库系统修改commit_delay参数是有意义的。

在可能出现的暂停之后,完成事务的进程会将所有累积的 WAL 条目刷新到磁盘并执行同步操作(关键是要保存提交记录以及与该事务相关的所有前置记录;至于其他记录,则只是顺便写入,因为它们不会增加额外开销)。

从这一刻起,ACID 的持久性要求便得到保证——事务被认为已经可靠提交。这就是为什么默认使用同步模式的原因。

同步提交的缺点在于延迟更长(在同步完成之前,COMMIT 命令不会返回控制权),并且系统吞吐量较低,尤其对于 OLTP loads。

异步模式:要启用异步模式,必须关闭 synchronous_commit 参数。
在异步模式下,WAL 条目由 walwriter 进程写入磁盘,该进程在“工作—休眠”之间交替运行。休眠时长由 wal_writer_delay 参数决定(默认 200ms)。

当 walwriter 从休眠中唤醒时,它会检查缓存中是否存在新的、已经完全填满的 WAL 页面。如果存在,就将这些页面写入磁盘,同时跳过当前未写满的页面;否则,它会写入当前半空的页面,因为既然已经被唤醒了。

这种算法的目的在于避免同一个页面被多次刷盘,这在数据变更频繁的负载下能带来显著的性能提升。

虽然 WAL 缓存采用环形缓冲区(ring buffer)的形式,但 walwriter 在到达缓存的最后一页时会停止;在经过一次休眠后,下一轮写入循环会从缓存的第一页重新开始。因此,在最坏的情况下,walwriter 可能需要 三次循环才能处理某个特定的 WAL 记录:

第一次,它会写出缓存尾部的所有已填满页面;(当前位置之后的所有满块)
第二次,它回到开头;(当前位置之前的所有满块)
第三次,才会处理包含目标记录的那个未填满页面。
不过,在大多数情况下,只需要 一到两次循环 就能完成。

每当写入的数据量达到 wal_writer_flush_after 时,就会执行一次同步操作;在写入循环结束时,也会再次进行同步。

与同步提交相比,异步提交更快,因为它不需要等待物理写入磁盘完成。但可靠性有所下降:在发生故障前的 3 × wal_writer_delay 时间内提交的数据可能会丢失(默认值为 0.6 秒)

在实际应用中,这两种模式是互补的。

  • 同步模式 下,与长事务相关的 WAL 条目仍然可以 异步写入,以释放 WAL 缓冲区。
  • 反之,即使在 异步模式 下,如果某个 WAL 条目所在的页即将被 从缓冲区淘汰,该条目也会被 立即刷盘,否则系统无法继续正常操作。
    在大多数情况下,系统设计者必须在 性能和持久性之间做出权衡。

synchronous_commit 参数也可以针对特定事务进行设置。如果能够在应用层将所有事务分类为 绝对关键(如处理财务数据)或 非关键,就可以在只冒非关键事务丢失风险的前提下,提升整体性能

为了了解 异步提交 可能带来的性能提升,我们可以通过 pgbench 测试,比较两种模式下的 延迟(latency)和吞吐量(throughput)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
postgres@lavm-bar1guved6:/root$ pgbench -i test
dropping old tables...
NOTICE: table "pgbench_accounts" does not exist, skipping
NOTICE: table "pgbench_branches" does not exist, skipping
NOTICE: table "pgbench_history" does not exist, skipping
NOTICE: table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 1.54 s (drop tables 0.11 s, create tables 0.49 s, client-side generate 0.54 s, vacuum 0.18 s, primary keys 0.23 s).

postgres@lavm-bar1guved6:/root$ pgbench -T 30 test
pgbench (19devel)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 3522
number of failed transactions: 0 (0.000%)
latency average = <strong>8.518</strong> ms
initial connection time = 4.025 ms
tps = <strong>117.400485</strong> (without initial connection time)

修改参数后跑异步模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
postgres@lavm-bar1guved6:/root$ psql test
psql (19devel)
Type "help" for help.

test=# ALTER SYSTEM SET synchronous_commit = off;
ALTER SYSTEM
test=# SELECT pg_reload_conf();
pg_reload_conf
----------------
t
(1 row)

postgres@lavm-bar1guved6:/root$ pgbench -T 30 test
pgbench (19devel)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 9510
number of failed transactions: 0 (0.000%)
latency average = <strong>3.154</strong> ms
initial connection time = 4.383 ms
tps = <strong>317.038330</strong> (without initial connection time)

异步模式 下,这个简单的基准测试显示出 显著更低的延迟(latency)和更高的吞吐量(tps)。当然,每个具体系统的数值会根据当前负载有所不同,但可以清楚地看出,对于 短事务,性能提升是相当明显的。

恢复参数:

1
2
3
4
5
6
7
8
9
10
11
postgres@lavm-bar1guved6:/root$ psql test
psql (19devel)
Type "help" for help.

test=# ALTER SYSTEM RESET synchronous_commit;
ALTER SYSTEM
test=# SELECT pg_reload_conf();
pg_reload_conf
----------------
t
(1 row)

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

昨日和同事探讨了一下pg与mysql,发现对于mysql的innodb存储引擎了解甚少,正好周末,深入学习了解了一下,整理一下这两天学到的东西。

概念介绍

InnoDB:MySQL 的主要存储引擎
heap table:postgres数据库P表在物理上的存储(Heap File)

数据存储方式

innodb 底层存储

innodb是一个基于聚簇索引(Clustered Index)的存储引擎。

数据存储方式

  • 聚簇索引存储表数据
    • 表的主键索引就是数据本身的物理存储顺序。
    • 每个页(page)一般 16KB,页内数据按主键顺序排列。
    • 非主键列数据和行信息都在叶子节点。
  • 二级索引
    • 非主键索引存储的是主键值而不是行指针。
    • 查询非主键列时,需要先通过二级索引拿到主键,再去主键聚簇索引里查数据(回表,row lookup)。

页与数据组织

  • 数据按 B+ 树页组织。
  • 插入/更新可能导致页分裂。

postgres heap table

数据存储方式

  • 数据按插入顺序存放,没有聚簇索引。
  • 行标识符 TID(tuple id) 用于指向表的页和行。
  • 页(默认 8KB)内存储多行 tuple。
  • 没有聚簇索引,非索引扫描可能会更慢,但插入非常快。

索引

  • B+ 树、哈希、GIN、GiST 等索引都是附加结构,存储独立于表。
  • 回表操作是通过 TID 定位行。

对比

特性 InnoDB PostgreSQL 堆表
数据组织 聚簇索引(主键决定物理顺序) 堆表,顺序插入
主键访问 快(顺序扫描/范围查询) 需要 B+ 树索引或全表扫描
二级索引访问 回表(先索引后主键查行) 回表(索引 -> TID -> 行)
插入性能 如果主键顺序插入快,否则可能页分裂 快,顺序写入,几乎不分裂
更新/删除 支持 in-place 更新,但大行更新可能迁移 创建新行,旧行留存,需要 vacuum
MVCC Undo log + hidden columns xmin/xmax + heap tuple
表膨胀 自动管理页空间 容易膨胀,需要 vacuum
查询优化 聚簇索引优化范围查询 多依赖索引,或者全表扫描

因为pg的存储结构导致访问数据,多了许多随机io:从索引到数据文件。导致pg的查询性能不如innnodb

pg的一些优化

CLUSTER 命令(物理重排)

1
CLUSTER t USING idx_id;

把表物理顺序按照索引顺序重排 —— 效果类似 InnoDB 聚簇索引。
缺点:

  • 是离线操作;
  • 之后新插入的行会破坏顺序(除非周期性 recluster)

索引仅扫描(Index Only Scan)

如果查询的字段都在索引里且可见性检查通过(可见性 map 中标记为 all-visible),可以不访问堆表,直接从索引返回结果。

BRIN 索引

BRIN 全称是 Block Range INdex,是一种 非常轻量级的索引,设计理念与 B-Tree 不同:
不是存储每一行的索引它只存储堆表的一段范围(block range)内的最小值和最大值等摘要信息。每个 BRIN 索引条目覆盖 多个物理数据页(例如 128 个 8KB 页面 = 1MB 的行数据)。
通过这些范围信息,可以快速排除不可能匹配的块,再去 heap 查找具体 tuple。
简单理解:BRIN 是“粗粒度索引”,通过块范围(block range)而非单行建立索引
非常适合 大表 + 顺序或局部相关数据:大表 + 顺序或局部相关数据:

服务器启动时,第一个启动的进程是 postmaster(新版本为postgres)。postmaster 接着会生成 startup process(启动进程),startup process 负责在发生故障时进行数据恢复。

startup process 是一个短暂的、一次性的进程,它的主要职责是在数据库启动时执行崩溃恢复或归档恢复。它完成它的工作后,就会退出。

1
2
3
postgres@lavm-bar1guved6:/root$ pg_controldata -D /home/postgres/pgdata/ |grep state
Database cluster state: in production
postgres@lavm-bar1guved6:/root$

一个正常停止的服务器会处于“已关闭”(shut down)状态;而一个未运行的服务器却显示为“生产中”(in production)状态,则表明发生了故障。在这种情况下,启动进程(startup process)将自动从在同一个 pg_control 文件中找到的最新完成的检查点(checkpoint)**的起始 LSN 处开始进行恢复。

如果 PGDATA 目录中包含与备份相关的 backup_label 文件,则起始 LSN 位置会从该文件中获取。

在启动过程中,系统会从指定位置开始,逐一读取WAL(Write-Ahead Log,预写式日志)条目。如果数据页的 LSN(Log Sequence Number,日志序列号)小于当前读取到的 WAL 条目的 LSN,系统会将该 WAL 条目应用到数据页上。如果数据页的 LSN 已经大于 WAL 条目的 LSN,则不应应用该 WAL 条目;事实上,也绝不能应用,因为 WAL 条目被设计为必须严格按顺序重放。

然而,有些 WAL 条目是Full Page Image(FPI)。这类条目可以应用于页面的任何状态,因为它们会完全覆盖页面内容,无论页面原先是什么状态都不重要。因此,这种修改是幂等的(idempotent)——多次应用不会改变结果另一个幂等操作的例子是注册事务状态的变更:每个事务的状态在 CLOG(事务提交日志)中是通过设置特定位来表示的,这种设置不依赖于原来的位值。因此,不需要在 CLOG 页面中记录最近变更的 LSN(日志序列号),因为日志重放时只要设置一次这些位就够了,重复设置也不会有副作用最后,系统会执行一次 checkpoint(检查点),将恢复后的所有修改持久化到磁盘,此时 启动进程(startup process) 的任务就完成了。

WAL 日志条目会被应用到缓冲池(buffer cache)中的页面上,就像正常运行时对数据页的普通修改一样。

文件的恢复也遵循类似方式:例如,若某条 WAL 记录表明某个文件应该存在,但实际却缺失,系统就会重新创建这个文件。
一旦恢复完成,所有 unlogged relations会被它们对应的 初始化副本(init fork) 覆盖。

最后,系统会执行一次 checkpoint,将恢复后的所有修改持久化到磁盘,此时 启动进程(startup process) 的任务就完成了
在其经典形式中,恢复过程包含两个阶段:

  • roll-forward阶段:重放 WAL 日志,重复执行在崩溃时丢失的操作;
  • roll-back阶段:服务器中止那些在故障发生时尚未提交的事务。
    在 PostgreSQL 中,向后回滚是不需要的。恢复完成后,CLOG(事务状态日志)中对未完成事务既没有提交(commit)标记,也没有中止(abort)标记(这在技术上表示该事务处于活动状态),但因为可以确定该事务已经不再运行,所以系统会将其视为已中止(aborted)。

我们可以通过强制服务器以“立即模式”(immediate mode)停止来模拟故障:

1
2
3
4
5
postgres@lavm-bar1guved6:/root$ pg_ctl stop -m immediate
waiting for server to shut down.... done
server stopped
postgres@lavm-bar1guved6:/root$ pg_controldata -D /home/postgres/pgdata/ |grep state
Database cluster state: in production

当我们启动服务器时,启动进程会检测到之前发生了故障,因而进入恢复模式:

1
2
3
4
5
6
7
8
9
10
11
2025-08-04 10:23:31.860 CST [487414] LOG:  starting PostgreSQL 19devel on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
2025-08-04 10:23:31.861 CST [487414] LOG: listening on IPv4 address "127.0.0.1", port 5432
2025-08-04 10:23:31.878 CST [487414] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432"
2025-08-04 10:23:31.920 CST [487420] LOG: database system was interrupted; last known up at 2025-08-01 09:36:11 CST
2025-08-04 10:23:32.098 CST [487420] LOG: database system was not properly shut down; automatic recovery in progress
2025-08-04 10:23:32.125 CST [487420] LOG: redo starts at 0/01B75168
2025-08-04 10:23:32.125 CST [487420] LOG: invalid record length at 0/01B752A8: expected at least 24, got 0
2025-08-04 10:23:32.125 CST [487420] LOG: redo done at 0/01B75270 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.01 s
2025-08-04 10:23:32.132 CST [487418] LOG: checkpoint starting: end-of-recovery fast wait
2025-08-04 10:23:32.152 CST [487418] LOG: checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.006 s, sync=0.005 s, total=0.022 s; sync files=2, longest=0.005 s, average=0.003 s; distance=0 kB, estimate=0 kB; lsn=0/01B752A8, redo lsn=0/01B752A8
2025-08-04 10:23:32.155 CST [487414] LOG: database system is ready to accept connections

如果服务器正在正常关闭,postmaster 会先断开所有客户端连接,然后执行最后一次检查点操作,将所有脏页(未写入磁盘的修改页面)刷写到磁盘上。

看当前的WAL位置

1
2
3
4
5
test=# SELECT pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
---------------------------
0/01B75358
(1 row)

我们正常停止服务

1
2
3
postgres@lavm-bar1guved6:~$ pg_ctl stop
waiting for server to shut down.... done
server stopped

现在的数据库状态:

1
2
postgres@lavm-bar1guved6:~$ pg_controldata -D /home/postgres/pgdata/ |grep state
Database cluster state: shut down

在WAL的末尾,我们看到了表示最后一次checkpoint的CHECKPOINT_SHUTDOWN 条目

1
2
3
4
postgres@lavm-bar1guved6:~$ pg_waldump  -p /home/postgres/pgdata/pg_wal -s 0/01B75358
rmgr: XLOG len (rec/tot): 114/ 114, tx: 0, lsn: 0/01B75358, prev 0/01B75320, desc: CHECKPOINT_SHUTDOWN redo 0/01B75358; tli 1; prev tli 1; fpw true; wal_level replica; xid 0:758; oid 24576; multi 1; offset 0; oldest xid 746 in DB 1; oldest multi 1 in DB 1; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown
pg_waldump: error: error in WAL record at 0/01B75358: invalid record length at 0/01B753D0: expected at least 24, got 0
postgres@lavm-bar1guved6:~$

最新的 pg_waldump 消息显示该工具已读取 WAL到末尾。

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

一 相关结构体说明

1
2
3
4
5
6
7
8
9
10
typedef struct LWLock
{
uint16 tranche; /* tranche ID */
pg_atomic_uint32 state; /* state of exclusive/nonexclusive lockers */
proclist_head waiters; /* list of waiting PGPROCs */
#ifdef LOCK_DEBUG
pg_atomic_uint32 nwaiters; /* number of waiters */
struct PGPROC *owner; /* last exclusive owner of the lock */
#endif
} LWLock;
  • tranche
    每个 LWLock 都属于某个 tranche,tranche 是一个 int 类型的 ID,代表这把锁的用途

  • state
    bit0~23:共享锁状态,实际最多用bit0~17,因为backend最多18位
    bit24: 排他锁状态
    bit28:用于锁定等待队列:waiters
    bit29: 是否允许唤醒等待队列里面的进程,初始状态:LW_FLAG_RELEASE_OK
    bit30: LW_FLAG_HAS_WAITERS :是否有waiter

  • waiters
    等待队列

二 Interface说明

2.1 LWLockAcquire

加锁,失败进入等待队列,直接加上锁的情况,可能会造成等待队列无效唤醒

acquire a lightweight lock in the specified mode
Side effect: cancel/die interrupts are held off until lock release.

1
2
3
4
5
6
7
8
9
10
11
12
if (mode == LW_EXCLUSIVE)
{
lock_free = (old_state & LW_LOCK_MASK) == 0;
if (lock_free)
desired_state += LW_VAL_EXCLUSIVE;
}
else
{
lock_free = (old_state & LW_VAL_EXCLUSIVE) == 0;
if (lock_free)
desired_state += LW_VAL_SHARED;
}

按位判断前25位即可知道此LWLock是否被进程所占

2.2 LWLockRelease

进行唤醒条件:

  • oldstate & LW_FLAG_HAS_WAITERS:等待队列非空。这个条件确认有进程在等待锁。
  • oldstate & LW_FLAG_RELEASE_OK: 允许唤醒。这个标志表示系统在上次唤醒后,已经将再次唤醒其他等待者的权限交给了被唤醒的进程。
  • (oldstate & LW_LOCK_MASK) == 0:锁已空闲。这个条件确认锁当前没有被任何进程占用(无论是独占模式还是共享模式,计数都为零)

全局hash

LockMethodLockHash:存储LOCK
LockMethodProcLockHash:存储PROCLOCK
LockMethodLocalHash:存储LOCALLOCK

同一个LOCK资源对象可以被多个不同的 PROCLOCK 持有,而这些 PROCLOCK 又分别属于不同的进程。

LOCKTAG

1
2
3
4
5
6
7
8
9
typedef struct LOCKTAG
{
uint32 locktag_field1; /* a 32-bit ID field */
uint32 locktag_field2; /* a 32-bit ID field */
uint32 locktag_field3; /* a 32-bit ID field */
uint16 locktag_field4; /* a 16-bit ID field */
uint8 locktag_type; /* see enum LockTagType */
uint8 locktag_lockmethodid; /* lockmethod indicator */
} LOCKTAG;
  • locktag_type
    标识这个锁是针对哪类资源的。每种 LockTagType 决定了 LOCKTAG 里后面几个字段(locktag_field1 ~ locktag_field4)是怎么解释的,比如:
    LOCKTAG_TRANSACTION:xid
    LOCKTAG_RELATION:dbOid + relOid
    LOCKTAG_TUPLE:dbOid + relOid + blockNum + offNum
  • locktag_lockmethodid
    • DEFAULT_LOCKMETHOD(id = DEFAULT_LOCKMETHOD)
      绝大多数用户可见的锁(relation, tuple, transactionid 等)都走它。
    • USER_LOCKMETHOD(id = USER_LOCKMETHOD)
      提供给 pg_advisory_lock() 一类的 advisory lock,用于用户自定义锁。

LOCK

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct LOCK
{
LOCKTAG tag;
LOCKMASK grantMask;
LOCKMASK waitMask;
dlist_head procLocks;
dclist_head waitProcs;
int requested[MAX_LOCKMODES];
int nRequested;
int granted[MAX_LOCKMODES];
int nGranted;
} LOCK;
  • waitProcs
    等待本LOCK的PGPROC链表
  • procLocks
    等待本LOCK的PROCLOCK链表
  • waitMask
    该资源上等待的锁类型
  • grantMask
    该资源上已经授予的锁类型

LOCKMODE

每种资源支持8种锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define NoLock					0
#define AccessShareLock 1 /* SELECT */
#define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL), ANALYZE, CREATE
* INDEX CONCURRENTLY */
#define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW
* SHARE */
#define ExclusiveLock 7 /* blocks ROW SHARE/SELECT...FOR UPDATE */
#define AccessExclusiveLock 8 /* ALTER TABLE, DROP TABLE, VACUUM FULL,
* and unqualified LOCK TABLE */

#define MaxLockMode 8 /* highest standard lock mode */

冲突矩阵

Requested Lock Mode ACCESS SHARE ROW SHARE ROW EXCL. SHARE UPDATE EXCL. SHARE SHARE ROW EXCL. EXCL. ACCESS EXCL.
ACCESS SHARE X
ROW SHARE X X
ROW EXCL. X X X X
SHARE UPDATE EXCL. X X X X X
SHARE X X X X X
SHARE ROW EXCL. X X X X X X
EXCL. X X X X X X X
ACCESS EXCL. X X X X X X X X

参考链接

postgres官方文档

如果后台进程(backend)需要从缓冲区中驱逐一个脏页(dirty page),它必须将这个页面写入磁盘。这种情况是不希望发生的,因为它会导致等待(例如 I/O 阻塞)——更好的方式是在后台异步地执行写入操作。

这一工作部分由 checkpointer 进程完成,但这仍然不够。因此,PostgreSQL 还引入了另一个名为 bgwriter(后台写入进程)的进程,专门用于后台写盘操作。

bgwriter 和驱逐(eviction)使用相同的缓冲区遍历算法,但有两个关键区别:

bgwriter 使用自己独立的时钟指针(clock hand),该指针从不会落后于驱逐的指针,通常还会超过它;
在遍历缓冲区时,bgwriter 不会降低页面的 usage count(使用计数)。
当一个缓冲页未被固定(unpinned)且其 usage count 为 0 时,如果它是脏的,bgwriter 就会将其刷新到磁盘。换句话说,bgwriter 在驱逐操作发生之前运行,主动地将那些很可能即将被驱逐的页面提前写入磁盘。

这样做的好处是:被选中驱逐的缓冲页很可能已经是干净的(clean),从而提高了驱逐操作的效率,避免了后台进程被迫同步写盘的代价。

总结

checkpointer 是“周期性清理工”,而 bgwriter 是“持续扫地工”。

checkpointer 负责最终的数据落盘一致性,而 bgwriter 提前清理“潜在垃圾”,让后台线程少“踩雷”。二者配合,保障了 PostgreSQL 的高并发性能和写入平滑性。

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals

CPU

top

1
top -H -p <PID>

只显示指定进程 的所有线程,并实时显示它们的 CPU、内存等使用情况。

1
pstack <PID>

perf

1
perf top -p <PID>

查看函数热点,采样一段时间

1
2
perf record -p <PID> -g -- sleep 10
perf report

strace

1
strace -ttT -p <PID>

火焰图

  • 安装perf

    1
    sudo apt-get install linux-tools-common linux-tools-$(uname -r)
  • 采样数据

    1
    sudo perf record -F 99 -p <PID> -g -- sleep 30

    -F 99 每秒采样 99 次
    -p 针对指定进程
    -g 采集调用栈(火焰图需要)
    – sleep 30 采样 30 秒

  • 生成调用栈

    1
    sudo perf script > out.perf
  • 下载 FlameGraph 工具

    1
    2
    git clone https://github.com/brendangregg/FlameGraph.git
    cd FlameGraph
  • 生成火焰图

    1
    2
    ./stackcollapse-perf.pl ../out.perf > out.folded
    ./flamegraph.pl out.folded > flamegraph.svg

IO

系统io

1
iostat -x 1

重点关注 await(平均等待时间)和 svctm(服务时间)。如果 await >> svctm,说明队列很长,设备忙不过来。

某个进程io

1
pidstat -d -p <PID> 1

Each page has a certain inner layout that usually consists of the following parts:
page header

  • page header
  • an array of item pointers
  • free space
  • items (row versions)
  • special space

Page结构示意图:

page.png

pageinspect extension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
test=# CREATE EXTENSION pageinspect;
CREATE EXTENSION

test=# select * from accounts;
id | client | amount
----+---------+--------
2 | bob | 100.00
3 | bob | 900.00
1 | alice | 800.00
4 | charlie | 100.00
(4 rows)

test=# SELECT lower, upper, special, pagesize FROM page_header(get_raw_page('accounts',0));
lower | upper | special | pagesize
-------+-------+---------+----------
40 | 8032 | 8192 | 8192
(1 row)

test=# select * FROM heap_page_items(get_raw_page('accounts',0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+--------------------------------------
1 | 8152 | 1 | 39 | 757 | 758 | 0 | (0,4) | 16387 | 1282 | 24 | | | \x010000000d616c6963650b0081e803
2 | 8112 | 1 | 37 | 757 | 802 | 0 | (0,5) | 16387 | 2306 | 24 | | | \x0200000009626f620b00816400
3 | 8072 | 1 | 37 | 757 | 802 | 0 | (0,6) | 16387 | 2306 | 24 | | | \x0300000009626f620b00818403
4 | 8032 | 1 | 39 | 758 | 802 | 0 | (0,7) | 49155 | 10498 | 24 | | | \x010000000d616c6963650b00812003
5 | 7992 | 1 | 37 | 802 | 0 | 0 | (0,5) | 32771 | 10754 | 24 | | | \x0200000009626f620b00816300
6 | 7952 | 1 | 37 | 802 | 0 | 0 | (0,6) | 32771 | 10754 | 24 | | | \x0300000009626f620b00818303
7 | 7912 | 1 | 39 | 802 | 0 | 0 | (0,7) | 32771 | 10754 | 24 | | | \x010000000d616c6963650b00811f03
8 | 7864 | 1 | 41 | 809 | 0 | 0 | (0,8) | 3 | 2562 | 24 | | | \x0300000011636861726c69650b00816400
9 | 7816 | 1 | 41 | 811 | 0 | 0 | (0,9) | 3 | 2050 | 24 | | | \x0400000011636861726c69650b00816400
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/*
* disk page organization
*
* space management information generic to any page
*
* pd_lsn - identifies xlog record for last change to this page.
* pd_checksum - page checksum, if set.
* pd_flags - flag bits.
* pd_lower - offset to start of free space.
* pd_upper - offset to end of free space.
* pd_special - offset to start of special space.
* pd_pagesize_version - size in bytes and page layout version number.
* pd_prune_xid - oldest XID among potentially prunable tuples on page.
*
* The LSN is used by the buffer manager to enforce the basic rule of WAL:
* "thou shalt write xlog before data". A dirty buffer cannot be dumped
* to disk until xlog has been flushed at least as far as the page's LSN.
*
* pd_checksum stores the page checksum, if it has been set for this page;
* zero is a valid value for a checksum. If a checksum is not in use then
* we leave the field unset. This will typically mean the field is zero
* though non-zero values may also be present if databases have been
* pg_upgraded from releases prior to 9.3, when the same byte offset was
* used to store the current timelineid when the page was last updated.
* Note that there is no indication on a page as to whether the checksum
* is valid or not, a deliberate design choice which avoids the problem
* of relying on the page contents to decide whether to verify it. Hence
* there are no flag bits relating to checksums.
*
* pd_prune_xid is a hint field that helps determine whether pruning will be
* useful. It is currently unused in index pages.
*
* The page version number and page size are packed together into a single
* uint16 field. This is for historical reasons: before PostgreSQL 7.3,
* there was no concept of a page version number, and doing it this way
* lets us pretend that pre-7.3 databases have page version number zero.
* We constrain page sizes to be multiples of 256, leaving the low eight
* bits available for a version number.
*
* Minimum possible page size is perhaps 64B to fit page header, opaque space
* and a minimal tuple; of course, in reality you want it much bigger, so
* the constraint on pagesize mod 256 is not an important restriction.
* On the high end, we can only support pages up to 32KB because lp_off/lp_len
* are 15 bits.
*/

typedef struct PageHeaderData
{
/* XXX LSN is member of *any* block, not only page-organized ones */
PageXLogRecPtr pd_lsn; /* LSN: next byte after last byte of xlog
* record for last change to this page */
uint16 pd_checksum; /* checksum */
uint16 pd_flags; /* flag bits, see below */
LocationIndex pd_lower; /* offset to start of free space */
LocationIndex pd_upper; /* offset to end of free space */
LocationIndex pd_special; /* offset to start of special space */
uint16 pd_pagesize_version;
TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */
} PageHeaderData;

typedef PageHeaderData *PageHeader;

Record

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
test=# drop table t;
DROP TABLE
test=# CREATE TABLE t(
test(# id integer GENERATED ALWAYS AS IDENTITY, s text
test(# );
CREATE TABLE
test=# CREATE INDEX ON t(s);
CREATE INDEX
test=# begin;
BEGIN
test=*# INSERT INTO t(s) VALUES ('FOO');
INSERT 0 1
test=*# SELECT pg_current_xact_id();
pg_current_xact_id
--------------------
766
(1 row)

test=*# select * from heap_page_items(get_raw_page('t',0))\gx
-[ RECORD 1 ]-------------------
lp | 1
lp_off | 8160
lp_flags | 1
lp_len | 32
t_xmin | 766
t_xmax | 0
t_field3 | 0
t_ctid | (0,1)
t_infomask2 | 2
t_infomask | 2050
t_hoff | 24
t_bits |
t_oid |
t_data | \x0100000009464f4f

test=# SELECT '(0,'||lp||')' AS ctid,
test-# CASE lp_flags
test-# WHEN 0 THEN 'unused'
test-# WHEN 1 THEN 'normal'
test-# WHEN 2 THEN 'redirect to '||lp_off
test-# WHEN 3 THEN 'dead'
test-# END AS state,
test-# t_xmin as xmin,
test-# t_xmax as xmax,
test-# (t_infomask & 256) > 0 AS xmin_committed,
test-# (t_infomask & 512) > 0 AS xmin_aborted,
test-# (t_infomask & 1024) > 0 AS xmax_committed,
test-# (t_infomask & 2048) > 0 AS xmax_aborted
test-# FROM heap_page_items(get_raw_page('t',0));
ctid | state | xmin | xmax | xmin_committed | xmin_aborted | xmax_committed | xmax_aborted
-------+--------+------+------+----------------+--------------+----------------+--------------
(0,1) | normal | 766 | 0 | f | f | f | t
(1 row)

Reference:

【转】AntDB/PostgreSQL内部原理:表Page结构解析_postgresql对应antdb的版本-CSDN博客

0%