PostgreSQL——CLOG日志系统解析

日志

目的是以安全的方式记录数据库变更的历史,当出错后可以用日志来重建对数据库所作更新的过程从而恢复到一致的状态,保证数据库的一致性和完整性。

用到了XLOG和CLOG,分别是事务日志和提交日志。

  • XLOG
    • 是通常意义的日志记录,记录更新过程和事务最终状态
  • CLOG
    • 记录事务的最终状态,比XLOG小也比XLOG高效

为了支持嵌套,引入了SUBTRANS日志记录父事务的ID从而通过一个事务可以递归查找到父事务,然而不能从父事务查找到子事务。

为了支持多版本并发控制,引入了MultiXactID组合事务ID来记录事务的组合关系。

日志中的LSN,log sequence number。

  • 每个页面都会有一个LSN,保存最新的影响该页的LOG。
  • 当写出一个脏页时,需要保证xlog已经被flush到对应的LSN。这样当进行WAL回放时,如果LSN大于等于日志项的WAL位置,则说明该页面对应的时间点在该日志之后,则不需要再次应用。如此可以加速WAL回放速度。

日志的存储

日志通过日志文件存放,设置了日志缓冲区,先将日志记录写到内存的缓冲区,满了之后再按块写道外存。

缓冲区使用SLRU缓冲区,采用LRU算法作为页面置换算法的缓冲池。

SLRU缓冲池

每一个日志文件为一个段,一个段由32个磁盘页面组成,每个磁盘页面8KB,每一个段文件以段号命名。日志页面号即可计算具体的段文件和偏移<Segmentno, Pageno>

SLRU缓冲池由8个缓冲区,每一个为一个页面,对应一个磁盘块,用SLRU算法调度页面。读写写日志先写到对应日志页面的缓冲区,再写入磁盘的日志段文件。

在对缓冲区进行I/O时,我们在释放控制锁之前独占地获取每个缓冲区的锁。在完成I/O、重新获取控制锁并更新共享状态之后,释放每个缓冲区的锁。(在这里不可能发生死锁,因为我们永远不会在其他进程正在对同一缓冲区进行I/O时尝试启动I/O。)为了等待I/O完成,释放控制锁,以共享模式获取每个缓冲区的锁,立即释放每个缓冲区的锁,重新获取控制锁,然后重新检查状态(因为在我们没有锁定时可能发生任意的事情)。

共享内存中的缓冲池数据结构:

 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
34
35
36
37
38
//共享内存状态
typedef struct SlruSharedData
{
	LWLock	   *ControlLock;

	//缓冲槽数量
	int			num_slots;

	//保存每个缓冲槽的信息,其中pagenumber在status为空时未定义。
	char	  **page_buffer;
	SlruPageStatus *page_status;
	bool	   *page_dirty;
	int		   *page_number;
	int		   *page_lru_count;
	LWLockPadded *buffer_locks;

	/*
	 * Optional array of WAL flush LSNs associated with entries in the SLRU
	 * pages.  If not zero/NULL, we must flush WAL before writing pages (true
	 * for pg_xact, false for multixact, pg_subtrans, pg_notify).  group_lsn[]
	 * has lsn_groups_per_page entries per buffer slot, each containing the
	 * highest LSN known for a contiguous group of SLRU entries on that slot's
	 * page.
	 */
	XLogRecPtr *group_lsn;
	int			lsn_groups_per_page;

    //每次访问时调用page_lru_count[slotno] = ++cur_lru_count,通过判断cur_lru_count - page_lru_count[slotno]判断页面有多老。
	int			cur_lru_count;

	//最新页号
	int			latest_page_number;

	/* SLRU's index for statistics purposes (might not be unique) */
	int			slru_stats_idx;
} SlruSharedData;

typedef SlruSharedData *SlruShared;

