MySQL:刷髒相關

資料庫工作筆記發表於2023-11-15

來源:MySQL學習

這裡簡單記錄一下刷髒和IO程式碼的簡要流程(8.0.23),以下內容僅供參考。先簡要總結一下:

  • 當大於預設8192(innodb_lru_scan_depth*innodb_buffer_pool_instances)的時候只有flush list刷髒,也就是buffer pool 的page會盡量填充,不做LRU淘汰。
  • 當小於預設8192(innodb_lru_scan_depth*innodb_buffer_pool_instances)的時候會觸發LRU掃描,掃描的時候需要釋放page,如果page是乾淨的則直接釋放放入free list,且做LRU淘汰。如果page不是乾淨的則需要進行LRU刷髒,完成後且放入free list,且需要做LRU淘汰。
  • 刷盤總是先寫入到DBLWR,這是同步IO的方式,如果不是O_D開啟的檔案還需要做對DBLWR做FSYNC。
  • 刷盤寫入資料檔案由DBLWR部分完成,為非同步IO,非同步IO主要為前臺thread 發起io_submit,後臺非同步IO執行緒堵塞等待結果,然後處理完成的一些動作,比如檢查page的IO狀態,資料檔案的fsync操作,刷盤後一個更加重要的動作是非同步IO執行緒還要完成記憶體的維護,比如對於flush list刷髒需要從flush list連結串列中刪除,對於lru刷髒則額外還需要從page的hash結構和LRU連結串列中刪除並且返回給free list成為可用的空閒page。
  • 非同步IO通常在Linux上為Linux native aio,但是需要滿足條件innodb_use_native_aio = ON、libaio安裝、innodb_flush_method =O_DIRECT
  • 讀取為同步IO,預讀為非同步IO,刷髒為非同步IO,寫DBLWR為同步IO。
  • DBLWR的預設情況下為2個物理檔案,其中一個要大一些,固定為512*16K,為s_single_segments型別的segment。

一、刷髒

當cleaner執行緒及其worker執行緒需要刷髒的時候,先計算需要刷髒的page數量,然後呼叫pc_flush_slot進行刷髒,我們就從這個函式開始向下探索。

下面是一些主要流程供參考,

pc_flush_slot
  獲取一個instance進行刷盤
-----------------LRU 淘汰如果是髒頁則需要刷髒,淘汰中還要判斷是否為state 頁
  ->buf_flush_LRU_list       
    首先進行LRU刷盤,!!!主要是如果free_len 小於了srv_LRU_scan_depth需要開啟LRU淘汰或者刷盤
    ->buf_flush_do_batch(buf_pool, BUF_FLUSH_LRU, scan_depth, 0, &n_flushed);
      標記為LRU刷盤,帶入掃描深度,返回刷盤的page數量  srv_LRU_scan_depth
      ->buf_flush_batch(buf_pool, type, min_n, lsn_limit)
        min_n為帶入的scan_depth引數
        ->buf_flush_start
          ->buf_pool->init_flush[flush_type] = TRUE
            正在進行BUF_FLUSH_LRU刷髒
        -> switch (flush_type)
          case BUF_FLUSH_LRU:(這裡為LRU刷髒)
            mutex_enter(&buf_pool->LRU_list_mutex);
            ->count = buf_do_LRU_batch(buf_pool, min_n)
              這裡的count是需要返回的重新整理的page數量
              -> buf_flush_LRU_list_batch(buf_pool, max - count)
                 max - count普通情況就是max  
                 ------見下1.1-----這個就比較複雜了,需要判斷是否為state page/乾淨page/髒頁,前面2個自己維護記憶體,髒頁刷盤後非同步IO完成記憶體維護               
            mutex_exit(&buf_pool->LRU_list_mutex);
      ->buf_flush_batch
        ->count = buf_do_LRU_batch(buf_pool, min_n)
          呼叫LRU重新整理
      ->buf_flush_end
