目录

Postgres 内部工作原理

Postgres 内部工作机制

介绍

TODO: Postgres 介绍

Database 和 Tables

一个 Postgres server 中会有多个 database,每个 database 中会有多张 table,每个 table 中会有多条数据记录 (tuple), 每个数据记录会有多个字段。除此之外,每个 database 中还会有很多其他被管理的对象.

每个数据库对象都有唯一的 OID, 各数据库对象被保存在各自的 system catalogs 中. 例如 database 和 heap tuple 被保存在 pg_databasepg_class 中, 可以通过如下 sql 查询到

1
2
3
4
5
postgres=# select datname, oid from pg_database where datname = 'postgres';
postgres |   5

postgres=# select relname, oid from pg_class where relname='actor';
actor   | 16475

物理结构

当前我们介绍文件目录存储结构。 Postgres 的一个数据库实例的数据存储在环境变量 PGDATA 中,通常 PGDATA 的值为 /var/lib/pgsql/data。同一台机器中可以部署多个 Postgres 服务实例,不同的服务实例使用不同的 PGDATA 以及不同的端口. PGDATA 子目录下包含数据库控制配置文件及数据文件。控制数据库服务实例运行的配置文件 postgresql.confpg_hba.confpg_ident.conf 通常情况下也存储在 PGDATA 中,也可以把它们放到其他的地方(具体可以看 postgres 命令的启动参数或者 pg_ctl 的启动参数) PGDATA 中的文件结构如下:

 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
# tree -L 2 $PGDATA
.
├── PG_VERSION
├── base
│   ├── 1
│   └── pgsql_tmp
├── global
│   ├── 1213
│   ├── 1213_fsm
│   ├── 1213_vm
│   ├── pg_control
│   ├── pg_filenode.map
│   └── pg_internal.init
├── pg_commit_ts
├── pg_dynshmem
├── pg_hba.conf
├── pg_ident.conf
├── pg_logical
│   ├── mappings
│   ├── replorigin_checkpoint
│   └── snapshots
├── pg_multixact
│   ├── members
│   └── offsets
├── pg_notify
├── pg_replslot
├── pg_serial
├── pg_snapshots
├── pg_stat
├── pg_stat_tmp
├── pg_subtrans
│   └── 0000
├── pg_tblspc
├── pg_twophase
├── pg_wal
│   ├── 000000010000000000000004
│   ├── 000000010000000000000005
│   └── archive_status
├── pg_xact
│   └── 0000
├── postgresql.auto.conf
├── postgresql.conf
├── postmaster.opts
└── postmaster.pid

https://www.postgresql.org/docs/devel/storage-file-layout.html

Item Description
PG_VERSION PostgreSQL 主要版本号的文件
base 每个数据库的子目录
global 系统表, 比如 pg_database
pg_commit_ts 事务提交时间戳数据, Version 9.5 or later.
pg_dynshmem dynamic shared memory subsystem 使用的文件, Version 9.4 or later.
pg_logical status data for logical decoding
pg_multixact multitransaction status data (used for shared row locks)
pg_notify LISTEN/NOTIFY status data
pg_replslot replication slot data
pg_serial information about committed serializable transactions
pg_snapshots exported snapshots
pg_stat permanent files for the statistics subsystem
pg_stat_tmp temporary files for the statistics subsystem
pg_subtrans subtransaction status data
pg_tblspc symbolic links to tablespaces
pg_twophase state files for prepared transactions
pg_wal WAL (Write Ahead Log) files. It is renamed from pg_xlog in Version 10.
pg_xact transaction commit status data, It is renamed from pg_clog in Version 10.
postgresql.auto.conf A file used for storing configuration parameters that are set by ALTER SYSTEM
postmaster.opts A file recording the command-line options the server was last started with
postmaster.pid A lock file recording the current postmaster process ID (PID), cluster data directory path, postmaster start timestamp, port number, Unix-domain socket directory path (could be empty), first valid listen_address (IP address or *, or empty if not listening on TCP), and shared memory segment ID (this file is not present after server shutdown)

数据库对象在文件存储中的布局

为了研究数据库数据存储,首先启动数据库实例,创建如下表结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
create database test;

CREATE TABLE sal_emp (
    id int primary key,
    name            text,
    pay_by_quarter  integer[],
    schedule        text[][]
);

INSERT INTO sal_emp
    VALUES (
    1,
    'Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {"training", "presentation"}}'
);

