暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

openGauss刷脏机制

原创 lbsswhu 2024-12-16
715

  在数据库中,脏页通常指的是在缓存中修改过但是尚未落盘的页面,刷脏指的是将缓存中的页面写入磁盘数据文件的过程。在openGauss中,触发刷脏的行为主要有两个:

  • 后台线程主动刷脏:由后台线程pagewriter线程组周期性发起,将脏页批量写入磁盘;主动刷脏一方面可以将腾空Buffer Pool中的部分脏页,另一方面配合openGauss增量checkpoint机制减少崩溃恢复场景下的RTO;
  • 用户线程被动刷脏:由用户线程自己发起,主要原因是Buffer Pool使用紧张,要进行Buffer页面淘汰时,待淘汰的Buffer页面是一个脏页,用户线程需要将该页面落盘才能复用该Buffer;

  刷脏设计的好坏直接影响了数据库性能。本文将从源码的角度,详细介绍在openGauss的刷脏机制。

脏页的管理

  在openGauss中页面内容如果发生了修改,就会将该Buffer置脏,并将该脏页放入到脏页队列中,这个设计主要是为了checkpoint机制,关键的数据结构如下:

/* incremental checkpoint */ typedef struct knl_g_ckpt_context { /* 由增量checkpoint线程周期性生成,可以认为是下一次checkpoint的redo位点。*/ uint64 dirty_page_queue_reclsn; /* 脏页队列尾部的位置 */ uint64 dirty_page_queue_tail; ...... /* 脏页队列以及大小 */ DirtyPageQueueSlot* dirty_page_queue; uint64 dirty_page_queue_size; /* 脏页队列的头部位置 */ pg_atomic_uint64 dirty_page_queue_head; /* 当前有多少个脏页 */ pg_atomic_uint32 actual_dirty_page_num; ...... } knl_g_ckpt_context;

  在函数push_pending_flush_queue中可以看到脏页的置脏过程:

bool push_pending_flush_queue(Buffer buffer){ ...... /* 同时获取dirty_page_queue_reclsn和dirty_page_queue_tail的值,并更新dirty_page_queue_tail */ push_finish = atomic_push_pending_flush_queue(&queue_head_lsn, &new_tail_loc); ...... /* 在Buffer的描述符中写上当前的dirty_page_queue_reclsn. */ pg_atomic_write_u64(&buf_desc->extra->rec_lsn, queue_head_lsn); /* 向脏页队列中插入该脏页 */ actual_loc = new_tail_loc % g_instance.ckpt_cxt_ctl->dirty_page_queue_size; buf_desc->extra->dirty_queue_loc = actual_loc; g_instance.ckpt_cxt_ctl->dirty_page_queue[actual_loc].buffer = buffer; pg_memory_barrier(); pg_atomic_write_u32(&g_instance.ckpt_cxt_ctl->dirty_page_queue[actual_loc].slot_state, (SLOT_VALID)); (void)pg_atomic_fetch_add_u32(&g_instance.ckpt_cxt_ctl->actual_dirty_page_num, 1); }

结构体knl_g_ckpt_contextdirty_page_queue_reclsndirty_page_queue_tail放在一起的目的是为了实现这两个变量的无锁读写。置脏的过程包括两个关键操作:

  1. 获取dirty_page_queue_reclsn和以及插入脏页队列的位置dirty_page_queue_tail;
  2. 设置本Buffer的extra->rec_lsn设置为dirty_page_queue_reclsn,同时将该Buffer插入到脏页队列g_instance.ckpt_cxt_ctl->dirty_page_queue中;

因为dirty_page_queue_reclsn只会递增,因此脏页队列中的所有页面一定按照buf_desc->extra->rec_lsn递增排列。

  增量checkpoint线程会获取队列首部页面的rec_lsn作为此次增量checkpoint的LSN,小于该LSN的脏页不在脏页链表中,那么一定落盘了,那么恢复的时候只需要使用大于该LSN的redo进行恢复即可。

  脏页队列由数组组成,其总大小为Buffer Pool总页面个数的5倍。一个页面只会放入脏页队列一次,即使它被反复修改,当该Buffer落盘后需要将该页面从Buffer队列中移除,移除的操作在函数remove_dirty_page_from_queue中。