缓冲池的控制结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//不共享,指向共享内存中的活动记录
typedef struct SlruCtlData
{
	SlruShared	shared;//指向共享内存中的数据缓冲区的指针。

	/*
	 * Which sync handler function to use when handing sync requests over to
	 * the checkpointer.  SYNC_HANDLER_NONE to disable fsync (eg pg_notify).
	 */
	SyncRequestHandler sync_handler;
    
    //判断前一个int代表的页面是否比第二个页面更老的函数。
	bool		(*PagePrecedes) (int, int);

    //物理文件在磁盘的存储目录
	char		Dir[64];
} SlruCtlData;

typedef SlruCtlData *SlruCtl;

记录打开的物理磁盘文件,在写入时一次性写入:

1
2
3
4
5
6
7
typedef struct SlruWriteAllData
{
	int			num_files;		/* # files actually open */
	int			fd[MAX_WRITEALL_BUFFERS];	/* their FD's */
	int			segno[MAX_WRITEALL_BUFFERS];	/* their log seg#s */
} SlruWriteAllData;
typedef struct SlruWriteAllData *SlruWriteAll;

这记录了SimpleLruWriteAll()时用到的文件及日志段号,并定义SlruWriteAll为对应的指针类型。

主要操作

SimpleLruInit

初始化缓冲池,分配共享内存空间。