create index sal_emp_btree ON sal_emp (pay_by_quarter);
create index sal_emp_gin ON sal_emp USING gin(pay_by_quarter);

database、tables、index 等文件布局

在 9.0 版本之后,可以通过如下命令查看当前登录数据库的数据目录

1
2
3
4
5
test=# show data_directory;
      data_directory
---------------------------
 /home/vagrant/pgdata/data
(1 row)

数据库位于 base 子目录下,数据库目录名字是其 oid。例如 test 的 oid 为 16966,他的目录名字为 16966.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
test=# select oid,datname from pg_database;
  oid  |  datname
-------+-----------
     5 | postgres
 16966 | test
     1 | template1
     4 | template0

$ ls -ld base/16966/
drwx------ 2 vagrant vagrant 12288 Sep 16 01:59 base/16966/

每个不超过 1GB 的 table 或者 index 存储在其所属的 database 目录下的一个文件下.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
test=# SELECT relname, oid, relfilenode FROM pg_class WHERE relname = 'sal_emp';
 relname |  oid  | relfilenode
---------+-------+-------------
 sal_emp | 16967 |       16967

test=# select * from pg_relation_filepath('sal_emp');
 pg_relation_filepath
----------------------
 base/16966/16967

$ ls -alh base/16966/16967
-rw------- 1 vagrant vagrant 8.0K Sep 16 02:03 base/16966/16967

当一个 table 或者 index 的文件大小超过 1GB,PostgreSQL 会创建名字类似于 relfilenode.1, 并使用它。如果新的文件被填满了,下一个新的文件 relfilenode.2 将被创建,以此类推。

1
2
3
ls -alh base/16966/16967*
-rw------- 1 vagrant vagrant 1.0G Sep 16 02:03 base/16966/16967
-rw------- 1 vagrant vagrant   1M Sep 16 02:03 base/16966/16967.1

单个 table/index 索引文件大小是可以配置的, PostgreSQL 编译选项是 --with-segsize .

仔细观察数据库子目录,可以发现一些文件的后缀是 _fsm_vm, 它们分别是对应文件的 free space map visibility map, 分别存储着每个文件中每个 page 的 空闲空间及是否可见。indexs 只有 fsm,没有 vm.

可以使用如下命令反向查看文件对应的数据库对象.

1
2
3
4
5
test=# SELECT pg_filenode_relation(0, 16967);
pg_filenode_relation
----------------------
 sal_emp
(1 row)

Tablespaces

PostgreSQL 中的 Tablespaces 是 base 目录之外的附加数据区域。 该功能已在 8.0 版本中实现。

TODO: 补充更多细节

Heap Table File 的内部文件格式

storage-page-layout

数据文件内部存储着大量的 pages, 每个 page 的固定大小为 8192 byte(8KB)。每个数据文件中的 page 从 0 开始编号,这些编号叫 block numbers.

page 大小可以执行 sql 查看, SELECT current_setting('block_size');,其值可以在编译时添加参数修改

–with-blocksize=BLOCKSIZE

eq:

./configure --with-blocksize=BLOCKSIZE --with-wal-blocksize=BLOCKSIZE

不同种类的文件的内部布局不相同。如下为 heap table file 的文件布局

/media/img/db/log/postgres-internal/postgres-tuple-layout.svg

heap table 中的 page 包含如下三部分:

  1. heap tuple(s): 一个 heap tuple 代表 tables 中的一行记录。heap tuple 以栈入顺序添加到 page 中,即新的 tuple 回被插入到旧的前边;
  2. line pointer(s): line pointer 持有 tuple 的指针。line pointer 是一个变长指针,每个 line pointer 的序号从 1 开始,当新的 tuple 被插入到 page 中时,指向新 tuple 的 line pointer 也会被从插入到 line pointer 数组的尾部;
  3. header data: 位于 page 的开始位置,记录了当前 page 的元数据。其详细构成定义在 PageHeaderData 中.其主要信息如下:
    • pd_lsn: 保存了 WAL 的 LSN,于 WAL 机制相关;
    • pd_checksum: 保存了当前 page 的 checksum;
    • pd_lower, pd_upper: pd_lower 指向 linepointer 的末尾,pd_upper 指向最新的 tuple 的开始;
    • pd_special, 这个变量是给 index 使用的,他指向 page 的末尾最后一个 byte