void remove_dirty_page_from_queue(BufferDesc* buf) { Assert(buf->extra->dirty_queue_loc != PG_UINT64_MAX); /* 注意,此时此时脏页队列的slot仍未被清理 */ g_instance.ckpt_cxt_ctl->dirty_page_queue[buf->extra->dirty_queue_loc].buffer = 0; /* rec_lsn复位后可以再次放入脏页队列中 */ pg_atomic_write_u64(&buf->extra->rec_lsn, InvalidXLogRecPtr); buf->extra->dirty_queue_loc = PG_UINT64_MAX; (void)pg_atomic_fetch_sub_u32(&g_instance.ckpt_cxt_ctl->actual_dirty_page_num, 1); }

后台线程刷脏

  现在我们知道脏页都被集中存放在脏页队列g_instance.ckpt_cxt_ctl->dirty_page_queue中,后台pagewriter线程组会定期地(pagewriter_sleep)将该队列中的Buffer进行落盘。pagewriter线程组包括一个main线程和多个sub线程(由参数pagewriter_thread_num控制子线程的个数),分别扮演着协调者和工作线程的角色,main线程负责计算需要刷多少个脏页,唤醒sub线程进行实际的刷脏,以及环形脏页队列的回收,这个刷脏过程本文简称为脏页队列刷脏。

  此外,pagewriter线程组还会定期使用ClockSweep算法扫描Buffer Pool,并将Buffer Pool中的空闲页面放入到candidate_list队列中,该过程中发现的空闲脏页也会批量刷盘,这个过程的刷脏本文称为LRU刷脏。

刷脏IO能力限制

  后台刷脏能够使用的最大IO能力是由main线程决定的,计算过程在函数calculate_max_flush_num中:

static void calculate_max_flush_num() { ...... /* 刷脏线程能够使用的最高的IOPS的上限,除以2应该是考虑到double write的IO。*/ uint32 max_io = u_sess->attr.attr_storage.max_io_capacity / blk_size / 2; min_lsn = ckpt_get_min_rec_lsn(); ...... /* primary get the xlog insert loc, standby get the replay loc */ if (RecoveryInProgress()) { cur_lsn = GetXLogReplayRecPtr(NULL); } else { cur_lsn = GetXLogInsertRecPtr(); } /* 估算脏页队列中的页面使用了多少比例的redo log. */ lsn_percent = (double)(cur_lsn - min_lsn) / ((double)u_sess->attr.attr_storage.max_redo_log_size * BYTE_PER_KB); /* 预估这么多比例redo log对应的脏页队列中的页面需要刷盘,会占用多大比例的IOPS,不超过75% */ rate_lsn = 1 - HIGH_WATER / (lsn_percent + HIGH_WATER); rate_lsn = MIN(rate_lsn, HIGH_WATER); rate_buf = 1 - rate_lsn; queue_flush_max = max_io * rate_lsn; /* 剩余的IOPS预留给LRU的刷脏 */ list_flush_max = max_io * rate_buf; g_instance.ckpt_cxt_ctl->pgwr_procs.queue_flush_max = queue_flush_max; g_instance.ckpt_cxt_ctl->pgwr_procs.list_flush_max = list_flush_max; ....... }

刷脏的最大IO能力受两个参数影响:

  1. max_io_capacity: 后台线程刷脏能够使用的最大IO带宽,包括脏页队列刷脏和LRU刷脏。其中最高75%分配给脏页队列刷脏,剩余的资源预留给LRU刷脏;
  2. max_redo_log_size: redo log的最大大小,该值越大,每次脏页队列刷脏的脏页IOPS占用的越少,意味着redo log越大,脏页队列刷脏能够使用的IO资源越少,更倾向将更多的IO资源留给LRU刷脏;

脏页队列刷脏

