searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

PostgreSQL多版本控制(二)

2023-08-11 07:06:59
67
0

1. Commit Log(clog)日志分析

在进行可见性判断时,需要获取事务的状态。PostgreSQL将事务的状态记录在clog中。同时为了提高访问速度,共享内存中也开辟了一块区域用来储存clog buffer,多数情况下不需要读取磁盘中的clog文件。clog文件保存在 $PGDATA/pg_xact目录中。

1.1 clog buffer

clog的大小定义如下:
 
 
其占用 NBuffers 的 1/512,最大为128个页,最小为4个页。上面提到的事务的四种状态分别可以用2bit来表示,则四个事务占用8bit(1字节)。

1.2 根据事务ID查看在clog日志中的事务状态

事务ID并不是在事务开始时就被分配,首先会为其分配一个虚拟事务号,只有当数据发生变化时才会真正分配事务ID,当事务提交或者回滚时,其事务状态会被写入clog。通过事务ID获取其在clog中状态的方法是:
    #define TransactionIdToPage(xid)    ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE)        //页号
    #define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE)        // 页内偏移
    #define TransactionIdToByte(xid)    (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)  // 页内的字节号
    #define TransactionIdToBIndex(xid)  ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE)        // 字节中偏移

如下是根据事务ID查找clog中对应的事务状态的示意图:

如果每次读取tuple都要去clog中查找对应事务的状态,效率会非常低,因此一种优化方法为:在tuple上的t_infomask字段,其中的标志位帮助加快判断速度。如果标志位HEAP_XMIN_COMMITTED被设置,就知道xmin代表的事务已提交;如果HEAP_XMAX_COMMITTED被设置,就知道xmax代表的事务已提交,不需要clog文件中去判断。即设置了Hint Bits提示位。

1.3 Hint Bits

把事务状态记录在元组头中,避免频繁访问clog文件,提高了性能。Hint Bits采用延迟更新策略,并不会在事务提交或者回滚时主动更新所有操作过的tuple。等到第一次访问(VACUUM,DML,SELECT)该tuple并进行可见性判断时。
  • 如果Hint Bits已经设置,则直接读取
  • 如果Hint Bits没有设置,则调用方式从clog中读取事务状态,如果事务状态为COMMITED或者ABORTED,则将Hint Bits设置到t_infomask字段;如果事务状态为IN_PROGRESS,此时无需设置Hint Bits。设置的字段如下:
 
如下是HeapTupleSatisfiesMVCC函数中设置Hint Bits的示例:
 

1.4 Hint Bits 和 WAL 日志

在发生设置Hint Bits的时候,是有可能需要写WAL日志的,具体机制为:在开启了checksu或者wal_log_hints为真时,如果checkpoint后第一次使得页面修改(即dirty),这种情况下需要将整个页面的内容写到WAL日志中,之所以写整个页面,是为了避免部分写入导致数据的checksum异常。
还有一种特殊情况,由于Hint Bits是针对单个tuple的,因此如果并行的会话对一个页面的多个tuple设置Hint Bits时,可能导致这个页面在多次checkpoint时被多次写入WAL中,或者在两个checkpoint之间,被多次刷到OS dirty page。这也就解释了有时候执行select后也能改变页面的Hint Bits值,从而有WAL日志写入,这会在一定程度上增加WAL日志的占用空间。

2.拓展知识

2.1 xmax 和行级锁

事实上 xmax 储存的并不只是 0 (tuple插入后删除前)和事务ID(被删除时),实际上它还可以储存行锁的信息以及持有该锁的事务号。对于同一个tuple,可能有多个事务要对其进行更新或者修改,在这个过程中会持有该行的行锁。PostgreSQL中共有四种行锁
  • FOR UPDATE:delete和修改主键、唯一键值的UPDATE会获得这种锁模式
  • FOR NO KEY UPDATE:不修改主键、唯一键值的UPDATE会获得这种锁模式
  • FOR SHARE:读该行,不允许对行进行更新
  • FOR KEY SHARE:读该行键值,当允许对除键外的其他字段更新。在外键检查时使用该锁。