lin pointer 的最后到最新 tuple 的开始是当前 page 的 空闲空间。 tuple identifier(TID)唯一标识一个 tuple. TID 包含两个指,block number 标识它所在的 page,offset number 标识它在 page 中的位置. 它的典型使用场景的 index.

另外,当 heap tuple 的大下超过 2k(1/4 page size 8k) 时候,存储会使用 TOAST(The Oversized-Attribute Storage Technique), 详细信息见 Postgres 文档

tuple 最多 MaxHeapAttributeNumber=1664 个 column.

heap tuple 的读写

heap tuple 写

如下图所示, 假设当前 table 只有一页数据,当前页中有一条数据. pg_lower 指向 linp 最后一项的末尾, pg_upper 指向最后一个 tuple 的开始.

/media/img/db/log/postgres-internal/postgres-tuple-write-before.png

tuple 的增长方向是从每页的尾部到头部。第二个 tuple 会插入到第一个 tuple 的前边(地址更小的位置), 新的 linp 会插入到第一个 linp 后边,pg_lower 会指向新的 linp 尾部, 原本指向第一个 tuple 头部的 pg_upper 会修改为指向第二个 tuple 头部, 其他 page header 中的数据(eq: pd_lsn, pg_checksum, pg_flag)也会被修改. 修改后的 page 布局如下图所示

/media/img/db/log/postgres-internal/postgres-tuple-write-after.png

TODO: 执行 sql 配合 pg_inspect 分析插入读取过程

heap tuple 读

heap tuple 的读有两种方式: sequential scanindex scan:

  1. sequential scan: 每个 tables 会有多个文件,每个文件中会有很多固定大小的 page 顺序存储,每个 page 中会有很多 tuple。sequential scan 会扫描所有文件中所有 page 中的 所有 tuple.
  2. index scan: index 是一棵搜索树,index 中包含很多 index tuple,这些 index tuple 中包含 index key 和 指向 heap tuple 的 TID。假设 index tuple 中包含如下内容 ‘(block=3, offset=7)’. 当 index scan 找到某个 index tuple 符合条件时,获得到 index tuple 中的 TID. 由于 page 是固定大小的,可以直接使用类似于 lseek 调用 定位到对应的 page 后将数据 load 到内存,再由 offset 定位到 内存中 linp 的位置,linp 中包含 tuple 的指针及状态,数据即可定位到并读出来。

TODO: 这里最好可以画个图

进程模型与内存模型

进程模型

Postgres server 是多进程模型,多个进程协同工作完成所有的数据库管理工作,它包含如下进程:

  1. postgres server 进程是所有 pg 进程的父进程,监听 tcp 端口并对外提供服务
  2. backend 进程处理从客户端接收到的请求
  3. 各种 background processes (包括 VACUUM、CHECKPOINT 等)
  4. replication associated processes 处理流复制
  5. background worker process 用户定义的后台进程,详细信息见 office docs

/media/img/db/log/postgres-internal/postgres-internal-process-model.svg

查看 Postgres 进程进程树的方法如下,其中假设 pg 的进程 id 为 503065

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export PID=503065
ps f -p $(pstree -p ${PID} | sed 's/(/\n(/g' | grep '(' | sed 's/(\(.*\)).*/\1/' | tr "\n" " ")

    PID TTY      STAT   TIME COMMAND
 503065 pts/1    S+     0:00 ./src/backend/postgres
 503068 ?        Ss     0:00  \_ postgres: checkpointer
 503069 ?        Ss     0:00  \_ postgres: background writer
 503071 ?        Ss     0:00  \_ postgres: walwriter
 503072 ?        Ss     0:00  \_ postgres: autovacuum launcher
 503073 ?        Ss     0:00  \_ postgres: logical replication launcher
 503265 ?        Ss     0:00  \_ postgres: vagrant postgres [local] idle

postgres server 进程是所有进程的父进程,负责启动并管理各后台进程;另外会监听 tcp 端口。当有新的连接请求过来时,主进程会初始化并 fork 子进程,由子进程处理请求。

backend 进程由 postgres server 启动,处理客户端请求,直到客户端断开 tcp 连接后, backend 进程也会一起退出。Postgres 支持有多个客户端同时连接并操作数据库。客户端数量的上限由配置项 max_connections 决定,默认值是 100。当客户端连接数超过 max_connections 后,服务会拒绝新的连接,直到当前连接数少于 max_connections 为止。另外,当连接数量超过 max_connections 后,服务器无法接受新的连接,此时如果想连接上 postgres 做一些维护工作将会被拒绝。配置了选项superuser_reserved_connections ,即使连接数超过了 max_connections ,超级管理员也可以登录到服务器。