刷脏数量

  main线程首先根据当前脏页的比例、redo log新增比例、以及最近刷脏的平均速度三个因素决定本次刷脏的数量,计算出本次期望的刷脏脏页数(expected_flush_num),但是最大允许的刷脏数不超过g_instance.ckpt_cxt_ctl->pgwr_procs.queue_flush_max,不小于DW_DIRTY_PAGE_MAX_FOR_NOHBK(818,即double write文件每批的刷脏数)。

  • 考虑脏页比例允许刷脏的脏页数:脏页的比例考虑两个因素,脏页数量占整个Buffer Pool大小的比例以及脏页队列的使用率(因为用户线程刷脏以及LRU刷脏可能会导致脏页队列产生空洞以及脏页队列的回收机制,因此可能会存在脏页不多,但是脏页队列的使用率高)。此外,还需要参考参数:dirty_page_percent_max(默认为90%),该参数表示多大脏页比例为最大的脏页比例(即刷脏的IO能力为允许的最大IO能力),该值越高,则每批刷脏的页面数就越多。
    ...... /* 脏页数量占整个Buffer Pool大小的比例 */ dirty_page_pct = g_instance.ckpt_cxt_ctl->actual_dirty_page_num / (float)(SegmentBufferStartID); /* 脏页队列的使用率 */ dirty_slot_pct = get_dirty_page_num() / (float)(g_instance.ckpt_cxt_ctl->dirty_page_queue_size); /* 实际上考虑脏页数量因素,本次应该刷多少个脏页 */ dirty_percent = MAX(dirty_page_pct, dirty_slot_pct) / u_sess->attr.attr_storage.dirty_page_percent_max; if (dirty_percent < HIGH_WATER) { num_for_dirty = min_io; num_for_lsn_max = max_io; } else if (dirty_percent <= 1) { num_for_dirty = min_io + (float)(dirty_percent - HIGH_WATER) / (float)(1 - HIGH_WATER) * (max_io - min_io); num_for_lsn_max = max_io + (float)(dirty_percent - HIGH_WATER) / (float)(1 - HIGH_WATER) * (max_io); } else { num_for_dirty = max_io; num_for_lsn_max = max_io * 2; }
    num_for_dirty即为考虑到脏页数量应该刷脏的个数,同时,脏页的比例也影响着LSN产生因素应该刷脏的脏页最大数;
  • 考虑LSN新增比例允许刷脏的脏页数:从redo产生的角度考虑,期望刷脏的数量,主要有两个方面:
    • lsn_scan_factor:默认值为3,如果脏页相对比例和本次LSN新增比例小于75%且redo的产生速度较小(小于4(通常update产生的log的数量)倍的redo segment的大小),该值为1
    • redo的平均产生速度:每隔30次脏页队列刷脏或者刷脏时间超过了pagewriter线程间隔的30倍,即参数pagewriter_sleep * 30,openGauss会计算一次redo的产生速度。
    /* 根据当前脏页队列中最小的`rec_lsn`,计算出应该要刷脏到哪个target_rec_lsn */ target_lsn = min_lsn + avg_lsn_rate * lsn_scan_factor; /* 再根据target_rec_lsn在脏页队列中数本次应该刷多少个页面 */ num_for_lsn = get_page_num_for_lsn(target_lsn, num_for_lsn_max); if (lsn_target_percent < HIGH_WATER) { num_for_lsn = MIN(num_for_lsn / lsn_scan_factor, max_io); } else if (lsn_target_percent < 1) { num_for_lsn = MIN(num_for_lsn / lsn_scan_factor, max_io) + (float)(lsn_target_percent - HIGH_WATER) / (float)(1 - HIGH_WATER) * max_io; } else { num_for_lsn = max_io * 2; } /*最终实际上考虑redo产生因素,需要刷脏的页面数量为本次和上一次的平均值 */ num_for_lsn = (num_for_lsn + prev_lsn_num) / 2; prev_lsn_num = num_for_lsn;
  • 考虑刷脏的平均速度允许刷脏的脏页数:每隔30次脏页队列刷脏或者刷脏时间超过了pagewriter线程间隔的30倍,即参数pagewriter_sleep * 30,统计的实际脏页的刷脏速度。

  本次刷脏的允许的脏页数expected_flush_num是以上三个方面允许刷脏的脏页数的期望值,但最终该刷脏数仍然还在[DW_DIRTY_PAGE_MAX_FOR_NOHBK, g_instance.ckpt_cxt_ctl->pgwr_procs.queue_flush_max]范围内,需要注意的是,该值只是限制了最大允许的刷脏数。openGauss真实刷脏页面数还受扫描脏页队列的最大深度为131072(1G的Buffer大小)限制,main线程会顺序从队列头部开始扫描最多131072个脏页slot,从中选取不超过expected_flush_num个脏页进行刷脏(ckpt_qsort_dirty_page_for_flush)。

  main线程找到所有需要刷脏的脏页后,会对脏页按照tablespaceRelFileNodeBlockNumber等进行排序,尽量保证每次按照相同文件的相邻数据块的顺序进行刷脏,并将这些已经排好序的Buffer缓存在g_instance.ckpt_cxt_ctl->CkptBufferIds中,同时唤醒sub线程完成刷脏工作。