-------------------FLUSH LIST刷髒,相對簡單判斷也比較少,因為flush list都是按照順序排列的髒頁,而且不釋放page只負責刷髒,那麼在壓力小的情況LRU 刷髒基本都是乾淨的page
  ->buf_flush_do_batch(buf_pool, BUF_FLUSH_LIST, slot->n_pages_requested,page_cleaner->lsn_limit, &slot->n_flushed_list)
    slot->n_pages_requested 引數和io_cap相關。type為BUF_FLUSH_LIST
    ->buf_flush_start
      -> buf_pool->init_flush[flush_type] = TRUE
         設定對應的type正在進行重新整理
      -> os_event_reset(buf_pool->no_flush[flush_type])
         設定沒有被喚醒
    ->buf_flush_batch
      ->switch (flush_type)
        case BUF_FLUSH_LIST
        這裡帶入的flush list刷髒
        ->buf_do_flush_list_batch(buf_pool, min_n, lsn_limit)
          len = UT_LIST_GET_LEN(buf_pool->flush_list)
          獲取flush list的長度
          ->for (buf_page_t *bpage = UT_LIST_GET_LAST(buf_pool->flush_list);count < min_n && bpage != nullptr 
                 && len > 0 &&bpage->get_oldest_lsn() < lsn_limit;bpage = buf_pool->flush_hp.get(), ++scanned)
            從尾部開始迴圈,結束條件是
            A count < min_n:如果重新整理page數量的大於了需要重新整理的page數量
            B len > 0:有需要重新整理的page,空閒例項這個條件不會滿足
            C bpage->get_oldest_lsn() < lsn_limit:lsn_limit一般比較大
            ->buf_flush_page_and_try_neighbors(bpage, BUF_FLUSH_LIST, min_n, &count)
              這個參考程式碼已經標記了,但是LRU刷髒還會返回給free list
              從刷髒後的非同步IO的buf_page_io_complete函式中也可以看出BUF_FLUSH_LIST不需要淘汰LRU
              ------------------------------------------------------BUF_FLUSH_LIST: don't evict