如果很多客户端(如 WEB 应用)频繁重复与 PostgreSQL 服务器的连接和断开连接,会增加建立连接和创建后端进程的成本,因为 PostgreSQL 没有实现原生的连接池特性.这会在一些情况下严重的影响数据库的性能。一些数据库中间件可以解决此问题(pgbouncerpgpool-II)

内存模型

宽泛的给 Postgres 中的内存可以被分成两类

  1. 本地内存区 - 每个 backend 分配来自己使用
  2. 共享内存区 - Postgres server 进程分配,由所有进程共享使用的内存区

/media/img/db/log/postgres-internal/postgres-internal-mem-model.svg

本地内存区

每个本地内存区会分配一个本地内存区用于处理查询,每个本地内存区有多个子内存区,这些子内存区一些是固定大小的,一些是可变大小的。

子区域 描述
work_mem 此区域用于 ORDER BYDISTINCT 排序 tuple, 以及 join 操作中的 merge-joinhash-join.
maintenance_work_mem 一些维护工作 (VACUUM, REINDEX) 使用的内存.
temp_buffers 用于存储临时表.

共享内存区

Postgres 启动的时候会分配固定大小的多个内存区

子区域 description
shared buffer pool PostgreSQL 从持久存储中加载表和索引中的 page 到此区域, 之后直接原地操作.
WAL buffer 为了保证数据库失败不对数据,Postgres 支持 WAL 机制. WAL 是 Postgres 的事务日志; WAL buffer 是 WAL 数据写回持久存储前的缓存区域。
commit log Commit Log(CLOG) 保存事务提交状态,用于 MVCC 机制.

并发控制

tutorial-transactions

事务 ID

TODO:

Tuple 的结构

HeapTuple 分为 普通 tupleTOAST tuple。 我们只阐述 普通 tuple。 一个 HeapTuple 包含三部分

  1. HeapTupleHeaderData structure
  2. NULL bitmap
  3. 用户数据

/media/img/db/log/postgres-internal/postgres-internal-heap-tuple-header.svg

HeapTupleHeaderData 包含很多字段,其中跟并发控制相关的字段如下:

  1. t_xmin 插入此 tuple 的事务的 txid
  2. t_xmax 更新或删除此 tuple 的事务的 txid。如果 tuple 没有被删除或更新过,此指为 0,标识 INVALID.
  3. t_cid 在当前事务下,执行此命令前执行过多少 sql;从 0 开始。例如我们在一个事务下执行多个 INSERT sql, “BEGIN;INSERT;INSERT;INSERT;COMMIT;”, 第一个 INSERT 命令执行,t_cid=0; 第二个值为 1,一次类推。
  4. t_ctid 当前 tuple 的 tid 或者新的 tuple 的 tid. tid 标识一个 tuple。如果此 tuple 有被更新,则指向新的 tuple,否则指向自己.
1
2
3
4
5
6
7
8
9
DROP TABLE tbl;
CREATE TABLE tbl (data text);
BEGIN;INSERT INTO tbl VALUES('A');INSERT INTO tbl VALUES('B');INSERT INTO tbl VALUES('C');COMMIT;
SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_infomask2, t_infomask, t_hoff, t_bits, t_data FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_data
-------+--------+--------+-------+--------+-------------+------------+--------+--------+--------
     1 |   1130 |      0 |     0 | (0,1)  |           1 |       2050 |     24 |        | \x0541
     2 |   1130 |      0 |     1 | (0,2)  |           1 |       2050 |     24 |        | \x0542
     3 |   1130 |      0 |     2 | (0,3)  |           1 |       2050 |     24 |        | \x0543
点我展开 HeapTupleHeaderData 声明
 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
typedef struct HeapTupleFields
{
    TransactionId t_xmin;		   /* inserting xact ID */
    TransactionId t_xmax;              /* deleting or locking xact ID */

    union
    {
            CommandId       t_cid;     /* inserting or deleting command ID, or both */
            TransactionId 	t_xvac;    /* old-style VACUUM FULL xact ID */
    } t_field3;
} HeapTupleFields;