sub线程脏页队列刷脏

  page writer sub线程组接收到脏页队列的刷脏任务后,每个线程会从g_instance.ckpt_cxt_ctl->CkptBufferIds中获取GET_DW_DIRTY_PAGE_MAX个脏页进行刷脏。

static void ckpt_pagewriter_sub_thread_loop() { ...... PageWriterProc* pgwr = &g_instance.ckpt_cxt_ctl->pgwr_procs.writer_proc[thread_id]; if (pgwr->need_flush) { pg_read_barrier(); total_flush_pages = g_instance.ckpt_cxt_ctl->CkptBufferIdsFlushPages; /* 脏页队列刷脏 */ while (pg_atomic_read_u32(&g_instance.ckpt_cxt_ctl->prepared) == 1 && pg_atomic_read_u32(&g_instance.ckpt_cxt_ctl->CkptBufferIdsCompletedPages) < total_flush_pages) { /* 从刷脏队列中获取一批脏页 */ if(!apply_batch_flush_pages(pgwr)) { break; } /* flush one batch dirty pages */ ResourceOwnerEnlargeBuffers(t_thrd.utils_cxt.CurrentResourceOwner); /* 真实的刷脏动作,包括doublewrite的写入和数据的写入 */ incre_ckpt_pgwr_flush_dirty_queue(&wb_context); ....... } pgwr->need_flush = false; old_running_num = pg_atomic_fetch_sub_u32(&g_instance.ckpt_cxt_ctl->pgwr_procs.running_num, 1); /* 本线程如果是最后一个退出的子线程,则唤醒主线程 */ if (old_running_num == 1) { wakeup_pagewriter_main_thread(); } smgrcloseall(); } return; }

  sub线程组真实的刷脏实现在函数incre_ckpt_pgwr_flush_dirty_queue,包括doublewrite的写入和文件的写入以及文件的fsync,后续章节数据块落盘中将详细介绍。

LRU刷脏

candidate_list

  在openGauss的Buffer管理中,用户申请Buffer会先从candidate_list中申请,如果candidate_list中没有页面,则会扫描整个Buffer Pool,从中获取一个尚未被使用的Buffer。为了让用户申请Buffer尽可能地从candidate_list中申请,pagewriter线程组还需要定期地使用Clock Sweep算法扫描部分Buffer Pool将最近最少使用的且未被正常使用的Buffer放入到candidate_list,如果这个过程中发现了脏页,也需要将脏页批量刷盘。

  candidate_list由pagewriter线程各自维护,因此一共有pagewriter_thread_num个,每个list负责Buffer Pool的一个分区,用户申请空闲Buffer会随机从任何一个candidate_list开始寻找。

static void init_candidate_list() { int thread_num = g_instance.ckpt_cxt_ctl->pgwr_procs.sub_num; /* 将Buffer Pool按照pagewriter线程数等分*/ int normal_avg_num = NORMAL_SHARED_BUFFER_NUM / thread_num; PageWriterProc *pgwr = NULL; /* candidate_buffers全局数组,每个pagewriter线程按照list来使用其中的一部分 */ Buffer *cand_buffers = g_instance.ckpt_cxt_ctl->candidate_buffers; /* Init main thread, the candidate list only store segment buffer */ pgwr = &g_instance.ckpt_cxt_ctl->pgwr_procs.writer_proc[0]; INIT_CANDIDATE_LIST(pgwr->normal_list, NULL, 0, 0, 0); ...... for (int i = 1; i <= thread_num; i++) { pgwr = &g_instance.ckpt_cxt_ctl->pgwr_procs.writer_proc[i]; int start = normal_avg_num * (i - 1); int end = start + normal_avg_num; ....... /* 初始化candidate_list的list,大小和管理的第一个Buffer在Bufer Pool中的位置 */ INIT_CANDIDATE_LIST(pgwr->normal_list, &cand_buffers[start], end - start, 0, 0); pgwr->normal_list.buf_id_start = start; ...... } }

candidate_list刷脏

  每个pagewriter子线程间隔max(bgwriter_delay, pagewriter_sleep)时间会检查当前的candidate_list是否满(默认为空),若没有满,则会扫描相应的分区,并向candidate_list插入对应的空闲Buffer。