1.1 buf_flush_LRU_list_batch
buf_flush_LRU_list_batch
  ->for (bpage = UT_LIST_GET_LAST(buf_pool->LRU);
          bpage != nullptr && count + evict_count < max &&
          free_len < srv_LRU_scan_depth + withdraw_depth &&
          lru_len > BUF_LRU_MIN_LEN; 
          ++scanned, bpage = buf_pool->lru_hp.get())
   從LRU的尾部開始掃描page,實際上這裡最重要的2個條件
   A: count + evict_count < max 
      如果重新整理的page數量小於了預期srv_LRU_scan_depth引數指定深度的page數量
      這裡evict_count代表stale page直接清理掉
      這裡count代表刷髒後清理掉
   B: free_len < srv_LRU_scan_depth + withdraw_depth 
      如果當前instance中free的page數量小於了srv_LRU_scan_depth引數的設定
   注意如果這兩個條件不滿足則不會進行LRU刷髒
     ->if (bpage->was_stale())
       如果page是stale page
       ->buf_page_free_stale(----OK)
         ->auto *block_mutex = buf_page_get_mutex(bpage)
           上page buf_block_t的mutex   
         ->io_type = buf_page_get_io_fix(bpage)
           獲取IO型別
         ->if (io_type == BUF_IO_NONE)
           如果io_type為NONE 這代表可以進行處理
           ->if (bpage->is_dirty())
             如果是髒塊
             ->buf_flush_remove(bpage)
               從flush list中刪除,注意stale狀態的page是直接從flush list刪除的,而不需要刷盤
               ->UT_LIST_REMOVE(buf_pool->flush_list, bpage)
                 從flush list中刪除
               ->bpage->set_clean()
                 set_oldest_lsn(0)
               ->if (bpage->get_flush_observer() != nullptr)
                 -> bpage->get_flush_observer()->notify_remove(buf_pool, bpage);
                 -> bpage->reset_flush_observer();
                    和DDL 有關
             <-buf_flush_remove
             ->buf_LRU_free_page
                 從hash結構和LRU中去掉這個page,並且反還給free list
                 持有鎖A.buf_pool->LRU_list_mutex B.buf_page_get_mutex(bpage)                
               ->buf_page_can_relocate
                 if (!buf_page_can_relocate(bpage)) 
                 ->(buf_page_get_io_fix(bpage) == BUF_IO_NONE && bpage->buf_fix_count == 0)
                  判定是否處於IO fixed狀態,如果處於則return false,不能直接去掉
               <-buf_page_can_relocate
               -> if (is_dirty)
                  如果是髒快則return (false),但是對這裡來講肯定不是前面已經判定了,如果是髒頁這設定lsn為0
               -> buf_LRU_block_remove_hashed
                  if (!buf_LRU_block_remove_hashed(bpage, zip, false))
                  從hash結構和LRU中去掉這個page,這需要持有 A.buf_pool->LRU_list_mutex B.buf_page_get_mutex(bpage) C.rw_lock_own(hash_lock, RW_LOCK_X)
                  同時需要確保IO沒有被fixed buf_page_get_io_fix(bpage) == BUF_IO_NONE
                  ->buf_LRU_remove_block(bpage)
                    從LRU連結串列刪除page
                    ->buf_LRU_adjust_hp(buf_pool, bpage);
                      調整風險指標
                    ->if (bpage == buf_pool->LRU_old)
                      如果來到3/8 端 需要對指標進行調整
                      ->buf_page_t *prev_bpage = UT_LIST_GET_PREV(LRU, bpage);
                        獲取前一個page
                      ->buf_page_set_old(prev_bpage, TRUE); buf_pool->LRU_old_len++;
                        設定為old指標
                    ->UT_LIST_REMOVE(buf_pool->LRU, bpage)
                      從LRU 刪除這個page
                    ->buf_page_is_old(bpage)
                      調整old len的長度
                    ->buf_LRU_old_adjust_len(buf_pool)
                      調整old page的指標
                  ->BUF_BLOCK_FILE_PAGE
                    ->buf_block_modify_clock_inc((buf_block_t *)bpage)
                      這一步主要作為最佳化,增加了buf_block_t的modify_clock++ 如果遊標中的page本值沒有改變則說明沒有修改過,可以繼續使用,否則?重新定位?
                  ->buf_page_hash_get_low
                    hashed_bpage = buf_page_hash_get_low(buf_pool, bpage->id);
                    上hash lock
                    ->HASH_SEARCH(hash, buf_pool->page_hash, page_id.fold(), buf_page_t *, bpage,
                                  ut_ad(bpage->in_page_hash && !bpage->in_zip_hash &&
                                  buf_page_in_file(bpage)),page_id == bpage->id);
                      進行hash查詢,這裡用到的是bpage->id這是一個space id和page no的結構體
                        /** Tablespace id. */space_id_t m_space;
                        /** Page number. */  page_no_t m_page_no;
                    ->返回找到的page,
                      A.如果找不到,報錯 not found in the hash table B.當前page進行比對是否是同一個page,如果不是則報錯 n hash table we find block  of  which is not
                  ->HASH_DELETE(buf_page_t, hash, buf_pool->page_hash, bpage->id.fold(), bpage)
                    從例項的hash結構刪除這個page
                  -> switch (buf_page_get_state(bpage))
                     buf_page_set_state(bpage, BUF_BLOCK_REMOVE_HASH)
                     標記page為從hash中刪除了,這是一個比較短暫的狀態  ----注意page 狀態的轉換    
               <- buf_LRU_block_remove_hashed 
               ->btr_search_drop_page_hash_index((buf_block_t *)bpage)  
                 AHI 刪除這個PAGE
               ->buf_LRU_block_free_hashed_page((buf_block_t *)bpage) 
                 -> buf_block_set_state(block, BUF_BLOCK_MEMORY)       ----注意page 狀態的轉換  
                    先設定為BUF_BLOCK_MEMORY狀態
                 ->buf_LRU_block_free_non_file_page
                   ->buf_block_set_state(block, BUF_BLOCK_NOT_USED)    ----注意page 狀態的轉換
                     設定page狀態為BUF_BLOCK_NOT_USED
                   ->UT_LIST_ADD_FIRST(buf_pool->free, &block->page)
                     將page加入到free list
               <-buf_LRU_block_free_hashed_page    
             <-buf_LRU_free_page                
       <-buf_page_free_stale 
     ->else 如果不是stale page -------- 這裡包含了是否為髒頁,分別走不同的邏輯,如果不是髒頁自己維護記憶體,如果是髒頁非同步IO維護記憶體
       ->mutex_enter_nowait         
         buf_block_t的mutex,這裡拿這個鎖,因為下面要進行判斷      
       ->if (acquired && buf_flush_ready_for_replace(bpage))
         這個分支主要是走的是非髒頁進行淘汰,不需要刷盤
         ->buf_flush_ready_for_replace(bpage)  主要判斷是否為髒頁,並且IO沒有被fixed住,判定如下
           ->if (!buf_page_in_file(bpage))
           因為這裡是掃描的LRU,如果有幾種狀態比如BUF_BLOCK_REMOVE_HASH/BUF_BLOCK_MEMORY/BUF_BLOCK_NOT_USED/BUF_BLOCK_READY_FOR_USE
           這幾種狀態的page是明顯不應該在LRU上的,因此需要報錯Buffer block " << bpage << " state " << bpage->state << " in the LRU list!";
           ->if (!buf_page_can_relocate(bpage))
             需要判斷page是否可以被重新填充,如果IO fixed了則不能重新填充
             -> return(buf_page_get_io_fix(bpage) == BUF_IO_NONE && bpage->buf_fix_count == 0)
               需要判斷是否有IO FIXED的情況
           ->if (bpage->was_stale())
             是否處於stale狀態如果是則返回為true(判斷space是否已經drop 過了),在這個流程下前面是判斷過的
           ->bpage->is_dirty
             返回的是 這個page是否是髒頁,很明顯如果是非髒頁,這不需要刷盤     
         <-buf_flush_ready_for_replace
        ->如果不是髒頁
          ->(buf_LRU_free_page(bpage, true))
            參考上面 從hash結構和LRU中去掉這個page,並且反還給free list,因為不是髒頁,因此不需要刷盤
            ++evict_count,不需要刷盤的就是就是這個統計          
       ->else if (acquired && buf_flush_ready_for_flush(bpage, BUF_FLUSH_LRU))
         這個分支主要是走的髒頁淘汰,這個需要刷盤的----------------------------- 刷盤後的非同步IO執行緒來完成記憶體的維護
         ->buf_flush_ready_for_flush
           ->!bpage->is_dirty()||buf_page_get_io_fix_unlocked(bpage) != BUF_IO_NONE
             主要判斷是否io 被fix住了,並且判斷是否是髒頁
           ->(buf_page_get_state(bpage) != BUF_BLOCK_REMOVE_HASH)
         ->buf_flush_page_and_try_neighbors(bpage, BUF_FLUSH_LRU, max, &count)
           同樣呼叫臨近頁刷髒,flush list也是這樣刷的
           ->buf_flush_page
             呼叫這裡進行刷髒,見下1.2,從後面的IO 非同步刷到磁碟的動作來看在buf_page_io_complete函式中,也是會進行LRU淘汰等管理操作,--------------------------------BUF_FLUSH_LRU: always evict
