深入浅出 PG 数据膨胀
多版本并发控制 MVCC
多版本并发控制,是数据库管理系统常用的一种并发控制,每个事务读到的数据项都是一个历史快照(snapshot)并依赖于实现的隔离级别。写操作不覆盖已有数据项,而是创建一个新的版本,直至所在操作提交时才变为可见。Postgres(以下简称PG)中的多版本,是保存在各个表中的,相比较 MySQL 是保存在全局中的。
隐藏的版本信息
PG 各个表独立保存着多版本信息,也意味着在每条记录都有版本号。通过一些技巧可以挖掘到这些隐藏的信息。
pg_ninja=# \d employee
Table "public.employee"
Column | Type | Modifiers
---------+-----------------------+-----------
emp_id | integer | not null
name | character varying(20) |
dept_id | integer |
Indexes:
"employee_pkey" PRIMARY KEY, btree (emp_id)
在 pg_attribute
中可以看到几个隐藏的字段:
pg_ninja=# SELECT attname, format_type (atttypid, atttypmod)
pg_ninja-# FROM pg_attribute
pg_ninja-# WHERE attrelid::regclass::text='employee'
pg_ninja-# ORDER BY attnum;
attname | format_type
----------+-----------------------
tableoid | oid
cmax | cid
xmax | xid
cmin | cid
xmin | xid
ctid | tid
emp_id | integer
name | character varying(20)
dept_id | integer
(9 rows)
通过 SELECT
返回隐藏字段的值:
pg_ninja=# SELECT txid_current();
txid_current
--------------
1088042
(1 row)
pg_ninja=# INSERT into employee VALUES (9,'avi',9);
INSERT 0 1
pg_ninja=# SELECT xmin,xmax,* FROM employee where emp_id = 9;
xmin | xmax | emp_id | name | dept_id
---------+------+--------+------+---------
1088043 | 0 | 9 | avi | 9
(1 row)
xmin:记录了创建(INSERT)记录的事务 ID(简称 xid)。
通过 SELECT txid_current()
可以得到当前的 xid,下一个 INSERT 的 xid 是当前 xid+1
就是 1088043,与保存在 xmin
中的值相等。
xmax:记录了删除(DELETE)记录的 xid。
删除之后该记录就不能简单的 SELECT 返回,这里通过两个会话在验证 xmax 的值。
在终端 A 中开始事务,删除但不提交。
pg_ninja=# begin;
BEGIN
pg_ninja=# SELECT txid_current();
txid_current
--------------
1088046
(1 row)
pg_ninja=# DELETE FROM employee where emp_id = 9;
DELETE 1
在终端 B 中连接数据库,查询 xmax 值。
pg_ninja=# SELECT xmin, xmax, * FROM employee where emp_id = 9;
xmin | xmax | emp_id | name | dept_id
---------+---------+--------+------+---------
1088043 | 1088046 | 9 | avi | 9
(1 row)
因为在一个事务中,SELECT txid_current()
就是删除语句的 xid 即 1088046,与保存在 xmax
中的值相等。
“删除” 的记录
删除的记录在 PG 中并没有真正的删除。通过 pageinspect 模块可以窥视 PG 底层的存储秘密。
pg_ninja=# CREATE EXTENSION pageinspect;
pg_ninja=# INSERT into employee values (generate_series(1,10),'avi',1);
INSERT 0 10
pg_ninja=# SELECT t_xmin, t_xmax, t_data FROM heap_page_items(get_raw_page('employee', 0));
t_xmin | t_xmax | t_data
---------+--------+----------------------------
1089069 | 0 | \x010000000961766901000000
1089069 | 0 | \x020000000961766901000000
1089069 | 0 | \x030000000961766901000000
1089069 | 0 | \x040000000961766901000000
1089069 | 0 | \x050000000961766901000000
1089069 | 0 | \x060000000961766901000000
1089069 | 0 | \x070000000961766901000000
1089069 | 0 | \x080000000961766901000000
1089069 | 0 | \x090000000961766901000000
1089069 | 0 | \x0a0000000961766901000000
(10 rows)
删除其中的五条记录。
pg_ninja=# DELETE FROM employee where emp_id > 5;
DELETE 5
pg_ninja=# SELECT t_xmin, t_xmax, t_data FROM heap_page_items(get_raw_page('employee', 0));
t_xmin | t_xmax | t_data
---------+---------+----------------------------
1089069 | 0 | \x010000000961766901000000
1089069 | 0 | \x020000000961766901000000
1089069 | 0 | \x030000000961766901000000
1089069 | 0 | \x040000000961766901000000
1089069 | 0 | \x050000000961766901000000
1089069 | 1089147 | \x060000000961766901000000
1089069 | 1089147 | \x070000000961766901000000
1089069 | 1089147 | \x080000000961766901000000
1089069 | 1089147 | \x090000000961766901000000
1089069 | 1089147 | \x0a0000000961766901000000
(10 rows)
可以看到删除的记录依然在底层存储中。
更新剩下的五条记录。
pg_ninja=# UPDATE employee SET emp_name = 'avii';
UPDATE 5
pg_ninja=# SELECT t_xmin, t_xmax, t_data FROM heap_page_items(get_raw_page('employee', 0));
t_xmin | t_xmax | t_data
---------+---------+------------------------------------
1089069 | 1089148 | \x010000000961766901000000
1089069 | 1089148 | \x020000000961766901000000
1089069 | 1089148 | \x030000000961766901000000
1089069 | 1089148 | \x040000000961766901000000
1089069 | 1089148 | \x050000000961766901000000
1089069 | 1089147 | \x060000000961766901000000
1089069 | 1089147 | \x070000000961766901000000
1089069 | 1089147 | \x080000000961766901000000
1089069 | 1089147 | \x090000000961766901000000
1089069 | 1089147 | \x0a0000000961766901000000
1089148 | 0 | \x010000000b6176696900000001000000
1089148 | 0 | \x020000000b6176696900000001000000
1089148 | 0 | \x030000000b6176696900000001000000
1089148 | 0 | \x040000000b6176696900000001000000
1089148 | 0 | \x050000000b6176696900000001000000
(15 rows)
PG 更新的逻辑是删除并创建,可以看到这里返回了15条记录,其中10条记录了 t_xmax
(其中5条是更新时删除的),另外新创建的5条记录的 t_xmin
值是新的 xid,而 t_xmax
值为 0。
Vacuum
很显然,维护这样的版本信息的存储成本是巨大的,尤其是更新频繁的表。PG 解决方案就是 Vacuum。这些删除的数据在 PG 中叫做 dead tuple。一旦没有任何事务使用这些数据,这些数据就可以安全的被删除。Vacuum 就是承担了清理这些数据的工作。
从库的热反馈 (Hot Standby Feedback 以下简称 HSF)
在配置了主从后,如果打开了 HSF,HSF 会将当前从库的最小 xid 发给主库,目的是解决同步冲突问题。如果从库在使用某些记录,即使在主库中已经删除,并且主库中没有任何事务使用该记录,Vacuum 依然会保留该记录。
Reference:
- https://www.percona.com/blog/2018/08/06/basic-understanding-bloat-vacuum-postgresql-mvcc/
- https://www.postgresql.org/docs/9.6/routine-vacuuming.html