static void incre_ckpt_pgwr_scan_candidate_list(WritebackContext *wb_context, CandidateList *list, CandListType type) { ...... /* 如果candidate_list未满 */ if (get_thread_candidate_nums(list) < list->cand_list_size) { ...... start = MAX(list->buf_id_start, list->next_scan_loc); end = list->buf_id_start + list->cand_list_size; /* MAX_SCAN_BATCH_NUM为1310720,即10GB大小的Buffer,即存在硬编码的上限 */ batch_scan_num = MIN(list->cand_list_size, MAX_SCAN_BATCH_NUM); ...... end = MIN(start + batch_scan_num, end); max_flush_num = get_list_flush_num(type); need_flush_num = get_candidate_buf_and_flush_list(start, end, max_flush_num, &is_new_relfilenode); ...... if (end >= list->buf_id_start + list->cand_list_size) { list->next_scan_loc = list->buf_id_start; } else { list->next_scan_loc = end; } ...... if (need_flush_num > 0) { incre_ckpt_pgwr_flush_dirty_list(wb_context, need_flush_num, is_new_relfilenode); } } }

从函数incre_ckpt_pgwr_scan_candidate_list中可以看到,pagewriter在向candidate_list插入空闲Buffer时,需要扫描Buffer Pool的深度最大为1310720,最小为每个pagewriter线程分区中的所有Buffer Pool中的元素。

  函数get_list_flush_num限制了每个pagewriter的LRU刷脏的最大IO能力(g_instance.ckpt_cxt_ctl->pgwr_procs.list_flush_max/pagewriter_thread_num),在前面的章节刷脏IO能力限制中,我们已经知道openGauss已经预留了一部分的IO资源给LRU刷脏,实际上openGauss中还有另外一个硬限制MAX_DIRTY_LIST_FLUSH_NUM = 1000 * DW_DIRTY_PAGE_MAX_FOR_NOHBK/pagewriter_thread_num(即为818000/pagewriter_thread_num),因此,LRU刷脏的最大IO能力为这两个之间的最小值。此外该函数还提供了LRU刷脏的期望脏页数(max_flush_num),该值由参数candidate_buf_percent_targetcandidate_list期望的页面占总Buffer Pool页面数的比例)决定。如果当前candidate_list中的页面数和总Buffer Pool页面数比例小于该值则使用最大IO能力进行刷脏,否则按一定比例的最大IO能力刷脏。

  pagewriter扫描Buffer Pool向candidate_list插入空闲Buffer以及收集脏页在PageWriterProc->dirty_buf_list中过程在函数get_candidate_buf_and_flush_list中,主要包括以下步骤:

  • 扫描给定范围的Buffer Pool
    1. 找到一个尚未被使用的Buffer;
    2. 若启用了ClockSweep算法(参数enable_consider_usecount),则继续判断其Buffer的USAGECOUNT是否为0,不为0则减一继续找下一个页面,否则进入下一步;
    3. 判断该页面是否为脏页,若不为脏页则将该页面直接push到candidate_list中,否则进入下一步;
    4. 判断当前收集的脏页数是否超过了max_flush_num,若未超过,则将该页面放入PageWriterProc->dirty_buf_list中;
    5. 继续扫描Buffer Pool中的下一个页面;

  需要说明的是,candidate_list不是一个列表,而是一个队列。向candidate_list插入空闲Buffer时,不会将该Buffer淘汰掉,这意味着仍然通过BufferTag直接定位的Buffer也在candidate_list,因此candidate_list不等价于常规的FreeList,candidate_list中的Buffer可能也是脏页、或者正在使用的页面。

  PageWriterProc->dirty_buf_list收集到的脏页会在函数incre_ckpt_pgwr_flush_dirty_list中进行刷脏:

static void incre_ckpt_pgwr_flush_dirty_list(WritebackContext *wb_context, uint32 need_flush_num, bool is_new_relfilenode) { ...... /* 将dirty_buf_list页面进行排序,尽量将相同文件的相同数据块一次落盘 */ qsort(dirty_buf_list, need_flush_num, sizeof(CkptSortItem), ckpt_buforder_comparator); ...... /* Double write can only handle at most DW_DIRTY_PAGE_MAX at one time. */ for (int i = 0; i < runs; i++) { ...... /* 先将dirty_buf_list的页面批量写入到doublewrite文件中 */ dw_perform_batch_flush(batch_num, dirty_buf_list + offset, thread_id, &pgwr->thrd_dw_cxt); /* 先将dirty_buf_list的页面依次写入相应的文件中 */ flush_num = incre_ckpt_pgwr_flush_dirty_page(wb_context, dirty_buf_list, offset, batch_num); pgwr->thrd_dw_cxt.dw_page_idx = -1; num_actual_flush += flush_num; } ...... /* 将落盘之后的Buffer插入到candidate_list中 */ for (uint32 i = 0; i < need_flush_num; i++) { buf_id = dirty_buf_list[i].buf_id; if (buf_id == DW_INVALID_BUFFER_ID) { continue; } buf_desc = GetBufferDescriptor(buf_id); push_to_candidate_list(buf_desc); } ...... }