1.2 buf_flush_page
buf_flush_page
  刷髒
  先設定了page的IO fix等狀態
  ->buf_flush_write_block_low
    ->buf_flush_init_for_writing
      初始化物理page的相關header 比較重要
    ->dblwr::write
      先看正常的刷髒邏輯
      ->page_id = bpage->id
      ->if (!sync && flush_type != BUF_FLUSH_SINGLE_PAGE)
        這裡只有SINGLE_PAGE刷髒才會用到sync=true,正常的LRU和FLUSH LIST刷髒都走這裡
        ->Double_write::submit(flush_type, bpage, e_block, e_len)
          ->dblwr = instance(flush_type, bpage)
            呼叫Double_write::instance,返回dblwr instance
            ->Double_write::instance buf0dblwr.cc:508
              instance(flush_type, buf_pool_index(buf_pool_from_bpage(bpage)))
              獲取page所在buffer instance id,再次呼叫過載的Double_write::instance
            ->Double_write::instance buf0dblwr.cc:330
              實際就是根據flush_type計算出使用哪一個dblwr instance
              計算方式主要還是分LRU和FLUSH LIST ,其中BUF_FLUSH_LIST為後面8個
              BUF_FLUSH_LRU為前面8個,主要還是根據buffer的instance來匹配用哪個
              也就是每個instance使用2個BS,其中一個為BUF_FLUSH_LRU另一個為
              BUF_FLUSH_LIST
          ->dblwr->enqueue(flush_type, bpage, e_block, e_len)
            Double_write::enqueue  
            ->Double_write::prepare
              這個函式作用不大,主要檢查一下page
            ->for (;;)
             ->mutex_enter(&m_mutex)
               對本dblwr instance加鎖
             ->if (m_buffer.append(frame, len))
               呼叫dblwr::Buffer::append,這個函式如果正常寫入則返回為true,跳出迴圈,否則buffer空間滿
               則返回flase
               { break;}
             ->if (flush_to_disk(flush_type))
               buffer滿刷入到磁碟,呼叫Double_write::flush_to_disk    
               ->Double_write::wait_for_pending_batch   
               ->Double_write::write_pages
                 ->segments->dequeue(batch_segment)
                   出隊一個Batch_segment,出隊演演算法?

二、DBWRL 系統

s_flush_list_batch_segments(buffer instance=8 innodb_doublewrite_pages=4 srv_n_write_io_threads=4,f0:  (4*9)*16K)  

       BS0                     BS1         ...          BS8       
S0   S1   S2   S3      S0   S1   S2   S3         S0   S1   S2   S3
               
|     |    |    |      |    |    |    |          |    |    |    |
|     |    |    |      |    |    |    |          |    |    |    |


16K  16K 16K  16K     16K  16K 16K  16K   ...    16K  16K 16K  16K      f0:  (4*9)*16K 
        
        
 
                                                            
s_LRU_batch_segments(buffer instance=8 innodb_doublewrite_pages=4 srv_n_write_io_threads=4,f1:(4*9)*16K)                                     
s_single_segments(f1:512*16K)               
               
       BS0                     BS1         ...          BS8                S37    S38  ...  S547   S548                    
S0   S1   S2   S3      S0   S1   S2   S3         S0   S1   S2   S3          |      |         |      | 
                                                                            |      |         |      | 
|     |    |    |      |    |    |    |          |    |    |    |           |      |         |      |
|     |    |    |      |    |    |    |          |    |    |    |           |      |         |      |
                                                                                           
                                                                                       
16K  16K 16K  16K     16K  16K 16K  16K   ...    16K  16K 16K  16K         16K    16K  ...  16K     16K                           


----------------------(4*9)*16K--------------------------------------      ----------512*16K---------

 
s_segments flush:BS0...flush:BS8|LRU:BS0......LRU:BS8


s_instances(vector):  

Double_write0                 Double_write1          ...       Double_write14             Double_write15
      |                             |                                |                          |