typedef struct DatumTupleFields
{
    int32          datum_len_;          /* varlena header (do not touch directly!) */
    int32          datum_typmod;   	    /* -1, or identifier of a record type */
    Oid            datum_typeid;   	    /* composite type OID, or RECORDOID */

    /*
        * Note: field ordering is chosen with thought that Oid might someday
        * widen to 64 bits.
        */
} DatumTupleFields;

typedef struct HeapTupleHeaderData
{
    union
    {
            HeapTupleFields t_heap;
            DatumTupleFields t_datum;
    } t_choice;

    ItemPointerData t_ctid;         /* current TID of this or newer tuple */

    /* Fields below here must match MinimalTupleData! */
    uint16          t_infomask2;    /* number of attributes + various flags */
    uint16          t_infomask;     /* various flag bits, see below */
    uint8           t_hoff;         /* sizeof header incl. bitmap, padding */
    /* ^ - 23 bytes - ^ */
    bits8           t_bits[1];      /* bitmap of NULLs -- VARIABLE LENGTH */

    /* MORE DATA FOLLOWS AT END OF STRUCT */
} HeapTupleHeaderData;

typedef HeapTupleHeaderData *HeapTupleHeader;

Tuple Insert Update Delete

当前节主要描述 tuple,下面没有表示 page header 、linp。

1
2
3
4
5
6
7
8
9
DROP TABLE tbl;
CREATE TABLE tbl (data text);
INSERT INTO tbl VALUES('A');UPDATE tbl SET data='B';
SELECT t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_bits, t_data FROM heap_page_items(get_raw_page('tbl', 0));

 t_xmin | t_xmax | t_cid | t_ctid | t_bits | t_data
--------+--------+-------+--------+--------+--------
   99   |   100  |     0 | (0,2)  |        | \x0541
   100  |      0 |     0 | (0,2)  |        | \x0542

Insert

insert 操作会直接将 tuple 插入到 page 中.

1
2
3
4
5
6
7
8
DROP TABLE tbl;
CREATE TABLE tbl (data text);
INSERT INTO tbl VALUES('A');
SELECT t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_data FROM heap_page_items(get_raw_page('tbl', 0));

 t_xmin | t_xmax | t_cid | t_ctid | t_data
--------+--------+-------+--------+--------
   99   |      0 |     0 | (0,1)  | \x0541
  1. t_xmin 为 99,这个 tuple 是在事务 txid=99 插入的
  2. t_xmax 为 0,当前 tuple 没有被更新或删除过
  3. t_cid 为 0,表示 tid=99 第一个 sql
  4. t_ctid 为 ‘(0,1)’, 指向他自己,因为他是第一个 tuple

Delete

删除操作是逻辑删除. 执行 DELETE 操作实际的操作是将 t_xmax 的值修改为 txid=100 的 txid

1
2
3
4
5
6
7
8
9
DROP TABLE tbl;
CREATE TABLE tbl (data text);
INSERT INTO tbl VALUES('A'); -- txid=99
Delete from tbl where data='A'; -- txid=100
SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_data FROM heap_page_items(get_raw_page('tbl', 0));

 tuple | t_xmin | t_xmax | t_cid | t_ctid | t_data
-------+--------+--------+-------+--------+--------
     1 |   99   |   100  |     0 | (0,1)  | \x0541

当事务被提交之后,tuple1 已经不需要了。通常这种不需要的 tuple 被称为 dead tuples. dead tuples 最终会被清理掉。清理 dead tuples 工作由 VACUUM 执行。

Update

更新操作会删除(t_xmax 修改为当前 txid)旧的 tuple,插入一个新的 tuple. 如下,我们在一个事务中执行一次插入,后续在同一个事务下执行两个 UPDATE。

TODO: 画图把数据变化过程可视化一下

 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
DROP TABLE tbl;
CREATE TABLE tbl (data text);
INSERT INTO tbl VALUES('A'); -- txid=99
SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_data FROM heap_page_items(get_raw_page('tbl', 0));
BEGIN;
UPDATE tbl set data='B';
SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_data FROM heap_page_items(get_raw_page('tbl', 0));
UPDATE tbl set data='C';
COMMIT;
SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid, t_data FROM heap_page_items(get_raw_page('tbl', 0));

-- 执行 insert
 tuple | t_xmin | t_xmax | t_cid | t_ctid | t_data
-------+--------+--------+-------+--------+--------
     1 |   1168 |      0 |     0 | (0,1)  | \x0541