每种行级锁的冲突模式如下:

2.2 行锁上锁流程

PG 使用LockRows算子上行锁,其对下层查询执行之后得到的所有tuple顺序上锁。
 
 
函数 heap_lock_tuple 会根据传入的锁类型进行不同的处理。函数内部会调用上面提到的 HeapTupleSatisfiesUpdate 函数,查看tuple的状态,情况如下:
  • 如果 HeapTupleSatisfiesUpdate 返回的为以下三种情况,说明当前tuple已经存在行锁。根据传入的行锁模式和 该tuple的t_infomask 中的锁模式判断是否冲突,如果冲突则将 require_sleep 设置为真,表示需要等待,否则不需要等待。

  • 如果返回值为TM_OK,说明可以对该tuple进行上锁,调用compute_new_amax_infomask计算新的infomask和infomask2信息,并保存到tuple头部。为了区分xmax保存的是修改tuple的事务还是持有该tuple的行锁的事务,需要设置infomask中的HEAP_XMAX_KEYSHR_LOCK、HEAP_XMAX_EXCL_LOCK和HEAP_XMAX_SHR_LOCK信息,同时如果是FOR UPDATE锁还需要设置infomask2中HEAP_KEYS_UPDATED位。

2.3 多个事务锁住tuple的情况

如果有多个事务锁住tuple,那么该tuple中的infomask中的xmax无法储存多个持有锁的事务ID,这时xmax内保存的就是MultixactID结构,该结构是将多个事务ID组合起来形成的结构,此时HEAP_XMAX_IS_MULTI字段为真。

只有多个事务对同一个tuple持有共享锁才真正产生multixactID,下面模拟一下这种情况。两个会话中同时开启一个事务,并且执行如下语句:
begin;
select * from t for share;
在会话1中执行如下操作,此命令会被阻塞:
update t set a = 2;
此时会话2中提交commit后,会话1的update操作会执行成功。查看此时元组的xmax和infomask的值。
 
 
其中HEAP_XMAX_IS_MULTI是0x1000,十进制为4096,(t_infomask & 4096) != 0可以计算出元组是否使用了multixactID。同时可以使用函数 pg_get_multixact_members查看multixactID是由哪些事务ID组成的,以及锁的类型:
 

2.4 Multixact相关机制

由于MultixactID是多对一的映射关系,因此需要在事务ID中标记哪一段映射到一个MultixactID,在映射的过程中需要储存两种信息:标识一段事务ID的偏移量,以及这段偏移量的大小。为了对MultixactID的分配进行维护,定义了如下的数据结构
typedef struct MultiXactStateData
{
    MultiXactId nextMXact;  //下一个可分配的 MultixactID
    MultiXactOffset nextOffset; //下一个对应 MultiXactID 的起始偏移量
    MultiXactId perBackendXactIds[FLEXIBLE_ARRAY_MEMBER];  // MultiXactID队列起始位置
} MultiXactStateData;
另外还需要定义一个统一的接口来操作MultiXactID,即储存这种多对一的映射关系,PostgreSQL中使用如下结构体来完成此工作。
typedef struct mxactCacheEnt
{
    struct mXactCacheEnt *next;
    MultiXactId          multi;
    int                  nxids;
    TransactionId        xids[];
}
MultiXact相关的日志使用两个SLRU缓冲池来实现,分别是 MultiXactOffsetCtl 和 MultiXactMemberCtl,分别记录Members 和 Offsets。从一个MultiXactID映射到具体的储存位置是通过如下变换来完成的:
/* 获取 Offset 对应记录所在页面 */
#define MULTIXACT_OFFSETS_PER_PAGE (BLCKSZ / sizeof(MultiXactOffset))

/* 获取 Offset 对应记录位于页面内的偏移量 */
#define MultiXactIdToOffsetPage(xid) \
        ((xid) / (MultiXactOffset) MULTIXACT_OFFSETS_PER_PAGE)

/* 获取 Members 对应记录所在页面 */  
#define MultiXactIdToOffsetEntry(xid) \
        ((xid) % (MultiXactOffset) MULTIXACT_OFFSETS_PER_PAGE)