m_buffer(16K*4)               m_buffer(16K*4)                  m_buffer(16K*4)            m_buffer(16K*4)
m_buf_pages(verctor(4))       m_buf_pages(verctor(4))          m_buf_pages(verctor(4))    m_buf_pages(verctor(4))
   --std::tuple                  --std::tuple                  --std::tuple               --std::tuple 
   --std::tuple                  --std::tuple                  --std::tuple               --std::tuple
   --std::tuple                  --std::tuple                  --std::tuple               --std::tuple
   --std::tuple                  --std::tuple                  --std::tuple               --std::tuple


--------前8 instance為BUF_FLUSH_LIST使用----------             -----------後8個 instance為BUF_FLUSH_LRU---------
2.1 DBLWR segment的建立
dblwr::open
  ->Double_write::s_n_instances = std::max(4UL, srv_buf_pool_instances * 2)
    dblwr的例項個數和buffer pool的instance個數相關,並且每個buffer pool的instance
    包含了一個LRU list和一個flush list 兩個dblwr例項,預設為16
  ->if (dblwr::n_files == 0){dblwr::n_files = 2}
    定義全域性變數dblwr::n_files的個數和引數innodb_doublewrite_files有關,預設為2,一般也不會調整這個引數
    也就是dblwr物理檔案的個數,從下面的判斷來看最大數量不能大於Double_write::s_n_instances的定義
  ->if (dblwr::n_pages == 0){dblwr::n_pages = srv_n_write_io_threads}
    定義每一個batch segment的page數量,和引數innodb_doublewrite_pages有關,預設為4,也是srv_n_write_io_threads
    的大小
  這裡使用預設引數innodb_doublewrite_files=2為列子   
  ->Double_write::s_files.resize(dblwr::n_files)
    定義s_files vecotr陣列的大小為dblwr::n_files大小,也就是有多少的dblwr檔案
  ->segments_per_file = (Double_write::s_n_instances / dblwr::n_files) + 1
    定義每個檔案batch segment的個數,這裡可以看到預設實際上就是16/2+1 也就是9個segments
    這裡定義的batch segment
  ->dblwr::File::s_n_pages = dblwr::n_pages * segments_per_file
    定義每個檔案用於batch segment的pages的總數,預設segments_per_file為9,dblwr::n_pages為4
    也就是36
  ->for (auto &file : Double_write::s_files)
    物理部分1
    迴圈每個dblwr物理檔案,檔案下標從0開始
    ->dblwr_file_open(dblwr::dir, &file - first, file, OS_DBLWR_FILE)
      建立檔案,其中dblwr::dir為引數innodb_doublewrite_dir指定的位置
      其中可以看到建立檔案的規則,比如檔名怎麼來的
    ->pages_per_file = dblwr::n_pages * segments_per_file
      這裡pages_per_file為每個檔案包含的batch segment pages的個數
    ->if ((file.m_id & 1))
      如果檔案的序號為奇數(0,1)
      ->pages_per_file += SYNC_PAGE_FLUSH_SLOTS / (Double_write::s_files.size() / 2)
        那麼pages_per_file還需要加上segment 的page個數,預設為512(SYNC_PAGE_FLUSH_SLOTS)個pages用於single dblwr write
        而預設Double_write::s_files.size()就是2,那麼預設的情況下dblwr檔案1就會比檔案0大512個page,這個512個用於
        single dblwr write
    ->Double_write::init_file(file, pages_per_file)
        根據計算的pages數量來初始化檔案大小
      -----以上檔案初始化完成,但是需要建立相應的記憶體結構
    ->Double_write::create_batch_segments(segments_per_file)
        這裡輸入的是每個檔案batch segment的個數,但是初始化是LRU和FLUSH LIST都要初始化完成的
        ->n_segments = segments_per_file * s_files.size();
          用batch segment的個數*檔案個數,預設為9*2=18
        ->n = std::max(ulint{2}, ut_2_power_up((n_segments + 1)))
          18+1 後取2的N次方,實際上就是32,這裡可以看到記憶體結構實際上多了一些batch segment
        ->s_LRU_batch_segments = UT_NEW_NOKEY(Batch_segments(n))
        ->s_flush_list_batch_segments = UT_NEW_NOKEY(Batch_segments(n))
          初始化s_LRU_batch_segments和s_flush_list_batch_segments其大小都是32個batch segment
        ->total_pages = segments_per_file * dblwr::n_pages
          計算每個檔案用於batch segment的page數量
        -> for (auto &file : s_files)
           迴圈每個dblwr檔案
           ->for (uint32_t i = 0; i < total_pages; i += dblwr::n_pages, ++id)
             這裡以dblwr::n_pages也就是每個batch segment的pages個數作為增加值來初始化
             每個batch segment
             ->auto s = UT_NEW_NOKEY(Batch_segment(id, file, i, dblwr::n_pages))
               建立batch segment,並且進行標號,從0開始標號
             ->segments = (file.m_id & 1) ? s_LRU_batch_segments: s_flush_list_batch_segments;
               如果dblwr檔案為奇數這為s_LRU_batch_segments,偶數為s_flush_list_batch_segments
               f0:s_flush_list_batch_segments
               f1:s_LRU_batch_segments
             ->success = segments->enqueue(s)
               將初始化的batch segment放入到相應的佇列中
             ->s_segments.push_back(s)
               並且放入s_segments陣列中
    ->Double_write::create_single_segments(segments_per_file)
        物理部分2
        ->n_segments = std::max(ulint{2}, ut_2_power_up(SYNC_PAGE_FLUSH_SLOTS))
          因為SYNC_PAGE_FLUSH_SLOTS為512為2的n次方
        ->s_single_segments = UT_NEW_NOKEY(Segments(n_segments))
          分配對應數量的segment,作為single dblwr的segment
        ->n_pages = SYNC_PAGE_FLUSH_SLOTS / (s_files.size() / 2)
          預設s_files=2,因此這裡n_pages=512
        ->for (auto &file : s_files) 
          開始迴圈每個檔案
          ->if (!(file.m_id & 1) && s_files.size() > 1){continue;}
            偶數檔案直接continue,因此這裡是奇數檔案 奇數檔案就是s_LRU_batch_segments 對應的檔案,那麼這個檔案要大一些
          ->start = dblwr::File::s_n_pages
            這裡跳過batch segments部分
          ->for (uint32_t i = start; i < start + n_pages; ++i)
            這裡start為batch segments部分,n_pages為single dblwr的segment的page數量,每次增加1
            ->auto s = UT_NEW_NOKEY(Segment(file, i, 1UL)); 
              這裡的1UL就代表一個page,根據資訊初始化segment資訊
            ->success = s_single_segments->enqueue(s)
              加入到s_single_segments中這是一個Segments的陣列
    ->Double_write::create_v2()  
      建立記憶體部分資料結構
      ->for (uint32_t i = 0; i < s_n_instances; ++i)
        根據s_n_instances的數量進行dblwr instance的初始化,s_n_instances就是buffer instance *2 為16
        ->ptr = UT_NEW_NOKEY(Double_write(i, dblwr::n_pages))
          其中dblwr::n_pages為batch segment對應的page數量,預設為4
          ->auto ptr = UT_NEW_NOKEY(Double_write(i, dblwr::n_pages))
            m_id(id), m_buffer(n_pages), m_buf_pages(n_pages)
            mutex_create(LATCH_ID_DBLWR, &m_mutex)
            這裡主要初始化一個mutex和響應的buffer,初始化m_buffer和m_buf_pages
            A.m_buffer:定義為dblwr::Buffer型別,其本質為一段記憶體,其大小為n_pages+1,考慮到對齊操作因此+1,這是作為快取用的
            B.m_buf_pages:定義為Double_write::Buf_pages型別,其為一個std::tuple<buf_page_t *, const file::Block *, uint32_t>
              型別的vector,verctor的大小為n_pages定義,這是收集的髒資料的buf_page_t
         ->s_instances->push_back(ptr)
           將初始化好的Double_write的指標push到s_instances中
