PostgreSQL 事务吞吐量高带来的数据冻结

John Doe 十二月 13, 2024

摘要:在本文中,我们将了解 PostgreSQL 事务 ID 快速分配带来的数据冻结问题,以及相关的解决方案。

目录

事务号分配场景: 用例

下面我们在 PostgreSQL 的数据库中,创建一个表和一些用于数据操作的函数。

CREATE TABLE t_large (id integer, name text);
CREATE UNIQUE INDEX large_idx ON t_large (id);

CREATE OR REPLACE FUNCTION f_base(v int)
RETURNS void AS $$
BEGIN
  INSERT INTO t_large (id, name) VALUES(v, 'xxx');
EXCEPTION
  WHEN UNIQUE_VIOLATION THEN
    RAISE WARNING 'Exception for value: %', v;
END; $$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION f_loop(b int, e int)
RETURNS void AS $$
BEGIN
  FOR i IN b .. e LOOP
    PERFORM f_base(i);
  END LOOP;
END; $$ LANGUAGE plpgsql;

让我们通过存储函数插入一些数据,查看操作能否执行成功,在各个子事务中的事务 ID 是否会发生变化。

SELECT f_loop(1, 4);

SELECT ctid, xmin, * FROM t_large;
 ctid  | xmin | id | name
-------+------+----+------
 (0,1) |  743 |  1 | xxx
 (0,2) |  744 |  2 | xxx
 (0,3) |  745 |  3 | xxx
 (0,4) |  746 |  4 | xxx
(4 rows)

事务号分配场景: 内部原理

下面是我们在 PostgreSQL 中运行上面的存储函数时,数据库系统全局事务 ID 发生的变化:

XID Growth

  1. PostgreSQL 使用了子事务,来实现存储函数中的异常处理代码块。
  2. 每个事务/子事务都有一个事务 ID,它有点像数据库系统的运行时间。
  3. 每行都有两个默认的隐藏属性 xmin 和 xmax,这两个属性分别保存了创建和更改它们的事务/子事务的 ID。
  4. 系统中最多出现的事务 ID 数目约为 20 亿,在超过该数目后,MVCC 的机制将无法正常工作。

事务号分配场景: 思考

问题影响

  1. 需要定期扫描所有数据行,进行事务 ID 的冻结操作。
  2. 如果再叠加长事务的发生,会出现事务 ID 耗尽,数据库停止服务。
  3. 容易出现 pg_xact、pg_subtrans 缓存访问的等待事件,系统整体性能下降。

应对方案

  1. 配置监控告警,检查事务 ID 耗尽
  2. 配置监控告警,检查子事务的使用情况
  3. 配置监控告警,检查长时间运行的事务

Redrock Postgres 的解决方案

Redrock Postgres 是基于撤消日志记录位置实现的子事务,这些子事务不过是事务执行过程中的一个临时标记,它们并不需要分配独立的事务 ID,不会对系统造成任何风险。

另外,在 Redrock Postgres 中,事务 ID 类型xid是 8 字节的长度,可以近乎无限使用,无需担心事务 ID 用尽的风险。

下面我们在 Redrock Postgres 的数据库中,创建和上面相同的表和相同的操作函数,通过存储函数插入同样的数据量,查看事务 ID 在各个子事务中是否会发生变化。

SELECT f_loop(1, 4);

SELECT ctid, rowxid, * FROM t_large;
 ctid  |   rowxid    | id | name
-------+-------------+----+------
 (0,1) | (8,64,4988) |  1 | xxx
 (0,2) | (8,64,4988) |  2 | xxx
 (0,3) | (8,64,4988) |  3 | xxx
 (0,4) | (8,64,4988) |  4 | xxx
(4 rows)

从上面的测试结果来看,在我们对表t_large进行数据插入完后,数据库系统只消耗了一个全局事务 ID。子事务使用了和所属事务块同样的事务 ID,它们并不需要分配独立的事务 ID。