1
2
3
4
void
SimpleLruInit(SlruCtl ctl, const char *name, int nslots, int nlsns,
			  LWLock *ctllock, const char *subdir, int tranche_id,
			  SyncRequestHandler sync_handler)
  • 参数

    • ctl:本地非共享缓冲池控制结构指针
    • 缓冲池名称:用户可见
    • nslots:缓冲池中的页面数
    • ctllock:用于控制对共享控制结构的访问的轻量锁
    • subdir:相对PGDATA的用于防止数据的相对目录
    • tranche_id:轻量级锁的组编号
    • sync_handler:用于处理LRU的同步请求的函数
  • 操作

    • 1
      2
      3
      
      shared = (SlruShared) ShmemInitStruct(name,
      									  SimpleLruShmemSize(nslots, nlsns),
      									  &found);
      

      通过缓冲池名称查找并获取共享内存中的缓冲池。

      在VC中,__int64表示64位整型,其他环境用32位int

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      
      #ifdef _WIN64
          typedef unsigned __int64 size_t;
          typedef __int64          ptrdiff_t;
          typedef __int64          intptr_t;
      #else
          typedef unsigned int     size_t;
          typedef int              ptrdiff_t;
          typedef int              intptr_t;
      #endif
      typedef size_t Size;
      

      获取共享内存中Slru控制结构的大小:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      Size
      SimpleLruShmemSize(int nslots, int nlsns)
      {
      	Size		sz;
      
      	/* we assume nslots isn't so large as to risk overflow */
      	sz = MAXALIGN(sizeof(SlruSharedData));
      	sz += MAXALIGN(nslots * sizeof(char *));	/* page_buffer[] */
      	sz += MAXALIGN(nslots * sizeof(SlruPageStatus));	/* page_status[] */
      	sz += MAXALIGN(nslots * sizeof(bool));	/* page_dirty[] */
      	sz += MAXALIGN(nslots * sizeof(int));	/* page_number[] */
      	sz += MAXALIGN(nslots * sizeof(int));	/* page_lru_count[] */
      	sz += MAXALIGN(nslots * sizeof(LWLockPadded));	/* buffer_locks[] */
      
      	if (nlsns > 0)
      		sz += MAXALIGN(nslots * nlsns * sizeof(XLogRecPtr));	/* group_lsn[] */
      
      	return BUFFERALIGN(sz) + BLCKSZ * nslots;
      }
      

      在共享内存中寻找/分配一个数据结构,如果没有创建过则会为这个结构分配内存,如果创建过则会把已有的架构返回。

      return一个指向共享内存中对应位置的指针。如果是已有则会把foundPtr设置成true。

      9.0之前该函数出错会返回NULl,15.4中会抛出错误。

      1
      2
      
      void *
      ShmemInitStruct(const char *name, Size size, bool *foundPtr)
      
      • 操作:

        • 获取共享内存索引锁:

          1
          
          LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE);
          

          该锁在lwlognames.h中define,对应MainLWLockArray[1]

          1
          2
          3
          4
          
          #define ShmemIndexLock (&MainLWLockArray[1].lock)
          #define OidGenLock (&MainLWLockArray[2].lock)
          #define XidGenLock (&MainLWLockArray[3].lock)
          ...
          
        • 检查ShmemIndex是否已经被分配,没有的话先分配

        • 在ShememIndex中搜索name对应的位置

          1
          2
          
          result = (ShmemIndexEnt *)
          	hash_search(ShmemIndex, name, HASH_ENTER_NULL, foundPtr);
          

          HASH_ENTER_NULL的含义是如果内存不足则返回null,如果不存在则创建一个并返回表项。

          表项数据结构:

          1
          2
          3
          4
          5
          6
          7
          8
          
          /* this is a hash bucket in the shmem index table */
          typedef struct
          {
          	char		key[SHMEM_INDEX_KEYSIZE];//索引名
          	void	   *location;		//共享内存指针位置
          	Size		size;			//请求分配的大小
          	Size		allocated_size; //实际分配的大小
          } ShmemIndexEnt;
          
        • 如果没找到且内存不足,释放索引锁,退出。

        • 如果找到了

          • 如果找到的size与函数的输入size不相等,报错
          • 设置structPtr为result的location
        • 如果没找到需要分配

          • 调用ShememAllocaRaw分配内存并保存分配到的大小

            1
            
            structPtr = ShmemAllocRaw(size, &allocated_size);
            

            如果没分配到,报错。

          • 设置索引项的信息

            1
            2
            3
            
            result->size = size;
            result->allocated_size = allocated_size;
            result->location = structPtr;
            
        • 释放索引锁,返回structPtr

    • 如果当前处于Postmaster运行下,所需的SLRU缓冲池还不存在,初始化该缓冲池,否则理应已有。

      • 清空shared中的结构部分,将传入的信息放到结构中:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        
        memset(shared, 0, sizeof(SlruSharedData));
        
        shared->ControlLock = ctllock;
        
        shared->num_slots = nslots;
        shared->lsn_groups_per_page = nlsns;
        
        shared->cur_lru_count = 0;
        shared->slru_stats_idx = pgstat_get_slru_index(name);
        
      • 用char指针ptr指向shared。

        • 设置pagebuffer指向ptr=ptr+MAXALIGN(sizeof(SlruSharedData))
        • 设置page_status指向ptr=ptr+MAXALIGN(nslots * sizeof(char *));
        • 设置page_dirty指向ptr=ptr+MAXALIGN(nslots * sizeof(SlruPageStatus));
        • 设置page_number指向ptr=ptr+MAXALIGN(nslots * sizeof(bool));
        • 设置page_lru_count指向ptr=ptr+MAXALIGN(nslots * sizeof(int));
        • 设置buffer_locks指向ptr=ptr+MAXALIGN(nslots * sizeof(int));
        • 如果nlsns大于0,设置group_lsn指向ptr=ptr+offset += MAXALIGN(nslots * sizeof(LWLockPadded));
        • ptr指向当前所有数据结构的末尾,重复nslots次初始化每个slot对应的轻量级锁、page_buffer、page_status为SLRU_PAGE_EMPTY、page_dirty为false、page_lru_count为0。其中page_buffer从ptr开始排布。
    • 最后,设置ctr的shared为这个初始化完成/找到的shared,设置sync_handler,最后将subdir拷贝到ctl->Dir中,作为存储的目录。

slruSelectLRUPage

用于从缓冲池中替换掉一个page,从而获取一个空白槽。

1
2
static int
SlruSelectLRUPage(SlruCtl ctl, int pageno)

需要传入pageno是因为可能其它进程正在读取这个页面,这时要检查是否页面已经存在于槽中。所以返回值要么是一个已经存储这个页面的槽,或者是一个清空的槽。

1
2
3
4
5
6
for (slotno = 0; slotno < shared->num_slots; slotno++)
{
	if (shared->page_number[slotno] == pageno &&
		shared->page_status[slotno] != SLRU_PAGE_EMPTY)
		return slotno;
}

先初始化best_valid和best_invalid