需要注意的是LRU刷脏的脏页同时也一定在脏页队列中,但是并不是按照脏页队列的先后顺序,因此LRU的刷脏可能会导致脏页队列存在较多的空洞,进而导致脏页队列使用率低。想象一个极端场景,脏页队列的队首和队尾未被刷脏,其他脏页全部完成刷脏,此时即使脏页队列有较大的空间,但是用户线程已经无法置脏了。这就需要main线程进行脏页队列的回收。

脏页队列回收

  pagewrite sub线程完成刷脏的任务后,main线程需要对脏页队列进行回收(ckpt_move_queue_head_after_flush),即遍历全局脏页队列,将已经落盘的脏页从队列中移除。除此之外,需要注意的是,因为用户线程刷脏以及LRU刷脏并不是按照脏页队列顺序进行的,因此脏页队列中可能存在空洞,此时main线程负责移动脏页位置,将空洞的槽位释放出来(ckpt_try_prune_dirty_page_queue)。因为脏页队列也是有限资源,如果脏页队列满了,那么用户的写入就会阻塞住。

用户线程刷脏

  当用户线程需要从Buffer Pool中申请Buffer读取物理页面时,会先从candidate_list中读取页面,如果开启了ClockSweep算法(即enable_consider_usecount为ON),还会在candidate_list的扫描过程中使用ClockSweep算法。如果candidate_list未能找到未使用且usagecout为0的非脏页,则会查看candidate_list是否存在未使用的脏页(最多查看100个):

static BufferDesc* get_buf_from_candidate_list(BufferAccessStrategy strategy, uint64* buf_state) { ...... /* 如果脏页过多,超过75%,则即使在candidate_list中发现了未正在使用的脏页也可以返回 */ bool need_scan_dirty = (g_instance.ckpt_cxt_ctl->actual_dirty_page_num / (float)(g_instance.attr.attr_storage.NBuffers) > HIGH_WATER) && backend_can_flush_dirty_page(); ...... /* 相同会话将会从相同的candidate_list中开始查找空闲页面。。。 */ list_id = beentry->st_tid > 0 ? (beentry->st_tid % list_num) : (beentry->st_sessionid % list_num); for (int i = 0; i < list_num; i++) { /* the pagewriter sub thread store normal buffer pool, sub thread starts from 1 */ int thread_id = (list_id + i) % list_num + 1; Assert(thread_id > 0 && thread_id <= list_num); /* 从candidate_list的头部开始取出一个Buffer. */ while (candidate_buf_pop(&g_instance.ckpt_cxt_ctl->pgwr_procs.writer_proc[thread_id].normal_list, &buf_id)) { Assert(buf_id < SegmentBufferStartID); buf = GetBufferDescriptor(buf_id); local_buf_state = LockBufHdr(buf); /* 若Buffer已经在candidate_list中 */ if (g_instance.ckpt_cxt_ctl->candidate_free_map[buf_id]) { g_instance.ckpt_cxt_ctl->candidate_free_map[buf_id] = false; enable_available = BUF_STATE_GET_REFCOUNT(local_buf_state) == 0 && !(local_buf_state & BM_IS_META); /* 脏页过多,且发现的脏页数小于100,则需要收集该脏页 */ need_push_dirst_list = need_scan_dirty && dirty_list_num < CANDIDATE_DIRTY_LIST_LEN && free_space_enough(buf_id); /* Buffer引用计数未0,未被使用 */ if (enable_available) { if (NEED_CONSIDER_USECOUNT && BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0) { /* 使用了ClockSweep算法,使用次数减一,但是此时Buffer已经不在candidate_list中 */ local_buf_state -= BUF_USAGECOUNT_ONE; } else if (!(local_buf_state & BM_DIRTY)) { /* Buffer使用次数未0,或者未使用使用了ClockSweep算法,且不为脏页,直接返回该Buffer */ ...... return buf; } else if (need_push_dirst_list) { /* 收集该脏页 */ candidate_dirty_list[dirty_list_num++] = buf_id; } } } UnlockBufHdr(buf, local_buf_state); } } /* 唤醒pagewriter线程干活 */ wakeup_pagewriter_thread(); /* 尚未找到空闲的非脏页,接下来要在脏页链表中找到空闲Buffer */ if (need_scan_dirty) { for (int i = 0; i < dirty_list_num; i++) { ...... } } ...... }

