- 本文章基于Postgresql 9.5
- Page数据页结构
- tablepce、table、toast、index数据文件位置
- hexpdump解析数据文件
Page结构
PG数据表存储在N个数据文件中,每个数据文件有N个Page(大小默认为8K,可在编译安装时指定)组成,Page为Pg最小存取单元。
数据页8Kb = 8 * 1024 Byte, 其中分为五部分:
- PageHeaderData: 24字节长,包含关于页面的一般信息,包括空闲空间指针。24Byte;
- ItemIdData: 一个记录(偏移量,长度)对的数组,指向实际项。 4Byte;
- Free Space: 为分配的空间(空闲空间)
- 新itemIdData指针从头部开始分配
- 新数据Items从结尾开始分配
- Items 实际的项本身 – 实际结构取决于表包含的内容,表和序列都是用
HeapTupleHeaderData
结构, 索引需要不同对待。 - Special Space 索引访问模式相关的数据。不同的索引访问方式存放不同的数据。在普通表中为空。
1 | /* include/stotage/bufpage.h |
- pg_lsn:记录最后一次对page修改的xlog记录id。
- pg_checksum:页面的校验和,主要是通过函数pg_checksum_block函数生成的,0也是有效地,参数为PageHeaderData和BLCKSZ(page’s size)。当校验和验证失败,即认为当前页面无效。
pg_flags:page的flags,具体值为,可以叠加:
1
2
3
4
5
6pg_lower和pg_upper:最后一个项指针的位置和最新的tuple位置。主要进行查找空闲位置,进行插入工作。
- pg_special:page预留的位置,可以存储索引等信息。
pg_pagesize_version:page大小以及当前版本。page大小可以通过configure进行设置。version的意思是
1
2
3
4
5
6
7
8
9
10
11
12/*
* Page layout version number 0 is for pre-7.3 Postgres releases.
* Releases 7.3 and 7.4 use 1, denoting a new HeapTupleHeader layout.
* Release 8.0 uses 2; it changed the HeapTupleHeader layout again.
* Release 8.1 uses 3; it redefined HeapTupleHeader infomask bits.
* Release 8.3 uses 4; it changed the HeapTupleHeader layout again, and
* added the pd_flags field (by stealing some bits from pd_tli),
* as well as adding the pd_prune_xid field (which enlarges the header).
*
* As of Release 9.3, the checksum version must also be considered when
* handling pages.
*/pg_prune_xid: 一般是最后一次删除或者更新的xid;
- pg_linp 项指针;
ItemIdData指向指针:
1 | typedef struct ItemIdData |
表与序列的结构HeapTupleHeaderData
- Tuple 头部信息
- 实际数据信息
1 | typedef struct HeapTupleFields |
Rel结构体展开
1 | Field Type Length Offset Description |
- xmin 和 xmax 是插入,删除,更新操作时的事务ID,插入时会在xmin内写入当前事务ID,删除时在xmax写入当前事务ID,更新是进行删除后再插入。
- t_cid 是指一个事务内的命令ID, 每个事务都从0开始。
- t_ctid 物理ID。
t_infomask2 取值
1
2
3
4
5
6
7
8
9
10
11/*
* information stored in t_infomask2:
*/
/* bits 0x1800 are available */t_infomask 取值:
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/*
* information stored in t_infomask:
*/
/* xmax is a shared locker */
HEAP_XMAX_KEYSHR_LOCK)t_hoff: HeapTupleHeaderData长度,如果有OID会增加4,受字节对齐影响,会增加8
- t_bits: 指向实际具体数据。
tablespace
位置
查看数据文件首先知道数据文件存放于表空间下,首先需要知道表空间OId
1 | Postgres=# select oid, datname from pg_database ; |
其次表空间分为系统表空间与用户自定义表空间。
PG系统表空间默认有两个:pg_default 和 pg_global
pg_default 的文件存放在PGDATA/base目录下
pg_global 的文件存放在PGDATA/global目录下。
用户自定义的表空间在PGDATA/pg_tblspc存在一个软连接,指向实际的存放目录 create tablespace 指定的location
软连接以表空间OID命名。
1 | Postgres=# create tablespace test owner Postgres LOCATION '/home/Postgres/db'; |
1 | shell$ /home/Postgres/dbtest/pg_tblspc |
table
位置
pg中每个表都是一个文件,文件以filenode命名。filenode的值可以从pg_class
的relfilenode
字段获取。
当表大小超过1G的时候,表文件会被分割,第一个文件的名字是filenode,第二个则是filenode.1 依次后推,
表文件大小限额1G是PG默认值,该值可以通过编译–with-segsize来改变。
方法一
1 | Postgres=# select relname, relfilenode, relnamespace from pg_class where relname = 'pagetest'; |
方法二
1 | Postgres=# select pg_relation_filepath('pagetest'); |
在表空间目录下, 通常与表Filenode相关的还有两个文件, filenode_fsm 和 filenode_vm .
fsm 文件为 free space map. 用于跟踪表文件中的空闲空间。
vm 文件为 visibility map. 用于跟踪哪些page包含对所有事务都可见的tuple。
index
位置
在Pagetest表上创建索引
1 | Postgres=# create index ON pagetest using brin ( id ); |
查看索引存放文件位置:
1 | Postgres=# select pg_relation_filepath('pagetest_id_idx'); |
Pageinspect查看Page内容
安装
1 | cd $PGSRC/contrib/pageinspect |
简单使用
1 | create extension pageinspect ; |
Table
查看raw_page 十六进制内容:
1 | \x # Expanded display is on. |
header & items;
1 | Postgres=# select * from page_header(get_raw_page('pagetest',0)); |
update:
page_items 变化
1 | Postgres=# select * from heap_page_items(get_raw_page('pagetest', 0)); |
第4条记录 t_xmax 被修改为 1846 , t_xmax 只有被delete的时候才会被标记, 而第五条记录发现t_xmin被修改为1846,说明是同一事务进行操作,说明进行的操作为update;
insert
page_header变化
1 | Postgres=# select * from page_header(get_raw_page('pagetest',0)); |
进行insert操作insert into pagetest values ( 7, '7', 'g' );
1 | lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid |
比较发现 lsn, lower, upper 三项发生了变化。
lsn 记录最后一次对page修改的xlog记录id
lower 每次从前向后申请 4Byte字节
upper 每次从后向前从Free Space申请39字节,因为字节对齐 申请40Byte;
index
查看索引类型:
1 | Postgres=# \d pagetest |
查看Page Header;
1 | Postgres=# SELECT * from page_header(get_raw_page('pagetest_id_idx', 0)); |
根据pageinspect提供相关索引进行查看信息:
不同类型索引使用的存储结构不同; 常见索引如下:
- brin
- btree
- gin
- gist
- hash
- spgist
hexdump 解析数据文件
可以对照pageinspesct 进行信息对照。
1 | shell$ hexdump -C 16406 -s 0 -n 8 |
- 前4Byte是TimeLineID 0x00000000
- 后4Byte是 0x0185e030
组合 0/185e030
ItemIdData:
1 | hexdump -C 16406 -s 24 -n 4 |
第一个Tuple偏移8152Byte;
1 | hexdump -C 16406 -s 8152 -n 39 |
itemidData->lp_len = 39;
t_hoff = 24; ==> hexdump -C 16406 -s 8152+22 -n 1
第一条Tuple元组信息 size = 39 - 24 = 15;
1 | hexdump -C 16406 -s 8152+t_hoff -n (itemidData->lp_len - t_hoff) |
那么第二条,则是, 因为实际数据申请,从文件向前申请。
1 | hexdump -C 16406 -s 8152+t_hoff-itemidData->lp_len -n 15 |
补充:
Tuple中变长字段尤其需要注意,因为其保存大尺寸数据,很容易超过Page大小,Postgresql不允许一行数据跨Page存储。因此引入TOAST机制来处理。采用压缩 + 切片的方法将大数据分割成小数据,变成多行保存在对用的toast表中,并对它们进行索引。所以从文件角度来看,一行大数据可能被分割存在于多个数据文件中。(普通表与toast表分别对应的文件)。
查看Pagetest普通表与它的toast表:
1 | Postgres=# select oid, relname, relnamespace, relfilenode, reltablespace, relpages, reltuples, reltoastrelid, relkind from pg_class where relname = 'pagetest'; |
所有的toast表都在模式(schema)pg_toast下,查看toast表结构和数据:
1 | \d+ pg_toast.pg_toast_2606 |
- chunk_id 具有同样的chunk_id值得所有行组成原表的toast字段的一行数据。
- chunk_seq 表示该行切片数据在完整数据中的顺序。
- chunk_data 实际存储的切片数据。
一张普通的表最多对应一张toast表,但是每个字段根据不同的数据类型具有不同的toast策略。修改toast策略,不会影响已有的数据存放方式。
- plain:不压缩,不切片。不会触发toast机制。
- extended:优先压缩,后切片。
- external:不压缩,只切片。用空间换时间的策略,提高操作效率。
- main:尽量不切片。
1 | Postgres=# \d+ pagetest |
当将要保存到表的一行数据超过一个阈值(BLOCK/4 ~ 2KB)时,会触发toast机制,如果产生切片则保存到对应的toast表中。
Pageinit
1 | // include/pg_config.h |
数据更新
数据更新的最终结果就是把变更的内容持久化到硬盘上。一次事务可能涉及多个page的改变。如果直接刷盘会产生多次随机写,付出的代价相对较高。所以在涉及层面采用写缓冲,异步和WAL来解决性能上的问题,并且WAL机制也保证了事务的持久性和数据的完整性,还使得在线备份和时间回复(PITR)称为可能。
事务过程
存储引擎在执行更新类操作时,首先开启事务,然后将变更后的内容记入Wal buffer, 并更新shared buffer中的page,最后提交事务,相应客户端。
shared buffer中的page发生更新后被标记为dirty page,之后由其他进程完成磁盘回写。
事务提交可以设置是否等待WAL记录被写入磁盘。控制参数为synchronous_commit。
设置off时对性能有一定提升,程序将不会等待本次WAL记录写入到磁盘,而直接影响客户端。
WAL写入磁盘
默认情况下,WAL是在事务提交时写入磁盘。以保证提交的事务不会因为宕机而丢失。当synchronous_commit设置为off时,WAL写入磁盘的过程由Wal write进程异步完成,这种情况下宕机存在丢失最近提交的事务风险。但不会影响数据库的一致性。
Dirty Page回写磁盘
一般情况下,每隔一段时间writer进程就会扫描shared buffer, 将Dirty Page刷入操作系统的Page Cache,并记录处理过的Dirty Page,之后由系统内核处理page cache刷入磁盘,当空闲内存低于阈值 或 脏页驻留时间超过阈值时,触发回写磁盘。
另一种情况是由checkpointer进程定义执行checkpoint,强制当前处理过的dirty page回写磁盘,保证数据持久化,为事务前滚提供参照点。默认情况下(full_page_write = on), checkpoint发生后,在page第一次写的时候会将整个page写入WAL,以保证宕机或者备份拷贝时,产生的partial wirte page可以保证被正确的完整的page覆盖来修复。
空间回收
在PostgreSQL中,delete和一些的update(例如,涉及到索引字段的更新)会使表和索引中的旧行(tuple)被废弃掉。这些废弃的tuple称之为dead tuple,他们仍然占用空间,称为一种资源浪费。
PostgreSQL的VACUUM机制就是用来回收这些dead tuple空间的,用来回收利用。
Full Vacuum
1
2
3VACUUM FULL VERBOSE;
VACUUM FULL t_user_info;
ANALYZE;Lazy Vacuum
1
2VACUUM t_user_info;
VACUUM ANALYZE t_user_info;