1
2
3
4
5
6
7
8
int			slotno;
int			cur_count;
int			bestvalidslot = 0;	/* keep compiler quiet */
int			best_valid_delta = -1;
int			best_valid_page_number = 0; /* keep compiler quiet */
int			bestinvalidslot = 0;	/* keep compiler quiet */
int			best_invalid_delta = -1;
int			best_invalid_page_number = 0;	/* keep compiler quiet */

遍历所有槽,如果由槽的pageno就是所需且不是EMPTY状态,则直接返回。

随后开始正式寻找

 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
34
35
36
37
38
39
40
41
42
43
		for (slotno = 0; slotno < shared->num_slots; slotno++)
		{
			int			this_delta;
			int			this_page_number;

			if (shared->page_status[slotno] == SLRU_PAGE_EMPTY)
				return slotno;
            //如果找到了一个空槽,就它了。
            this_delta = cur_count - shared->page_lru_count[slotno];//这是多少次访问之前访问到的页面
            if (this_delta < 0)
			{
				shared->page_lru_count[slotno] = cur_count;
				this_delta = 0;
			}//与清理有关,如果cnt过大,则设为cur_count
            this_page_number = shared->page_number[slotno];
			if (this_page_number == shared->latest_page_number)
				continue;
            //如果这个页号是最新访问的,一定不选
            if (shared->page_status[slotno] == SLRU_PAGE_VALID)
			{
				if (this_delta > best_valid_delta ||
					(this_delta == best_valid_delta &&
		 			ctl->PagePrecedes(this_page_number,
						   best_valid_page_number)))
				{//如果这个页面不处于写入状态,可以选,判断是否比当前更老,更新bestvalidPage
					bestvalidslot = slotno;
					best_valid_delta = this_delta;
					best_valid_page_number = this_page_number;
				}
			}
			else
			{//同理,更新best_invalid_page
				if (this_delta > best_invalid_delta ||
					(this_delta == best_invalid_delta &&
		 			ctl->PagePrecedes(this_page_number,
						   best_invalid_page_number)))
				{		
					bestinvalidslot = slotno;
					best_invalid_delta = this_delta;
					best_invalid_page_number = this_page_number;
				}
			}
        }

如果best_valid_delta小于0,说明没找到。

1
2
3
4
5
if (best_valid_delta < 0)
{
	SimpleLruWaitIO(ctl, bestinvalidslot);
	continue;
}

这时选择等待它的io,因为它最老,所以按照预期它会最先完成io

如果找到了,判断是否是dirty,如果不是dirty说明可以直接用:

1
2
if (!shared->page_dirty[bestvalidslot])
	return bestvalidslot;

否则要写入外存并再次判断

SlruInternalWritePage(ctl, bestvalidslot, NULL);

SimpleLruZeroPage
1
2
int
SimpleLruZeroPage(SlruCtl ctl, int pageno)

SlruSelectLruPage选择slot号,设置相关参数

1
2
3
4
	shared->page_number[slotno] = pageno;
	shared->page_status[slotno] = SLRU_PAGE_VALID;
	shared->page_dirty[slotno] = true;
	SlruRecentlyUsed(shared, slotno);

其中SlruRecentlyUsed如下:

1
2
3
4
5
6
7
8
#define SlruRecentlyUsed(shared, slotno)	\
	do { \
		int		new_lru_count = (shared)->cur_lru_count; \
		if (new_lru_count != (shared)->page_lru_count[slotno]) { \
			(shared)->cur_lru_count = ++new_lru_count; \
			(shared)->page_lru_count[slotno] = new_lru_count; \
		} \
	} while (0)

由于对最新页面的访问可能连续进行,所以用if进行判断来比卖你老页面的count_delta增长的过快。

只有当这个页面不是最新访问的页面时,才让cur_lru_count递增,并设置slotno的lru_cout为递增后的count。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	MemSet(shared->page_buffer[slotno], 0, BLCKSZ);
	//清空缓冲区

	SimpleLruZeroLSNs(ctl, slotno);
	//设置新页面的LSNs为0

	shared->latest_page_number = pageno;
	//设置最新访问页面为pageno

	pgstat_count_slru_page_zeroed(shared->slru_stats_idx);
	//更新zeropage的状态统计