2.2 DBLWR segment 的使用
Double_write::flush_to_disk
  如果本instance的buffer滿了則寫入到磁碟
  ->wait_for_pending_batch()
    等待其他batch write 寫入
  -> segments = flush_type == BUF_FLUSH_LRU ? s_LRU_batch_segments : s_flush_list_batch_segments;
     根據型別分配到底是那個BS,因為一個instance分為2個BS,一個是LRU的一個是FLUSH的
  -> segments->dequeue(batch_segment)
     出隊已滿的BS,因為一個BS,就是4個page的物理空間。這裡需要確認如何選擇BS的,debug來看都是整數
     !!!!!在某些情況下不一定buffer滿才刷page,因此可能存在寫 1 2 3 個page的情況
  -> batch_segment->start(this)
     呼叫Batch_segment::start,傳入本instance,標記已經開始刷盤
  -> batch_segment->write(m_buffer)
     呼叫Batch_segment::write,將instance中buffer的資料寫入到本batch_segment
     ->Segment::write(buffer.begin(), buffer.size())
       ->IORequest req(IORequest::WRITE | IORequest::DO_NOT_WAKE)
         建立io請求
       ->req.dblwr()
         m_type |= DBLWR
         設定type為DBLWR
       ->os_file_write_retry(req, m_file.m_name.c_str(), m_file.m_pfs,ptr, m_start, len)
         len 也可以是一個page,force的情況,也可以是4個page或者1個page,
         ->pfs_os_file_write_func
          ->os_file_write_retry
           ->os_file_pwrite(同步IO)
  -> m_buffer.clear()
     清理buffer instance的快取
  ->if (is_fsync_required())
    呼叫Double_write::is_fsync_required    
    ->srv_unix_file_flush_method != SRV_UNIX_O_DIRECT && srv_unix_file_flush_method != SRV_UNIX_O_DIRECT_NO_FSYNC
     是否需要刷盤,如果為O_DIRECT 就不需要刷DBLWR,如果需要刷盤則呼叫下面的
    -> batch_segment->flush()
       Segment::flush  
       ->os_file_flush(m_file.m_pfs)
  ->for (uint32_t i = 0; i < m_buf_pages.size(); ++i)
    開啟迴圈           
    ->bpage = std::get<0>(m_buf_pages.m_pages[i])
    ->bpage->set_dblwr_batch_id(batch_segment->id())
    ->write_to_datafile(bpage, false, std::get<1>(m_buf_pages.m_pages[i]),std::get<2>(m_buf_pages.m_pages[i]))
      髒資料寫入到資料檔案,page\false\壓縮相關\長度
      Double_write::write_to_datafile
      這裡不考慮壓縮的page也就是e_block == nullptr的情況
      ->Double_write::prepare(in_bpage, &frame, &len)
        做一些檢查
      ->IORequest io_request(type)
        建立IO請求
        IORequest::WRITE 
        不需要做fsync
        IORequest::DO_NOT_WAKE
      ->fil_io(io_request, sync, bpage->id, bpage->size, 0, len, frame, bpage)
        ->shard = fil_system->shard_by_id(page_id.space())
          獲取shard
        ->shard->do_io((type, sync, page_id, page_size, byte_offset, len, buf,message)
          Fil_shard::do_io 

三、非同步IO部分

預讀為例子:
 -----------共享io_context_t-------------- 
  |                                    |
  |                                    |
session thread   kernel             AIO thread             
  |                |                   |    
  |                |                   |
  |                |                   |
  |--------->      |                   |
  |  提交IO        |                   |
  | IO_SUBMIT    處理IO請求            |
  |                | <-----------------|                 
 提交後session            io_getevents |
 就可以繼續了不           收割IO       |
 等待IO完成                            |
                                    Fil_shard::complete_io
                                    處理檔案的相關資訊
                                    buf_page_io_complete
                                    處理page的資訊
非同步IO主要完成不是馬上需要page的情況,可以將page的讀取
由AIO thread 處理後直接調入到記憶體,比如預讀。
3.1 刷髒提交IO
Fil_shard::do_io
 ->aio_mode = get_AIO_mode(req_type, sync)
   獲取aio的型別,這裡一共有幾種IO型別,
   AIO_mode::SYNC   同步 IO
   AIO_mode::LOG    redo IO
   AIO_mode::NORMAL 普通 非同步IO
   AIO_mode::IBUF   ibuf IO
   if (sync) { //如果是sync就是同步IO
    return AIO_mode::SYNC;
  } else if (req_type.is_log()) {
    return AIO_mode::LOG;
  } else {
    return AIO_mode::NORMAL;
  }
 ->並且按照讀寫分別放入
   srv_stats.data_read.add
   srv_stats.data_written.add
   中
 ->bpage = static_cast<buf_page_t *>(message)
 ->slot = mutex_acquire_and_get_space(page_id.space(), space)
   為開啟的檔案分配slot,注意這裡還包含了關閉開啟的檔案,如果
   超過了innodb_open_file_limits 引數
   ->fil_system->m_max_n_open <= s_n_open
   ->fil_system->close_file_in_all_LRU
 ->opened = prepare_file_for_io(file, false)
   Fil_shard::prepare_file_for_io
   ->if (!open_file(file, extend))
     Fil_shard::open_file
     開啟檔案
 ->os_aio(os_aio_func)
   這個函式是同步IO和非同步IO都是它調入主要看AIO_mode
   -> if (aio_mode == AIO_mode::SYNC && !srv_use_native_aio)
     -> if (type.is_read())
       呼叫pread,直接讀取
      { return (os_file_read_func(type, name, file.m_file, buf, offset, n)) }
       ->os_file_read_page
         ->os_file_pread
             ++os_n_file_reads
             os_n_pending_reads.fetch_add(1)
           ->os_file_io
             os_n_pending_reads.fetch_sub(1)
     -> 呼叫pwrite,直接寫入 
      return (os_file_write_func(type, name, file.m_file, buf, offset, n))
      和上面差不多的路徑           
   如果不是同步IO就需要進行非同步IO,將請分配給非同步IO執行緒
   -> array = AIO::select_slot_array(type, read_only, aio_mode)
   -> slot = array->reserve_slot(type, m1, m2, file, name, buf, offset, n, e_block)
   -> if (type.is_read())
      -> if (srv_use_native_aio){
         ++os_n_file_reads
      -> array->linux_dispatch(slot)
   -> if (type.is_write())
      -> if (srv_use_native_aio){
         ++os_n_file_writes
      -> array->linux_dispatch(slot)
        -> 這裡呼叫io_submit進行IO 提交,隨後由非同步IO執行緒進行收集
   -> return (DB_SUCCESS)
      /* AIO request was queued successfully! */
 ->if (sync) {
   如果是同步IO ,注意這裡是同步IO,不是fsync
   ->complete_io(file, req_type)
     Fil_shard::complete_io
     ->--file->n_pending;
      pending IO 減1
      ->if (type.is_write())
3.2 非同步IO執行緒處理
fil_aio_wait
  ->os_aio_handler
   ->os_aio_linux_handler
     ->handler.poll
       主要看做IO的收集的部分,因為IO正常是使用者執行緒自己提交的
       ->slot = find_completed_slot(&n_pending);  //這裡看應該是IO比較慢的情況
         找到可能的partial IO slot,透過迴圈整個segment中的solt完成
         ->LinuxAIOHandler::find_completed_slot
           ->for (ulint i = 0; i < m_n_slots; ++i, ++slot)
             -> if(slot->is_reserved)
                迴圈整個segment中的slot情況,如果存在,++*n_pending
                增加pending IO
               ->if (slot->io_already_done) 
                如果IO已經完成
               ->return (slot)
                返回這個slot
       ->if (slot != nullptr)
         如果slot不為null,這檢查是否是否為partial IO slot
         如果是需要重新提交
       ->err = check_state(slot);
       ->如果err為DB_FAIL,這呼叫resubmit來提交IO
       ->else srv_set_io_thread_op_info(m_global_segment,
                                "waiting for completed aio requests");
         這裡可以看出正在等待完成的aio 請求,如果有則負責收割IO
       ->collect()
          LinuxAIOHandler::collect
         ->io_getevents()    
           非同步IO堵塞收集,這裡收割IO 
  ->srv_set_io_thread_op_info(segment, "complete io for file"); ---
    設定IO已經完成
  ->Fil_shard::complete_io
    這個函式不管同步IO還是非同步IO都要呼叫,這個函式主要是對檔案資訊的維護
    是否
    ->--file->n_pending; 
     pending IO 減少
    ->if (type.is_write())
      如果是寫操作,則呼叫
      ->write_completed
       ->add_to_unflushed_list(file->space)
         ->UT_LIST_ADD_FIRST(m_unflushed_spaces, space);
           這個list主要用於儲存需要flush的資料檔案,以便後面使用
      ->UT_LIST_ADD_FIRST(m_LRU, file)
        如果是讀操作直接加入 file 的 LRU連結串列的頭部,如果淘汰file,這按照這個淘汰。   
  ->switch (file->space->purpose)
    如果是普通的page則呼叫
    ->srv_set_io_thread_op_info(segment, "complete io for buf page"); ---
    ->buf_page_io_complete(static_cast<buf_page_t *>(m2), false);
      這個函式不管同步IO還是非同步IO都要呼叫
      ->switch (io_type)
       ->case BUF_IO_READ
         ->buf_page_set_io_fix(bpage, BUF_IO_NONE);
         ->buf_pool->n_pend_reads.fetch_sub(1);
         ->buf_pool->stat.n_pages_read.fetch_add(1);
       ->case BUF_IO_WRITE
         ->buf_flush_write_complete(bpage)
           這個過程加buffer pool instance flush_state_mutex 鎖
           還完成dblwr的解鎖
           ->buf_pool = buf_pool_from_bpage(bpage)
           ->buf_flush_remove(bpage);
             從flush中刪除,同上
           ->buf_page_set_io_fix(bpage, BUF_IO_NONE);
             清理IO狀態
           ->flush_state_mutex 解鎖
           ->dblwr::write_complete(bpage, flush_type)
             ->Double_write::write_complete
               釋放batch segment等資源
               ->fil_flush_file_spaces(FIL_TYPE_TABLESPACE)
               負責對unflush的檔案進行刷盤
                ->fil_system->flush_file_spaces(purpose) Fil_system::flush_file_spaces
                  ->for (auto shard : m_shards)
                    迴圈每個file shard
                    ->shard->flush_file_spaces(purpose)
                      ->for (auto space = UT_LIST_GET_FIRST(m_unflushed_spaces); space != nullptr;space = UT_LIST_GET_NEXT(unflushed_spaces, space))
                        迴圈每個unflushed datafile的檔案和purpose進行比對,如果目標相同則
                        space_ids.push_back(space->id)
                        space id放入到space_ids vector中
                      ->迴圈space_ids 這裡已經是收集的做了刷盤的檔案
                        ->做 space_flush(space_id)
                          非同步IO合併了刷盤的IO 請求,進行fsync的壓力也會變小  
                          -> ++file.n_pending_flushes
                             增加pending IO,這個IO來自資料檔案的刷盤
                          -> os_file_flush(file.handle)
                            -> os_file_fsync_posix(file)
                          ->--file.n_pending_flushes
                             減少pending IO                                      
           ->if (flush_type == BUF_FLUSH_LRU)
             ->evict = true
               需要從LRU中剔除page
             ->if (evict && buf_LRU_free_page(bpage, true))
               同上,從hash結構和LRU中去掉這個page,並且反還給free list


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027826/viewspace-2995374/,如需轉載,請註明出處,否則將追究法律責任。

相關文章