-- 执行第一次 update
 tuple | t_xmin | t_xmax | t_cid | t_ctid | t_data
-------+--------+--------+-------+--------+--------
     1 |   1168 |   1169 |     0 | (0,2)  | \x0541
     2 |   1169 |      0 |     0 | (0,2)  | \x0542

-- 执行第二次 update
 tuple | t_xmin | t_xmax | t_cid | t_ctid | t_data
-------+--------+--------+-------+--------+--------
     1 |   1168 |   1169 |     0 | (0,2)  | \x0541
     2 |   1169 |   1169 |     0 | (0,3)  | \x0542
     3 |   1169 |      0 |     1 | (0,3)  | \x0543

假设第二个事务的 txid=1169 执行第一个 Update 的时候, 通过设置 t_xmax=txid 删除 Tuple1; t_ctid 指向新的 tuple Tuple2. tuple 的 head 变化如下:

1
2
3
4
5
6
7
8
Tuple1:
    t_xmax=txid
    t_ctid '(0,1)' 修改为 '(0,2)'
Tuple2:
    t_xmin=txid
    t_xmax=0
    t_cid=0
    t_ctid='(0,2)'

执行第二个 Update 的时候,通过设置 t_xmax=txid 删除 Tuple2; t_ctid 指向 Tuple3

1
2
3
4
5
6
7
8
Tuple2:
    t_xmax=txid
    t_ctid '(0,2)' 修改为 '(0,3)'
Tuple3:
    t_xmin=txid
    t_xmax=0
    t_cid=1
    t_ctid='(0,3)'

如果 txid 事务 commited,Tuple1、Tuple2 会变成 dead tuple;如果 txid 事务 aborded,Tuple2、Tuple3 会变成 dead tuple

Free Space Map

Free Space Map 用于跟踪数据库关系的可用空间. 当插入 heap/index tuple 的时候,PostgreSQL 使用 FSM 寻找合适的 page 插入 tuple.

FSM 文件命名为其关系的 filenode 数字加后缀 _fsm. 例如,关系 public.user 保存的文件的 filenode 为 1000,则它的 FSM 文件为同目录下的 1000_fsm 文件。FSM 文件会在需要的时候加载到 shared memory 中。

pg_freespacemap 扩展可以查看关系的空闲空间大小。如下查询关系 tbl 的空闲空间百分率。

1
2
3
4
5
CREATE EXTENSION pg_freespacemap;
SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('tbl');
 blkno | avail | freespace ratio 
-------+-------+-----------------
     0 |     0 |            0.00

Commit Log (clog)

Commit Log 保存着事务状态。Commit Log 通常也被叫做 clog,被分配在 shared memory, 贯穿在整个事务的处理过程中。

Transaction Status

Postgres 定义了4中事务状态 N_PROGRESS, COMMITTED, ABORTED, and SUB_COMMITTED.

Clog 如何工作的

clog 由 shared memory 中一个到多个 8kb 大小的 page 构成. clog 是逻辑上的数组。数组的 index 表示事务的 id,数组的各项

如图

/media/img/db/log/postgres-internal/postgres-internal-clog-01.svg

  • T1: txid=200 commit, 状态由 IN_PROGRESS 变为 COMMITTED
  • T2: txid=201 abort, 状态由 IN_PROGRESS 变为 ABORTED

后续会讲述 clog 是如何使用的。

Transaction Snapshot

transaction snapshot 保存了所有事务的的执行状态信息。Postgres 内部定义了文本格式的 transaction Snapshot 如 ‘100💯’. ‘100💯’ 意思是txids<=99 没有在活跃, txids>=100 在活跃。

内置的函数 txid_current_snapshot 展示当前事务的快照

1
2
3
4
SELECT txid_current_snapshot();
 txid_current_snapshot 
-----------------------
 100:104:100,102

文本格式的 txid_current_snapshot 是 ‘xmin:xmax:xip_list’, 每部分的含义如下:

  • xmin 最早的活跃事务。早于它的事务要么 committed 事务可见,要么 rollback 提交内容不生效/不可见
  • xmax 第一个尚未分配的 txid。 所有 >=txid 的事务尚未启动,是不可见的
  • xip_list 活跃的事务。这个列表只包含 xmin 和 xmax 之间的事务

/media/img/db/log/postgres-internal/postgres-internal-tx-snapshot.svg

如上图,第一个例子 ‘100💯’

  • xmin=100,表示 txids<100 的没活跃
  • xmax=100,表示 txids>=100 的活跃