SimpleLruReadPage

从缓存中找到对应的页面,当有需要时读取。如果write_ok,则可以返回正在写入的页面,否则要等待IO完成。

输入的xid用来进行错误报告。进入该函数前ctl内部的控制锁需要被获取。

返回值是一个缓存槽号。

1
2
3
int
SimpleLruReadPage(SlruCtl ctl, int pageno, bool write_ok,
				  TransactionId xid)

在外层无条件for中,先调用SlruSelectLRUPage来获取到可以存这个页面的槽号

1
slotno = SlruSelectLRUPage(ctl, pageno);

随后判断对应的是已经在内存中的缓存还是一个空的缓存,如果是已有的且可用则直接返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (shared->page_number[slotno] == pageno &&
	shared->page_status[slotno] != SLRU_PAGE_EMPTY)
{
	//如果这个页面不是空的,要判断是否正在reading,如果是则要等待reading结束。
	if (shared->page_status[slotno] == SLRU_PAGE_READ_IN_PROGRESS ||
		(shared->page_status[slotno] == SLRU_PAGE_WRITE_IN_PROGRESS &&
		 !write_ok))
	{
		SimpleLruWaitIO(ctl, slotno);
		continue;//这个continue是从select开始完整地重新查找。
	}
	//LRU计数更新
	SlruRecentlyUsed(shared, slotno);

	//更新统计数据
	pgstat_count_slru_page_hit(shared->slru_stats_idx);

	return slotno;
}

完成后,得到了内部是空的槽

标记为正在读取:

1
2
3
shared->page_number[slotno] = pageno;
shared->page_status[slotno] = SLRU_PAGE_READ_IN_PROGRESS;
shared->page_dirty[slotno] = false;

管理锁:

1
2
3
LWLockAcquire(&shared->buffer_locks[slotno].lock, LW_EXCLUSIVE);//先获取缓冲区锁

LWLockRelease(shared->ControlLock);//再释放进入函数前获取到的控制锁

进行实际的物理读

1
ok = SlruPhysicalReadPage(ctl, pageno, slotno);

具体的物理读操作再后文说明。

设置这个页面的LSN为空并重新获取控制锁:

1
2
SimpleLruZeroLSNs(ctl, slotno);
LWLockAcquire(shared->ControlLock, LW_EXCLUSIVE);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
		shared->page_status[slotno] = ok ? SLRU_PAGE_VALID : SLRU_PAGE_EMPTY;

		LWLockRelease(&shared->buffer_locks[slotno].lock);

		/* Now it's okay to ereport if we failed */
		if (!ok)
			SlruReportIOError(ctl, pageno, xid);

		SlruRecentlyUsed(shared, slotno);

		/* update the stats counter of pages not found in SLRU */
		pgstat_count_slru_page_read(shared->slru_stats_idx);

根据是否成功读取设置SLRU_PAGE的状态,释放buffer锁。如果没能成功读取,则报告IO错误。最后更新LRU计数和统计数据。

slru物理读SlruPhysicalReadPage
1
2
3
4
5
6
SlruShared	shared = ctl->shared;
int			segno = pageno / SLRU_PAGES_PER_SEGMENT;
int			rpageno = pageno % SLRU_PAGES_PER_SEGMENT;
off_t		offset = rpageno * BLCKSZ;
char		path[MAXPGPATH];
int			fd;

获取段号、偏移、目录、文件号

1
2
3
4
	SlruFileName(ctl, path, segno);
-----------------------------------
    #define SlruFileName(ctl, path, seg) \
	snprintf(path, MAXPGPATH, "%s/%04X", (ctl)->Dir, seg)

调用宏,将DIR和seg合并为最终文件路径放到path内,其中用至少4位16进制大写作为文件名。

随后调用OpenTransientFile打开文件

1
	fd = OpenTransientFile(path, O_RDONLY | PG_BINARY);

值得注意的是,在崩溃重启情况下,可能会出现事务对应的数据处于截断的提交日志中,故允许文件不存在并当作读入全0数据。