/* 获取 Members 对应记录位于页面内的偏移量 */
#define MultiXactIdToOffsetSegment(xid) (MultiXactIdToOffsetPage(xid) / SLRU_PAGES_PER_SEGMENT) 

2.5 事务回卷

事务ID一直递增,到达32位无符号数的最大值后从头开始循环(从3开始继续增加),这时候以前的事务ID都会比当前的事务ID大,在进行比较的是否会导致结果出错,即事务回卷的问题。源码比较事务新旧程度:
 
 
  • 首先使用TransactionIdIsNormal函数判断是否存在特殊事务,如果包含了特殊事务ID,则比较的结果为它们比任何普通事务ID都早
  • 如果两个都是普通事务ID,则将二者相减的结果转换为int32类型,即得到的结果是一个有符号的数字。
  • 会出问题的情况:例如两个事务ID分别为2^31+101和100,相减结果为2^31+1,这个值转换为int32就是-1,表示100表示的事务反而更新。但是如果100真的是一个非常早的事务,早于2^31+101,那么判断结果就是错的。因此PG中必须保证最早和最晚两个有效的事务之间的差距不能超过2^31。为了达到这个原则,PG引入了事务冻结的原理。即将tuple标记为已经冻结,在t_infomask中的HEAP_XMIN_FROZEN标志位,从而使得VACUUM机制可以对这些数据进行清理。

3. PostgreSQL中MVCC的优劣势

3.1 优势

  • 使用MVCC,读写操作不会互相阻塞,提高了并发访问下的性能
  • 事务的回滚可以立即完成,无论事务进行了多少操作
  • 数据可以大量更新,没有回滚段,不必考虑回滚段是否会被耗尽

3.2 劣势

  • 旧版本数据需要清理,PostgreSQL提供了VACUUM命令进行清理
  • 旧版本的数据会导致查询更慢,因为旧版本的数据仍然存在,查询时需要扫描的数据更多了
0条评论
0 / 1000
g****n
4文章数
1粉丝数
g****n
4 文章 | 1 粉丝
g****n
4文章数
1粉丝数
g****n
4 文章 | 1 粉丝
原创

PostgreSQL多版本控制(二)

2023-08-11 07:06:59
67
0

1. Commit Log(clog)日志分析

在进行可见性判断时,需要获取事务的状态。PostgreSQL将事务的状态记录在clog中。同时为了提高访问速度,共享内存中也开辟了一块区域用来储存clog buffer,多数情况下不需要读取磁盘中的clog文件。clog文件保存在 $PGDATA/pg_xact目录中。

1.1 clog buffer

clog的大小定义如下:
 
 
其占用 NBuffers 的 1/512,最大为128个页,最小为4个页。上面提到的事务的四种状态分别可以用2bit来表示,则四个事务占用8bit(1字节)。

1.2 根据事务ID查看在clog日志中的事务状态

事务ID并不是在事务开始时就被分配,首先会为其分配一个虚拟事务号,只有当数据发生变化时才会真正分配事务ID,当事务提交或者回滚时,其事务状态会被写入clog。通过事务ID获取其在clog中状态的方法是:
    #define TransactionIdToPage(xid)    ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE)        //页号
    #define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE)        // 页内偏移
    #define TransactionIdToByte(xid)    (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)  // 页内的字节号
    #define TransactionIdToBIndex(xid)  ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE)        // 字节中偏移

如下是根据事务ID查找clog中对应的事务状态的示意图:

如果每次读取tuple都要去clog中查找对应事务的状态,效率会非常低,因此一种优化方法为:在tuple上的t_infomask字段,其中的标志位帮助加快判断速度。如果标志位HEAP_XMIN_COMMITTED被设置,就知道xmin代表的事务已提交;如果HEAP_XMAX_COMMITTED被设置,就知道xmax代表的事务已提交,不需要clog文件中去判断。即设置了Hint Bits提示位。

1.3 Hint Bits

