Postgres 内部工作原理
Postgres 内部工作机制
介绍
TODO: Postgres 介绍
Database 和 Tables
一个 Postgres server 中会有多个 database,每个 database 中会有多张 table,每个 table 中会有多条数据记录 (tuple), 每个数据记录会有多个字段。除此之外,每个 database 中还会有很多其他被管理的对象.
每个数据库对象都有唯一的 OID, 各数据库对象被保存在各自的 system catalogs 中. 例如 database 和 heap tuple 被保存在 pg_database
和 pg_class
中, 可以通过如下 sql 查询到
|
|
物理结构
当前我们介绍文件目录存储结构。
Postgres 的一个数据库实例的数据存储在环境变量 PGDATA
中,通常 PGDATA
的值为 /var/lib/pgsql/data
。同一台机器中可以部署多个 Postgres 服务实例,不同的服务实例使用不同的 PGDATA 以及不同的端口.
PGDATA 子目录下包含数据库控制配置文件及数据文件。控制数据库服务实例运行的配置文件 postgresql.conf
、pg_hba.conf
和 pg_ident.conf
通常情况下也存储在 PGDATA
中,也可以把它们放到其他的地方(具体可以看 postgres
命令的启动参数或者 pg_ctl
的启动参数)
PGDATA
中的文件结构如下:
|
|
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) |
数据库对象在文件存储中的布局
为了研究数据库数据存储,首先启动数据库实例,创建如下表结构:
|
|
database、tables、index 等文件布局
在 9.0 版本之后,可以通过如下命令查看当前登录数据库的数据目录
|
|
数据库位于 base
子目录下,数据库目录名字是其 oid。例如 test
的 oid 为 16966,他的目录名字为 16966.
|
|
每个不超过 1GB 的 table 或者 index 存储在其所属的 database 目录下的一个文件下.
|
|
当一个 table 或者 index 的文件大小超过 1GB,PostgreSQL 会创建名字类似于 relfilenode.1, 并使用它。如果新的文件被填满了,下一个新的文件 relfilenode.2 将被创建,以此类推。
|
|
单个 table/index 索引文件大小是可以配置的, PostgreSQL 编译选项是
--with-segsize
.
仔细观察数据库子目录,可以发现一些文件的后缀是 _fsm
和 _vm
, 它们分别是对应文件的 free space map
visibility map
, 分别存储着每个文件中每个 page 的 空闲空间及是否可见。indexs 只有 fsm,没有 vm.
可以使用如下命令反向查看文件对应的数据库对象.
|
|
Tablespaces
PostgreSQL 中的 Tablespaces 是 base 目录之外的附加数据区域。 该功能已在 8.0 版本中实现。
TODO: 补充更多细节
Heap Table File 的内部文件格式
数据文件内部存储着大量的 pages, 每个 page 的固定大小为 8192 byte(8KB)。每个数据文件中的 page 从 0 开始编号,这些编号叫 block numbers
.
page 大小可以执行 sql 查看,
SELECT current_setting('block_size');
,其值可以在编译时添加参数修改eq:
./configure --with-blocksize=BLOCKSIZE --with-wal-blocksize=BLOCKSIZE
不同种类的文件的内部布局不相同。如下为 heap table file
的文件布局
heap table 中的 page 包含如下三部分:
- heap tuple(s): 一个 heap tuple 代表 tables 中的一行记录。heap tuple 以栈入顺序添加到 page 中,即新的 tuple 回被插入到旧的前边;
- line pointer(s): line pointer 持有 tuple 的指针。line pointer 是一个变长指针,每个 line pointer 的序号从 1 开始,当新的 tuple 被插入到 page 中时,指向新 tuple 的 line pointer 也会被从插入到 line pointer 数组的尾部;
- 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 的开始.
tuple 的增长方向是从每页的尾部到头部。第二个 tuple 会插入到第一个 tuple 的前边(地址更小的位置), 新的 linp 会插入到第一个 linp 后边,pg_lower 会指向新的 linp 尾部, 原本指向第一个 tuple 头部的 pg_upper 会修改为指向第二个 tuple 头部, 其他 page header 中的数据(eq: pd_lsn, pg_checksum, pg_flag)也会被修改. 修改后的 page 布局如下图所示
TODO: 执行 sql 配合 pg_inspect 分析插入读取过程
heap tuple 读
heap tuple 的读有两种方式: sequential scan
和 index scan
:
sequential scan
: 每个 tables 会有多个文件,每个文件中会有很多固定大小的 page 顺序存储,每个 page 中会有很多 tuple。sequential scan
会扫描所有文件中所有 page 中的 所有 tuple.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 是多进程模型,多个进程协同工作完成所有的数据库管理工作,它包含如下进程:
postgres server
进程是所有 pg 进程的父进程,监听 tcp 端口并对外提供服务backend
进程处理从客户端接收到的请求- 各种
background processes
(包括 VACUUM、CHECKPOINT 等) replication associated processes
处理流复制background worker process
用户定义的后台进程,详细信息见 office docs
查看 Postgres 进程进程树的方法如下,其中假设 pg 的进程 id 为 503065
|
|
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 没有实现原生的连接池特性.这会在一些情况下严重的影响数据库的性能。一些数据库中间件可以解决此问题(pgbouncer 、pgpool-II)
内存模型
宽泛的给 Postgres 中的内存可以被分成两类
- 本地内存区 - 每个 backend 分配来自己使用
- 共享内存区 -
Postgres server
进程分配,由所有进程共享使用的内存区
本地内存区
每个本地内存区会分配一个本地内存区用于处理查询,每个本地内存区有多个子内存区,这些子内存区一些是固定大小的,一些是可变大小的。
子区域 | 描述 |
---|---|
work_mem | 此区域用于 ORDER BY 和 DISTINCT 排序 tuple, 以及 join 操作中的 merge-join 和 hash-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 机制. |
并发控制
事务 ID
TODO:
Tuple 的结构
HeapTuple 分为 普通 tuple
和 TOAST tuple
。 我们只阐述 普通 tuple。
一个 HeapTuple 包含三部分
- HeapTupleHeaderData structure
- NULL bitmap
- 用户数据
HeapTupleHeaderData 包含很多字段,其中跟并发控制相关的字段如下:
- t_xmin 插入此 tuple 的事务的 txid
- t_xmax 更新或删除此 tuple 的事务的 txid。如果 tuple 没有被删除或更新过,此指为 0,标识 INVALID.
- t_cid 在当前事务下,执行此命令前执行过多少 sql;从 0 开始。例如我们在一个事务下执行多个 INSERT sql, “BEGIN;INSERT;INSERT;INSERT;COMMIT;”, 第一个 INSERT 命令执行,t_cid=0; 第二个值为 1,一次类推。
- t_ctid 当前 tuple 的 tid 或者新的 tuple 的 tid. tid 标识一个 tuple。如果此 tuple 有被更新,则指向新的 tuple,否则指向自己.
|
|
点我展开 HeapTupleHeaderData 声明
|
|
Tuple Insert Update Delete
当前节主要描述 tuple,下面没有表示 page header 、linp。
|
|
Insert
insert 操作会直接将 tuple 插入到 page 中.
|
|
- t_xmin 为 99,这个 tuple 是在事务 txid=99 插入的
- t_xmax 为 0,当前 tuple 没有被更新或删除过
- t_cid 为 0,表示 tid=99 第一个 sql
- t_ctid 为 ‘(0,1)’, 指向他自己,因为他是第一个 tuple
Delete
删除操作是逻辑删除. 执行 DELETE 操作实际的操作是将 t_xmax 的值修改为 txid=100 的 txid
|
|
当事务被提交之后,tuple1 已经不需要了。通常这种不需要的 tuple 被称为 dead tuples
.
dead tuples
最终会被清理掉。清理 dead tuples
工作由 VACUUM 执行。
Update
更新操作会删除(t_xmax 修改为当前 txid)旧的 tuple,插入一个新的 tuple. 如下,我们在一个事务中执行一次插入,后续在同一个事务下执行两个 UPDATE。
TODO: 画图把数据变化过程可视化一下
|
|
假设第二个事务的 txid=1169 执行第一个 Update 的时候, 通过设置 t_xmax=txid 删除 Tuple1; t_ctid 指向新的 tuple Tuple2. tuple 的 head 变化如下:
|
|
执行第二个 Update 的时候,通过设置 t_xmax=txid 删除 Tuple2; t_ctid 指向 Tuple3
|
|
如果 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 的空闲空间百分率。
|
|
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,数组的各项
如图
- 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
展示当前事务的快照
|
|
文本格式的 txid_current_snapshot 是 ‘xmin:xmax:xip_list’, 每部分的含义如下:
- xmin 最早的活跃事务。早于它的事务要么 committed 事务可见,要么 rollback 提交内容不生效/不可见
- xmax 第一个尚未分配的 txid。 所有 >=txid 的事务尚未启动,是不可见的
- xip_list 活跃的事务。这个列表只包含 xmin 和 xmax 之间的事务
如上图,第一个例子 ‘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 snapshots
。transaction snapshots
用于在事务执行过程中判断 tuple 是否可见。相同的 tuple 及相同的 transaction snapshots
下,不同的事务隔离级别会有不同的可见结果。
transaction manager
中持有当前正在执行的事务的执行状态。如图,三个事务相继执行,TxA 和 TxB 的事务隔离级别是 READ COMMITTED
,TxC 的是 REPREATABLE READ
.
|
|
可见性规则
基于现在的堆文件的实现,一个事务如何判断当前获取到的 tuple 是可见的呢?interdb 做的检查规则如下:
|
|
被 ABORTED 的 tuple 不可见(Rule 1)。
正在执行的事务,当前事务自己可见(Rule 2, Rule 3, Rule 4).
Rule 7, tuple 被当前事务删除了
Rule 8,其他的事务的删除没有提交,当前事务是可见的
|
|
TOAST
TODO
VACUUM Processing
TODO
Buffer Manager
TODO
Write Ahead Logging (WAL)
TODO