随后判断fd的数值,如果小于0说明出错。此时如果不在恢复状态,说明确实有问题返回false,处于恢复状态则记录log,随后项buffer中写入全0。

调用pgstat_report_wait_start来报告一个需要等待的事件信息,其中摄制my_wait_event_info全局变量为传入的info

随后真正开始读:

1
2
3
4
5
6
7
8
if (pg_pread(fd, shared->page_buffer[slotno], BLCKSZ, offset) != BLCKSZ)
{
	pgstat_report_wait_end();
	slru_errcause = SLRU_READ_FAILED;
	slru_errno = errno;
	CloseTransientFile(fd);
	return false;
}

如果读取不成功,则先汇报等待事件结束,设置全局变量说明为什么不成功,最后关闭打开的文件。

如果读取成功,进行最后的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pgstat_report_wait_end();

if (CloseTransientFile(fd) != 0)
{
	slru_errcause = SLRU_CLOSE_FAILED;
	slru_errno = errno;
	return false;
}

return true;

其中同样先汇报等待事件结束,随后关闭文件描述符并捕获记录的错误,返回结果true/false

SimpleLruWritePage->SlruInternalWritePage

将一个指定的slot号中的页面由缓冲区写入磁盘。与Read类似,调用前需要获取控制锁。

值得注意的是如果对应的页面不是dirty的,则不会写入。如果在写入的时候其它人再次污染了这个页面,不会有并发问题但是会导致离开函数的时候依然是脏页。

先计算出pageno,如果正在写入,则等待IO直到没写入。

如果不是dirty的,直接返回。

否则标记页面为正在写入,并且不是脏页,获取buffer锁,释放控制锁。

调用SlruPhysicalWritePage写入页面,如果失败了且fdata不为空,处于flush状态,则关闭fdata内的所有文件指针。

获取控制锁,如果没能成功写入则继续标记为dirty;设置页面状态为valid,释放buffer锁,如果fdata不为空则递增buff_written计数。

SimpleLruWriteAll

写入所有脏页,在checkpoint和数据库关闭时被调用。

首先记录flush统计数据,定义一个fdata局部变量。

设置写入文件数量为0,获取控制锁:

1
2
fdata.num_files = 0;
LWLockAcquire(shared->ControlLock, LW_EXCLUSIVE);

对所有slot,调用slruInternalWritePage刷写脏页,释放控制锁

对fdata中的所有文件,关闭文件。

SimpleLruTruncate

把所有比传入页面老的脏页面写入并删除文件,因为它们已经随着检查点建立而变得没用了。

获取控制锁,确保最新的latest_page_number不应当满足移除条件。

遍历所有比传入页面老的非空待写入页面,空闲写入,不空闲等待。

最后删除掉对应文件。

日志目录遍历 SlruScanDirectory
1
2
bool
SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data)

遍历ctl对应日志目录下的所有文件,向回调函数传入stl、文件名、文件内第一个页面的页面号、data指针。

日志的管理

通过日志管理器来管理日志缓冲区,数据库不操作磁盘上的日志文件,而是通过日志缓冲区。日志缓冲区和外存之间的交互由日志管理器来完成。

postgreSQL有四种日志管理器:

  • XLOG
  • CLOG
  • SUBTRANS子事务日志
  • MULTIXACT组合事务日志

获取事务的状态通过事务日志接口程序而非日志管理器进行操作。该程序定义了可使用的ID范围、可使用对象ID范文、设置和获取事务状态信息的接口,同时也建立了缓冲区来缓存最近获取的事务ID及其状态。建立了共享变量缓冲区数据结构,存储下一可分配事务ID、对象ID及已分配对象数。

资源管理器再日志系统中把各种需要记录日志的数据分类,在日志中标识资源管理器号来标记源数据属于哪一类,进而选择资源管理器的对应方法。

CLOG

数据提交日志

记录是无状态和事务嵌套关系

CLOG数据结构

1
2
3
4
#define TRANSACTION_STATUS_IN_PROGRESS		0x00
#define TRANSACTION_STATUS_COMMITTED		0x01
#define TRANSACTION_STATUS_ABORTED			0x02
#define TRANSACTION_STATUS_SUB_COMMITTED	0x03