第二个例子 ‘100:104:100,102’

  • xmin=100,表示 txids<100 的没有活跃
  • xmax=104,表示 txids>=104 的正在活跃
  • xip_list=100,102,表示 100和102 正常执行

transaction manager 管理 transaction snapshotstransaction snapshots 用于在事务执行过程中判断 tuple 是否可见。相同的 tuple 及相同的 transaction snapshots 下,不同的事务隔离级别会有不同的可见结果。

/media/img/db/log/postgres-internal/postgres-internal-tx-manager.svg

transaction manager 中持有当前正在执行的事务的执行状态。如图,三个事务相继执行,TxA 和 TxB 的事务隔离级别是 READ COMMITTED,TxC 的是 REPREATABLE READ.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
T1: 
    TxA 开始执行第一个 SELECT 命令。当执行第一个命令时候,TxA请求获得 txid 和 快照。transaction manager 分配 txid=200, transation snapshot '200:200:'
T2:
    TxB 开始执行第一个 SELECT 命令,transaction manager 分配 tx=201,transaction snapshot '200:200:', 因此 TxB 中无法看到 TxA
T3:
    TxC 开始执行第一个 SELECT 命令,分配 tx=202,tx snapshot '200:200:'. TxC 看不到 TxA 和 TxB
T4:
    TxA 提交。transaction manager 移除他的事务信息
T5:
    TxB 和 TxC 再次执行 SELECT命令。
    TxB 请求获得 tx snapshoot,因为他的事务隔离级别是 `READ COMMITTED`,TxB 获得的 tx snapshot 是 '201:201:'。 TxA 已经 commited 了,故 TxB 可以看到 TxA
    TxC 因为其事务隔离级别是 `REPEATABLE READ`,应该继续使用之前获得的 snapshot '200:200:'. TxC 看不到 TxA 和 TxB

可见性规则

基于现在的堆文件的实现,一个事务如何判断当前获取到的 tuple 是可见的呢?interdb 做的检查规则如下:

 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
/* t_xmin status = ABORTED */
Rule 1: IF t_xmin status is 'ABORTED' THEN
            RETURN 'Invisible'
        END IF
        
        /* t_xmin status = IN_PROGRESS */
        IF t_xmin status is 'IN_PROGRESS' THEN
    	       IF t_xmin = current_txid THEN
Rule 2:          IF t_xmax = INVALID THEN
			            RETURN 'Visible'
Rule 3:          ELSE  /* this tuple has been deleted or updated by the current transaction itself. */
			             RETURN 'Invisible'
                 END IF
Rule 4:        ELSE   /* t_xmin  current_txid */
		              RETURN 'Invisible'
               END IF
        END IF
             
        /* t_xmin status = COMMITTED */
        IF t_xmin status is 'COMMITTED' THEN
Rule 5:     IF t_xmin is active in the obtained transaction snapshot THEN
                RETURN 'Invisible'
Rule 6:     ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN
                RETURN 'Visible'
            ELSE IF t_xmax status is 'IN_PROGRESS' THEN
Rule 7:         IF t_xmax =  current_txid THEN
                    RETURN 'Invisible'
Rule 8:         ELSE  /* t_xmax  current_txid */
                    RETURN 'Visible'
                END IF
            ELSE IF t_xmax status is 'COMMITTED' THEN
Rule 9:         IF t_xmax is active in the obtained transaction snapshot THEN
                    RETURN 'Visible'
Rule 10:        ELSE
                    RETURN 'Invisible'
                END IF
            END IF
        END IF

被 ABORTED 的 tuple 不可见(Rule 1)。

正在执行的事务,当前事务自己可见(Rule 2, Rule 3, Rule 4).

Rule 7, tuple 被当前事务删除了

Rule 8,其他的事务的删除没有提交,当前事务是可见的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Rule 1: If Status(t_xmin) = ABORTED => Invisible

Rule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD => Visible
Rule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax != INVAILD => Invisible
Rule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin != current_txid => Invisible

Rule 5: If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active => Invisible
Rule 6: If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) => Visible
Rule 7: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid => Invisible
Rule 8: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax != current_txid => Visible
Rule 9: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active => Visible
Rule 10: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) != active => Invisible

TOAST

TODO

VACUUM Processing

TODO

Buffer Manager

TODO

Write Ahead Logging (WAL)

TODO

references