把事务状态记录在元组头中,避免频繁访问clog文件,提高了性能。Hint Bits采用延迟更新策略,并不会在事务提交或者回滚时主动更新所有操作过的tuple。等到第一次访问(VACUUM,DML,SELECT)该tuple并进行可见性判断时。
  • 如果Hint Bits已经设置,则直接读取
  • 如果Hint Bits没有设置,则调用方式从clog中读取事务状态,如果事务状态为COMMITED或者ABORTED,则将Hint Bits设置到t_infomask字段;如果事务状态为IN_PROGRESS,此时无需设置Hint Bits。设置的字段如下:
 
如下是HeapTupleSatisfiesMVCC函数中设置Hint Bits的示例:
 

1.4 Hint Bits 和 WAL 日志

在发生设置Hint Bits的时候,是有可能需要写WAL日志的,具体机制为:在开启了checksu或者wal_log_hints为真时,如果checkpoint后第一次使得页面修改(即dirty),这种情况下需要将整个页面的内容写到WAL日志中,之所以写整个页面,是为了避免部分写入导致数据的checksum异常。
还有一种特殊情况,由于Hint Bits是针对单个tuple的,因此如果并行的会话对一个页面的多个tuple设置Hint Bits时,可能导致这个页面在多次checkpoint时被多次写入WAL中,或者在两个checkpoint之间,被多次刷到OS dirty page。这也就解释了有时候执行select后也能改变页面的Hint Bits值,从而有WAL日志写入,这会在一定程度上增加WAL日志的占用空间。

2.拓展知识

2.1 xmax 和行级锁

事实上 xmax 储存的并不只是 0 (tuple插入后删除前)和事务ID(被删除时),实际上它还可以储存行锁的信息以及持有该锁的事务号。对于同一个tuple,可能有多个事务要对其进行更新或者修改,在这个过程中会持有该行的行锁。PostgreSQL中共有四种行锁
  • FOR UPDATE:delete和修改主键、唯一键值的UPDATE会获得这种锁模式
  • FOR NO KEY UPDATE:不修改主键、唯一键值的UPDATE会获得这种锁模式
  • FOR SHARE:读该行,不允许对行进行更新
  • FOR KEY SHARE:读该行键值,当允许对除键外的其他字段更新。在外键检查时使用该锁。
每种行级锁的冲突模式如下:

2.2 行锁上锁流程

PG 使用LockRows算子上行锁,其对下层查询执行之后得到的所有tuple顺序上锁。
 
 
函数 heap_lock_tuple 会根据传入的锁类型进行不同的处理。函数内部会调用上面提到的 HeapTupleSatisfiesUpdate 函数,查看tuple的状态,情况如下:
  • 如果 HeapTupleSatisfiesUpdate 返回的为以下三种情况,说明当前tuple已经存在行锁。根据传入的行锁模式和 该tuple的t_infomask 中的锁模式判断是否冲突,如果冲突则将 require_sleep 设置为真,表示需要等待,否则不需要等待。

  • 如果返回值为TM_OK,说明可以对该tuple进行上锁,调用compute_new_amax_infomask计算新的infomask和infomask2信息,并保存到tuple头部。为了区分xmax保存的是修改tuple的事务还是持有该tuple的行锁的事务,需要设置infomask中的HEAP_XMAX_KEYSHR_LOCK、HEAP_XMAX_EXCL_LOCK和HEAP_XMAX_SHR_LOCK信息,同时如果是FOR UPDATE锁还需要设置infomask2中HEAP_KEYS_UPDATED位。

2.3 多个事务锁住tuple的情况

如果有多个事务锁住tuple,那么该tuple中的infomask中的xmax无法储存多个持有锁的事务ID,这时xmax内保存的就是MultixactID结构,该结构是将多个事务ID组合起来形成的结构,此时HEAP_XMAX_IS_MULTI字段为真。

只有多个事务对同一个tuple持有共享锁才真正产生multixactID,下面模拟一下这种情况。两个会话中同时开启一个事务,并且执行如下语句:
begin;
select * from t for share;
在会话1中执行如下操作,此命令会被阻塞:
update t set a = 2;
此时会话2中提交commit后,会话1的update操作会执行成功。查看此时元组的xmax和infomask的值。
 
 
其中HEAP_XMAX_IS_MULTI是0x1000,十进制为4096,(t_infomask & 4096) != 0可以计算出元组是否使用了multixactID。同时可以使用函数 pg_get_multixact_members查看multixactID是由哪些事务ID组成的,以及锁的类型:
 