需要注意的是,在扫描candidate_list的过程也是一个将candidate_list中的元素清理的过程,因此如果开启了ClockSweep算法,Buffer的usagecount只会减少一次。如果candidate_list中仍未找到一个空闲的Buffer,则需要遍历整个Buffer Pool找到一个空闲的Buffer即可(此时不考虑ClockSweep算法)。

  因此,无论是从candidate_list寻找页面还是遍历Buffer Pool的过程,最终都可能返回一个脏页,用户线程需要将该脏页内容落盘后才能使用该Buffer,申请Buffer的过程如下:

ReadBuffer_common BufferAlloc ...... for (;;) { /* 分别从candidate_list(Clock Sweep算法)以及整个buffer pool中获取一个未被正在使用的buf*/ buf = (BufferDesc *)StrategyGetBuffer(strategy, &buf_state); Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); ....... old_flags = buf_state & BUF_FLAG_MASK; /* Buffer Pool中返回的缓存是一个脏页 */ if (old_flags & BM_DIRTY) { ....... if (dw_enabled() && pg_atomic_read_u32(&g_instance.ckpt_cxt_ctl->current_page_writer_count) > 0) { ....... uint32 pos = 0; /* 获取单页面doublewrite区域一个可用的位置*/ pos = first_version_dw_single_flush(buf); t_thrd.proc->dw_pos = pos; /* 先将页面写入doublewrite区,然后在写入磁盘*/ FlushBuffer(buf, NULL); ...... } ........ } .......

后续章节数据块落盘中将详细介绍openGauss数据页面落盘的过程。

数据块落盘

  无论是哪种刷脏,最终的目的都是将Buffer Pool中的页面刷新到磁盘。openGauss的页面大小是8K,为了解决刷脏的过程中发生了崩溃异常导致发生了页面的部分写失败问题,openGauss在写数据文件之前,先将页面写入到doublewrite文件(global/pg_dw_xx)中,doublewrite文件写成功之后才会写数据文件,通过这种机制保证了发生崩溃恢复之后,doublewrite文件和数据文件中至少有一个完整的页面。

   在openGauss中,doublewrite文件是通过O_DIRECT方式进行读写的,但是数据文件并不是。因此保证数据页面成功落盘,还需要在数据文件写入完成后,调用系统调用fsync让os cache中的页面落盘。 因此,对于Buffer Pool中的任何一个页面,其刷脏流程主要包括以下关键三个个步骤:写入doublewrite文件,写数据文件,fsync数据文件。

doublewrite文件写入

  针对后台线程批量刷脏和用户线程单个页面两种形式的刷脏,openGauss也设计两种格式的doublewrite文件,global/pg_dw_0x是后台线程使用,完成多个页面的批量写入,doublewrite文件数量由参数dw_file_num控制,global/pg_dw_single由用户线程使用,只有一个文件。
  doublewrite的批量写入在函数dw_perform_batch_flush中,本文不详细介绍doublewrite的具体细节,仅介绍其使用流程:

void dw_perform_batch_flush(uint32 size, CkptSortItem *dirty_buf_list, int thread_id, ThrdDwCxt* thrd_dw_cxt) { /* 获取一个doublewrite文件 */ file_id = dw_fetch_file_id(thread_id); dw_batch_file_context *dw_cxt = &g_instance.dw_batch_cxt.batch_file_cxts[file_id]; for (uint16 i = 0; i < batch_size; i++) { /* 将页面内容拷贝到当前线程的连续内存中 */ page_lsn = dw_copy_page(thrd_dw_cxt, dirty_buf_list[i].buf_id, &is_skipped); /* 获取当前批页面的最新修改的LSN,写doublewrite前需要确保对应的redo log已经落盘 */ if (XLByteLT(latest_lsn, page_lsn)) { latest_lsn = page_lsn; } } if (thrd_dw_cxt->write_pos > 0) { /* 将当前线程中已经连续缓存页面写入到对应的doublewrite文件中 */ dw_batch_flush(dw_cxt, latest_lsn, thrd_dw_cxt); } }