表示执行中、已提交、被终止、子事务已提交,四种状态两位比特表示

如果子事务已提交,但父事务没有提交,则设置子事务状态为SUB_COMMITED

通过理论计算CLOG的占用内存可以发现,共有$2^{32}$个事务,共需要$2^{33}$比特,$2^{17}$个页面,$2^{12}$个段,共需要与段数对应的文件来存储。

此处,PG定义了四个计算宏来从id计算对应的文件保存位置:

1
2
3
4
#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)

相关操作

内存初始化——CLOGShmemInit

CLOG全局有一个指向共享Slru控制器的非共享变量

在此函数中,设置其的比较器为COGPagePrecedes

调用SimpleLruInit初始化该LRU缓冲

最后,调用了一个UnitTest来确认正确性。

1
2
3
4
5
	XactCtl->PagePrecedes = CLOGPagePrecedes;
	SimpleLruInit(XactCtl, "Xact", CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE,
				  XactSLRULock, "pg_xact", LWTRANCHE_XACT_BUFFER,
				  SYNC_HANDLER_CLOG);
	SlruPagePrecedesUnitTests(XactCtl, CLOG_XACTS_PER_PAGE);

值得注意的是其共享内存索引名是Xact,子目录名是pg_xact,锁的组是LWTRANCHE_XACT_BUFFER,slot数量为CLOGShmemBuffers()

CLOG缓冲数量
1
2
3
4
5
Size
CLOGShmemBuffers(void)
{
	return Min(128, Max(4, NBuffers / 512));
}

含义是按照NBuffers的全局量定义来计算,大于4小于128。

写入
TransactionIdSetPageStatusInternal

设置的status要求不能是inProgress,进入前要获取XactSLRULock

1
2
3
4
	Assert(status == TRANSACTION_STATUS_COMMITTED ||
		   status == TRANSACTION_STATUS_ABORTED ||
		   (status == TRANSACTION_STATUS_SUB_COMMITTED && !TransactionIdIsValid(xid)));
	Assert(LWLockHeldByMeInMode(XactSLRULock, LW_EXCLUSIVE));

根据pageno用XactCtl读取对应的page并读取到缓存

1
slotno = SimpleLruReadPage(XactCtl, pageno, XLogRecPtrIsInvalid(lsn), xid);

如果xid有效则设置xid,如果要设置的是COMMITED,则先设置所有子事务为SUB_COMMIT提交状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
	if (TransactionIdIsValid(xid))
	{
		/* Subtransactions first, if needed ... */
		if (status == TRANSACTION_STATUS_COMMITTED)
		{
			for (i = 0; i < nsubxids; i++)
			{
				Assert(XactCtl->shared->page_number[slotno] == TransactionIdToPage(subxids[i]));
				TransactionIdSetStatusBit(subxids[i],
										  TRANSACTION_STATUS_SUB_COMMITTED,
										  lsn, slotno);
			}
		}

		/* ... then the main transaction */
		TransactionIdSetStatusBit(xid, status, lsn, slotno);
	}

最后,设置所有子事务状态为对应值,标记脏页面

1
2
3
4
5
6
7
	for (i = 0; i < nsubxids; i++)
	{
		Assert(XactCtl->shared->page_number[slotno] == TransactionIdToPage(subxids[i]));
		TransactionIdSetStatusBit(subxids[i], status, lsn, slotno);
	}

	XactCtl->shared->page_dirty[slotno] = true;
TransactionIdSetStatusBit

用来被内部调用,写入对应位

1
TransactionIdSetStatusBit(TransactionId xid, XidStatus status, XLogRecPtr lsn, int slotno)

需要先获取XactSLRULock

获取到Byteno和bshuft,用临时char指针byteptr指向对应的字节,随后获取到当前的事务状态。

1
2
	byteptr = XactCtl->shared->page_buffer[slotno] + byteno;
	curval = (*byteptr >> bshift) & CLOG_XACT_BITMASK;

设置对应状态。