2.4 Multixact相关机制

由于MultixactID是多对一的映射关系,因此需要在事务ID中标记哪一段映射到一个MultixactID,在映射的过程中需要储存两种信息:标识一段事务ID的偏移量,以及这段偏移量的大小。为了对MultixactID的分配进行维护,定义了如下的数据结构
typedef struct MultiXactStateData
{
    MultiXactId nextMXact;  //下一个可分配的 MultixactID
    MultiXactOffset nextOffset; //下一个对应 MultiXactID 的起始偏移量
    MultiXactId perBackendXactIds[FLEXIBLE_ARRAY_MEMBER];  // MultiXactID队列起始位置
} MultiXactStateData;
另外还需要定义一个统一的接口来操作MultiXactID,即储存这种多对一的映射关系,PostgreSQL中使用如下结构体来完成此工作。
typedef struct mxactCacheEnt
{
    struct mXactCacheEnt *next;
    MultiXactId          multi;
    int                  nxids;
    TransactionId        xids[];
}
MultiXact相关的日志使用两个SLRU缓冲池来实现,分别是 MultiXactOffsetCtl 和 MultiXactMemberCtl,分别记录Members 和 Offsets。从一个MultiXactID映射到具体的储存位置是通过如下变换来完成的:
/* 获取 Offset 对应记录所在页面 */
#define MULTIXACT_OFFSETS_PER_PAGE (BLCKSZ / sizeof(MultiXactOffset))

/* 获取 Offset 对应记录位于页面内的偏移量 */
#define MultiXactIdToOffsetPage(xid) \
        ((xid) / (MultiXactOffset) MULTIXACT_OFFSETS_PER_PAGE)

/* 获取 Members 对应记录所在页面 */  
#define MultiXactIdToOffsetEntry(xid) \
        ((xid) % (MultiXactOffset) MULTIXACT_OFFSETS_PER_PAGE)

/* 获取 Members 对应记录位于页面内的偏移量 */
#define MultiXactIdToOffsetSegment(xid) (MultiXactIdToOffsetPage(xid) / SLRU_PAGES_PER_SEGMENT) 

2.5 事务回卷

事务ID一直递增,到达32位无符号数的最大值后从头开始循环(从3开始继续增加),这时候以前的事务ID都会比当前的事务ID大,在进行比较的是否会导致结果出错,即事务回卷的问题。源码比较事务新旧程度:
 
 
  • 首先使用TransactionIdIsNormal函数判断是否存在特殊事务,如果包含了特殊事务ID,则比较的结果为它们比任何普通事务ID都早
  • 如果两个都是普通事务ID,则将二者相减的结果转换为int32类型,即得到的结果是一个有符号的数字。
  • 会出问题的情况:例如两个事务ID分别为2^31+101和100,相减结果为2^31+1,这个值转换为int32就是-1,表示100表示的事务反而更新。但是如果100真的是一个非常早的事务,早于2^31+101,那么判断结果就是错的。因此PG中必须保证最早和最晚两个有效的事务之间的差距不能超过2^31。为了达到这个原则,PG引入了事务冻结的原理。即将tuple标记为已经冻结,在t_infomask中的HEAP_XMIN_FROZEN标志位,从而使得VACUUM机制可以对这些数据进行清理。

3. PostgreSQL中MVCC的优劣势

3.1 优势

  • 使用MVCC,读写操作不会互相阻塞,提高了并发访问下的性能
  • 事务的回滚可以立即完成,无论事务进行了多少操作
  • 数据可以大量更新,没有回滚段,不必考虑回滚段是否会被耗尽

3.2 劣势

  • 旧版本数据需要清理,PostgreSQL提供了VACUUM命令进行清理
  • 旧版本的数据会导致查询更慢,因为旧版本的数据仍然存在,查询时需要扫描的数据更多了
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
1
0