需要说明的是,批量doublewrite写时,pagewriter线程需要获取对应doublewrite文件的flush_lock。单页面的doublewrite写入和批量写入类似,只是不需要flush_lock,本文不再赘述。

数据文件写入

  openGauss中数据的写入最终是调函数smgrwrite完成,该函数主要完成两个核心功能:

  1. 调用pwrite函数将Buffer中的内容写入到对应的文件中;
  2. 注册该文件fsync请求注册到全局的文件fsync系统中IncreCkptSyncShmemStruct

文件sync机制

  openGauss中所有的文件fsync请求都会注册到全局g_instance.ckpt_ctx_ctl->incre_ckpt_sync_shmem中,其结构如下:

typedef struct { /* 刷脏的请求主要为:SYNC_REQUEST*/ SyncRequestType type; /* request type */ /* 文件tag,主要包括RelFileNode, forknum, segno等,唯一标识一个物理文件 */ FileTag ftag; /* file identifier */ } CheckpointerRequest; typedef struct IncreCkptSyncShmemStruct { /* pagewriter主线程完成文件批量fsync */ ThreadId pagewritermain_pid; /* PID (0 if not started) */ slock_t sync_lock; /* protects all the fsync_* fields */ /* fsync开始处理前的计数 */SYNC_REQUEST int64 fsync_start; /* fsync处理完成后的计数 */ int64 fsync_done; LWLock *sync_queue_lwlock; /* */ /* 当前所有的文件fsync请求 */ int num_requests; /* current # of requests */ int max_requests; /* allocated array size */ CheckpointerRequest requests[1]; /* VARIABLE LENGTH ARRAY */ } IncreCkptSyncShmemStruct;

当文件的fsync请求注册到全局文件fsync系统之后,会在以下两种场景唤醒pagewriter main线程批量调用fsync:

  1. doublewrite文件写满了需要重用或者文件需要truncate:当doublewrite的空间需要复用时必须要保证对应的数据必须落盘,保障如果发生了故障数据文件一定可用,详细实现参考i函数dw_batch_file_recycle, dw_single_file_recyle等;
    static bool dw_batch_file_recycle(dw_batch_file_context *cxt, uint16 pages_to_write, bool trunc_file) { ...... file_full = (file_head->start + cxt->flush_page + pages_to_write >= dw_batch_page_num); Assert(!(file_full && trunc_file)); if (!file_full && !trunc_file) { return true; } ...... if (USE_CKPT_THREAD_SYNC) { ProcessSyncRequests(); } else { /* 唤醒pagewriter main线程对g_instance.ckpt_ctx_ctl->incre_ckpt_sync_shmem中所有的文件fsync请求做批量fsync */ PageWriterSync(); } ...... }
  2. 发生了checkpoint:在函数CheckPointBuffers中详细介绍了Buffer Pool中脏页的落盘机制
    void CheckPointBuffers(int flags, bool doFullCheckpoint) { ...... g_instance.ckpt_cxt_ctl->flush_all_dirty_page = false; t_thrd.xlog_cxt.CheckpointStats->ckpt_sync_t = GetCurrentTimestamp(); TRACE_POSTGRESQL_BUFFER_CHECKPOINT_SYNC_START(); if (USE_CKPT_THREAD_SYNC) { ProcessSyncRequests(); } else { /* 唤醒pagewriter main线程对g_instance.ckpt_ctx_ctl->incre_ckpt_sync_shmem中所有的文件fsync请求做批量fsync */ /* incremental checkpoint, requeset the pagewriter handle the file sync */ PageWriterSync(); dw_truncate(); } }

  openGauss Buffer的增量checkpoint的操作非常简单,只需要记录当前脏页队列队首脏页的rec_lsn,该LSN即为checkpoint记录安全恢复的redo位点,然后唤醒pagewriter main线程对g_instance.ckpt_ctx_ctl->incre_ckpt_sync_shmem中所有的文件fsync请求做批量fsync即可完成脏页的checkpoint。

  可以看到,在openGauss的刷脏设计中,并不会同步执行文件fsync,不需要数据立刻落盘,数据文件的fsync操作是由checkpoint线程同步请求给pagewriter main线程完成的。

最后修改时间:2024-12-17 16:53:23
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论