读取
TransactionIdGetStatus
1
2
XidStatus
TransactionIdGetStatus(TransactionId xid, XLogRecPtr *lsn)

返回xid状态,向*lsn中存储晚于xid对应CLOG的LSN

同样先计算页号及偏移:

1
2
3
	int			pageno = TransactionIdToPage(xid);
	int			byteno = TransactionIdToByte(xid);
	int			bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;

如果整个clog页面已经存到了磁盘里,则会返回非法XLogRecPtr;如果log所在组有其它更晚的clog,同样会返回同组更晚的CLOG的LSN

1
2
3
4
5
6
7
8
9
	slotno = SimpleLruReadPage_ReadOnly(XactCtl, pageno, xid);
	byteptr = XactCtl->shared->page_buffer[slotno] + byteno;

	status = (*byteptr >> bshift) & CLOG_XACT_BITMASK;

	lsnindex = GetLSNIndex(slotno, xid);
	*lsn = XactCtl->shared->group_lsn[lsnindex];

	LWLockRelease(XactSLRULock);

先用只读模式读页面并获取slotno,用类似set的方法获取状态,写入group_lsn到*lsn,最后释放锁。

日志页面初始化
ZeroCLOGPage
1
2
static int
ZeroCLOGPage(int pageno, bool writeXlog)

如果writeXlog为True,则会同时写一个Xlog record

用pageno和XactCtl调用SimpleLruZeroPage,根据writeXlog调用WriteZeroPageXlogRec。

日志段创建
BootStrapCLOG

在整个系统的启动过程中只被调用一次,用来创建最初的CLOG段。

其中CLOG文件目录被initdb创建

先获取XactSLRULock锁,再用ZeroCLOGPage获取一个可用的空页面,最后调用SimpleWritePage确保页面被写入文件,最后释放锁。

CLOG日志启动
StartupCLOG

当Postmaster或者一个服务进程启动时,启动CLOG日志管理器,仅被调用一次。

先从ShmemVaribleCache共享变量中获取下一个xid保存到xid中,再用xid得到正确的pageno。

获取XactSLRULock锁,设置XactCtl内保存的处于共享内存的缓冲区的最新page_number为下一个xid对应的pageno,最后释放锁。

对比新老版本,此处不再对对应页面进行清空处理。

CLOG日志保存点

在CLOG日志关闭时或SAVEPOINT,调用SimpleLruWriteAll刷新页面。

CLOG日志清理
TruncateCLOG
1
2
void
TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid)

删除oldestXact之前的CLOG日志段,先找到传入的xactid对应的页面作为最晚页面,调用SlruScanDIrectory,用SlruScanDirCbReportPresence和最晚页面号遍历所有Clog文件。如果找不到能删除的,直接退出。

1
2
3
	cutoffPage = TransactionIdToPage(oldestXact);
	if (!SlruScanDirectory(XactCtl, SlruScanDirCbReportPresence, &cutoffPage))
		return;		

随后先将共享变量的oldestClogXid提前到传入的xid,写入XLOG并刷写XLOG到磁盘。(?)

最后传入最老页面号调用SimpleLruTruncate。

构建CLOG日志时写入XLOG

info为CLOG_ZEROPAGE写入XLOG

1
2
3
4
5
6
7
static void
WriteZeroPageXlogRec(int pageno)
{
	XLogBeginInsert();
	XLogRegisterData((char *) (&pageno), sizeof(int));
	(void) XLogInsert(RM_CLOG_ID, CLOG_ZEROPAGE);
}

在XLOG部分详述

CLOG的REDO

在rmgrlist中注册为clog的redo方法

1
2
3
4
5
void
clog_redo(XLogReaderState *record)
--------------------------------xlogreader.h
typedef struct XLogReaderState XLogReaderState;
......

计算出info后根据info进行redo,只会有两种操作,ZEROPAGE和TRUNCATE,进行对应redo操作。

XLOG

数据库日志主体,记录所有数据操作的变化过程

Licensed under CC BY-NC-SA 4.0
京ICP备2021032224号-1
Built with Hugo
主题 StackJimmy 设计
vi ./themes/hugo-theme-learn/layouts/partials/footer.html