1. 程式人生 > >InnoDB日誌管理機制(七) – 運維派

InnoDB日誌管理機制(七) – 運維派

引子:

書接上文,在之前六篇講述了寫日誌,其實正常情況下,這都是無用功,因為根本用不到。上一節講到了,在什麼情況下會用到日誌,以及在什麼時候會用到,如何用到等等內容,我們這一節繼續講述,在掃描完成日誌之後,如何做資料庫恢復工作,裡面有什麼邏輯,有什麼可以改進的地方等等,這都是我們讀者要去深思的地方。

(本書作者在“白家大院”齊聚首)

從這些程式碼段中可以看到,快取到HASH表之後,應該是可以找合適的時機去APPLY了,那什麼時候呢?我們可以返回去看看函式recv_scan_log_recs的最後,呼叫了函式recv_apply_hashed_log_recs,那這個就是我們要找的真正做APPLY的函數了。我們詳細看一下它的實現。

繼續:

從這些程式碼段中可以看到,快取到HASH表之後,應該是可以找合適的時機去APPLY了,那什麼時候呢?我們可以返回去看看函式recv_scan_log_recs的最後,呼叫了函式recv_apply_hashed_log_recs,那這個就是我們要找的真正做APPLY的函數了。我們詳細看一下它的實現。

UNIV_INTERN void recv_apply_hashed_log_recs(

    ibool   allow_ibuf

)   

{

    /* local vaiables … */

loop:

    recv_sys->apply_log_recs = TRUE;

    recv_sys->apply_batch_on = TRUE;

    /* 遍歷HASH表?是的,把HASH表中的每一個桶中的每一個頁面,連續處理 */

    for (i = 0; i < hash_get_n_cells(recv_sys->addr_hash); i++) {

        /* 遍歷HASH表一個桶中的多個地址 */

        for (recv_addr = static_cast<recv_addr_t*>(

                HASH_GET_FIRST(recv_sys->addr_hash, i));

             recv_addr != 0;

             recv_addr = static_cast<recv_addr_t*>(

                HASH_GET_NEXT(addr_hash, recv_addr))) {

            /* 針對每一個頁面,做這個頁面上所有的REDO操作 */

            ulint   space = recv_addr->space;

            ulint   zip_size = fil_space_get_zip_size(space);

            ulint   page_no = recv_addr->page_no;

            if (recv_addr->state == RECV_NOT_PROCESSED) {

                mutex_exit(&(recv_sys->mutex));

                if (buf_page_peek(space, page_no)) {

                    buf_block_t*    block;

                    mtr_start(&mtr);

                    block = buf_page_get(

                        space, zip_size, page_no,

                        RW_X_LATCH, &mtr);

                    buf_block_dbg_add_level(

                        block, SYNC_NO_ORDER_CHECK);

                    /* 恢復一個頁面的資料,APPLY recv_addr中儲存的所有REDO記錄,

                    這裡使用了一個MTR來恢復。需要注意的是,這個MTR只是用來

                    獲取頁面時,給這個頁面加鎖使用的,而不會涉及REDO操作,因為

                    REDO是不需要再寫日誌的,所以不用擔心這個MTR涉及到的日誌量

                    太大的問題 */

                    recv_recover_page(FALSE, block);

                    mtr_commit(&mtr);

                } else {

                    /* 這裡的操作是,如果上面的buf_page_peek沒有在Buffer Pool中

                    找到這個頁面,那這裡就從檔案中將這個頁面載入到Buffer Pool,

                    並且預讀32個頁面以提高效能。恢復方法與是一樣的。*/

                    recv_read_in_area(space, zip_size, page_no);

                }

                mutex_enter(&(recv_sys->mutex));

            }

        }

    }

    /* Wait until all the pages have been processed */

    while (recv_sys->n_addrs != 0) {

        mutex_exit(&(recv_sys->mutex));

        os_thread_sleep(500000);

        mutex_enter(&(recv_sys->mutex));

    }

    /* Wait for any currently run batch to end. 

    如註釋所述,如果上面的操作做完了,則需要保證這些日誌APPLY之後

    要在ibdata及ibd(s)中落地,此時就會將Buffer Pool中全部的髒頁刷一遍

    以保證已經處理的這些日誌失效。可能有人會問,如果在恢復的過程中,假設

    就是這裡吧,還沒有做刷盤操作,資料庫又掛了,那怎麼辦?

    其實沒事兒,整個恢復過程,日誌也沒有寫,只是掃描了一遍,並且有可能在

    Buffer Pool中已經寫了很多頁面,有可能這些頁面已經因為LRU已經刷過

    了,但這些操作是可重入的,也就是說,資料庫再起來,可以重新做一次REDO

    操作,直到做成功為止。*/

    success = buf_flush_list(ULINT_MAX, LSN_MAX, NULL);

    recv_sys->apply_log_recs = FALSE;

    recv_sys->apply_batch_on = FALSE;

    /* 將HASH表中快取的所有內容清空 */

    recv_sys_empty_hash();

    mutex_exit(&(recv_sys->mutex));

}

到這裡,我們應該已經清楚了REDO資料庫恢復的整個過程,並且可以返回到函式recv_recovery_from_checkpoint_start_func中,看一下最後的說明,做完REOD之後,做一次檢查點以說明這次資料庫恢復已經完成。

但這裡我又有話說了,各位同學有沒有發現一個細節,那就是InnoDB在辛辛苦苦的將所有日誌分析並且根據不同頁面通過HASH表儲存之後,我們特別要注意下面兩點特徵:

  1. 對於同一個頁面的REDO記錄,必然是儲存在同一個HASH桶中的。
  2. 對於某一個頁面的所有日誌記錄,是按照先後順序來管理的。

這兩個特徵非常重要,因為我們知道,REDO日誌的APPLY,與順序有關係,LSN小的,必定要比LSN大的先做APPLY,不然有可能造成資料的覆蓋。但這有一個前提就是同一個頁面,不同頁面之間是不存在這樣的問題的。

那我們想想,是不是隻需要保證,同一個頁面的日誌順序執行其所有的日誌記錄即可,而不同頁面就沒必要守這個規則了,答案是肯定的。

那目前InnoDB難道不是這樣做的麼?上面程式碼中我們已經看到了,他是用了一個兩層迴圈,掃描了整個HASH表,慢慢的一條條的做REDO恢復。基於上面的分析,其實可以大膽的想象的一下,REDO恢復可以實現並行恢復。按照桶的下標為鍵值分配執行緒,那這樣同一個桶必然會分到同一個執行緒中去做,這樣自然保證了同一個頁面的執行順序,而不同的桶之間的頁面是沒有關係的,自然就可以並行恢復了。

啊?可以這樣?這個想法,可能會讓那些把日誌檔案設定的很大,又經常出現機器宕機問題的同學(上面已經提到了他們)心潮澎湃,這樣效能提升的不只一點點了。

還是那句話,這個是需要把日誌檔案設定很大,並且經常出現宕機時,優化效果才明顯。有需求,就能解決,我們希望這個優化會出現在某個版本中,少一些浪費的時間。

到現在為止,REDO日誌的恢復就做完了,到這個時候,才真正體現了這個“累贅”的價值,感謝有你!

上面所講的,是使用REDO日誌來恢復資料庫的過程,在它做完之後,整個資料庫就是完整的了,已經保證了所有的資料庫表都沒有丟資料的情況,所有的資料庫頁面也已經是完整的了。假設此時對資料庫做DML操作,也已經是可以的了,但還有一個問題沒有處理,那就是此時的資料庫,存在髒資料。因為有些事務沒有提交,但資料已經存在了(舉一個例子,事務在做的過程中,日誌已經寫完並刷盤,就是沒有提交,此時資料庫掛了),那根據事務的ACID特性,這樣的資料就不應該存在,此時InnoDB需要做的就是把這些事務回滾掉,這就用到了我們下面將要講的“資料庫回滾”。

資料庫

(神形兼備啊,另外那種霸氣也流露出來了)

資料庫回滾

回滾段的管理,也是有一個入口位置用來儲存回滾段的管理資訊的,在InnoDB中,是用第6個頁面(5號)來管理的,這個頁面是專門用來儲存事務相關的資訊的,我們先看看其頁面格式:

/** Transaction system header */

/*————————————————————- @{ */

#define TRX_SYS_TRX_ID_STORE    0   /*!< the maximum trx id or trx

                    number modulo

                    TRX_SYS_TRX_ID_UPDATE_MARGIN

                    written to a file page by any

                    transaction; the assignment of

                    transaction ids continues from

                    this number rounded up by

                    TRX_SYS_TRX_ID_UPDATE_MARGIN

                    plus

                    TRX_SYS_TRX_ID_UPDATE_MARGIN

                    when the database is

                    started */

#define TRX_SYS_FSEG_HEADER 8   /*!< segment header for the

                    tablespace segment the trx

                    system is created into */

#define TRX_SYS_RSEGS       (8 + FSEG_HEADER_SIZE)

                    /*!< the start of the array of

                    rollback segment specification

                    slots */

上面定義的是第6號頁面中儲存的資訊及其對應的位置,每一項的詳細意義如下:

  1. TRX_SYS_TRX_ID_STORE:用來儲存事務號,在每次新啟動一個事務時,都會去檢查當前最大事務號是不是達到了TRX_SYS_TRX_ID_WRITE_MARGIN(256)的倍數,如果達到了,就會將最大的事務號寫入到這個位置,在下次啟動時,將這個值取出來,再加上一個步長(TRX_SYS_TRX_ID_WRITE_MARGIN),來保證事務號的唯一性,其實就是一個經典的取號器的實現原理。
  2. TRX_SYS_FSEG_HEADER:用來儲存事務段資訊。
  3. TRX_SYS_RSEGS:這是一個數組,InnoDB有128個回滾段,那這個陣列的長度就是128,每一個元素佔用8個位元組,對應的一個回滾段儲存的內容包括回滾段首頁面的表空間ID號及頁面號。

而針對每一個回滾段,即上面陣列中的一個元素,也有其自己的儲存格式,程式碼中的巨集定義如下:

#define TRX_RSEG_MAX_SIZE   0   /* Maximum allowed size for rollback

                    segment in pages */

#define TRX_RSEG_HISTORY_SIZE   4   /* Number of file pages occupied

                    by the logs in the history list */

#define TRX_RSEG_HISTORY    8   /* The update undo logs for committed

                    transactions */

#define TRX_RSEG_FSEG_HEADER    (8 + FLST_BASE_NODE_SIZE)

                    /* Header for the file segment where

                    this page is placed */

#define TRX_RSEG_UNDO_SLOTS (8 + FLST_BASE_NODE_SIZE + FSEG_HEADER_SIZE)

                    /* Undo log segment slots */

上面這些資訊的儲存,是從頁面偏移38的位置開始的,在這個位置之前,儲存的是檔案管理的資訊(講參考索引管理相關章節),從38開始,儲存了上面5個資訊,它們的意義分別如下:

  1. TRX_RSEG_MAX_SIZE:回滾段管理頁面的總數量,即所有undo段頁面之和,一般為ULINT_MAX,即無上限。
  2. TRX_RSEG_HISTORY_SIZE:這個表來表示當前InnoDB中,在History List中有多少頁面,即需要做PURGE的回滾段頁面個數。
  3. TRX_RSEG_HISTORY:這個用來儲存History List的連結串列首地址,事務提交之後,其對應的回滾段如果還不能PURGE,那都會加入到這個連結串列中。
  4. TRX_RSEG_FSEG_HEADER:這個用來儲存回滾段的Inode位置資訊,通過這個地址,就可以找到這個段的詳細資訊。
  5. TRX_RSEG_UNDO_SLOTS:這個位置所儲存的是一個數組,長度為1024,每一個元素是一個頁面號,初始化為FIL_NULL,即空頁面。

這5個資訊,儲存了一個回滾段的資訊,最後一個位置的陣列,就是用來真正儲存回滾段的位置,我們後面會講到這128*1024個槽是如何使用的。

根據上面的講述,我們現在已經知道所有回滾段的儲存架構了,如下圖所示:

儲存架構

現在就可以知道,InnoDB中支援的回滾段總共有128*1024=131072個,TRX_RSEG_UNDO_SLOTS陣列的每個元素指向一個頁面,這個頁面對應一個段,頁面號就是段首頁的頁面號。

在每一個事務開始的時候,都會分配一個rseg,就是從長度為128的陣列中,根據最近使用的情況,找到一個臨近位置的rseg,在這個事務的生命週期內,被分配的rseg就會被這個事務所使用。

在事務執行過程中,會產生兩種回滾日誌,一種是INSERT的UNDO記錄,一種是UPDATE的UNDO記錄,可能有人會問DELETE哪去了?其實是包含在UPDATE的回滾記錄中的,因為InnoDB把UNDO分為兩類,一類就是新增,也就是INSERT,一類就是修改,就是UPDATE,分類的依據就是事務提交後要不要做PURGE操作,因為INSERT是不需要PURGE的,只要事務提交了,那這個回滾記錄就可以丟掉了,而對於更新和刪除操作而言,如果事務提交了,還需要為MVCC服務,那就需要將這些日誌放到History List中去,等待去做PURGE,以及MVCC的多版本查詢等,所以分為兩類。

所以,一個事務被分配了一個rseg之後,通常情況下,如果一個事務中既有插入,又有更新(或刪除),那這個事務就會對應兩個UNDO段,即在一個rseg的1024個槽中,要使用兩個槽來儲存這個事務的回滾段,一個是插入段,一個是更新段。

在事務要儲存回滾記錄的時候,事務就要從1024個槽中,根據相應的更新型別(插入或者更新)找到空閒的槽來作為自己的UNDO段。如果已經申請過相同型別的UNDO段,就直接使用,否則就需要新建立一個段,並將段首頁號寫入到這個rseg的長度為1024的陣列的對應位置(空閒位置)中去,這樣就將具體的回滾段與整個架構聯絡起來了。

如果在1024個槽中找不到空閒的位置,那這個事務就會被回滾掉,報出錯誤為:“Too many active concurrent transactions”,錯誤號為1637的異常。當然這種情況一般不會見到,如果能把這個用完,估計資料庫已經根本動不了了。

上面講述了整個回滾段儲存架構及與事務的相關性,具體到一個事務所使用的某個回滾段的管理,就儲存在了回滾段首頁中,管理資訊包括三部分,分別是Undo page header、Undo segment header及Undo log header。下面分別介紹:

Undo page header:

/** Transaction undo log page header offsets */

#define TRX_UNDO_PAGE_TYPE  0   /*!< TRX_UNDO_INSERT or

                    TRX_UNDO_UPDATE */

#define TRX_UNDO_PAGE_START 2   /*!< Byte offset where the undo log

                    records for the LATEST transaction

                    start on this page (remember that

                    in an update undo log, the first page

                    can contain several undo logs) */

#define TRX_UNDO_PAGE_FREE  4   /*!< On each page of the undo log this

                    field contains the byte offset of the

                    first free byte on the page */

#define TRX_UNDO_PAGE_NODE  6   /*!< The file list node in the chain

                    of undo log pages */

  1. TRX_UNDO_PAGE_TYPE:這個我們在上面已經解釋過了,就包括兩個值,分別是TRX_UNDO_INSERT和TRX_UNDO_UPDATE。
  2. TRX_UNDO_PAGE_START:用來表示當前頁面中,從什麼位置開始儲存了UNDO日誌。
  3. TRX_UNDO_PAGE_FREE:與上面的START相對,這個用來表示當前頁面中,UNDO日誌的結束位置,也表示從這個位置開始,可以繼續追加UNDO日誌,直到頁面儲存滿為止。
  4. TRX_UNDO_PAGE_NODE:一個UNDO段中所有的頁面,通過一個雙向連結串列來管理,這個位置儲存的就是雙向連結串列的指標。

Undo segment header:

/** Undo log segment header */

#define TRX_UNDO_STATE      0   /*!< TRX_UNDO_ACTIVE, … */

#define TRX_UNDO_LAST_LOG   2   /*!< Offset of the last undo log header

                    on the segment header page, 0 if

                    none */

#define TRX_UNDO_FSEG_HEADER    4   /*!< Header for the file segment which

                    the undo log segment occupies */

#define TRX_UNDO_PAGE_LIST  (4 + FSEG_HEADER_SIZE)

                    /*!< Base node for the list of pages in

                    the undo log segment; defined only on

                    the undo log segment’s first page */

  1. TRX_UNDO_STATE:用來儲存當前UNDO段的狀態,狀態包括TRX_UNDO_ACTIVE,TRX_UNDO_CACHED、TRX_UNDO_TO_FREE、TRX_UNDO_TO_PURGE、TRX_UNDO_PREPARED五種。
  2. TRX_UNDO_LAST_LOG:用來儲存最後一個UNDO日誌的偏移位置,用來在一個UNDO段中,找到最後一個UNDO日誌。
  3. TRX_UNDO_FSEG_HEADER:這個位置,就是用來儲存當前UNDO段的Inode資訊的,通過這個資訊可以知道本UNDO段的詳細資訊。
  4. TRX_UNDO_PAGE_LIST:段內所有的頁面都是通過連結串列連線起來的,這個位置是連結串列的首地址,用來管理這個連結串列,上面已經介紹的TRX_UNDO_PAGE_NODE則是每個節點的雙鏈指標。

Undo log header:

/** The undo log header. There can be several undo log headers on the first

page of an update undo log segment. */

#define TRX_UNDO_TRX_ID     0   /*!< Transaction id */

#define TRX_UNDO_TRX_NO     8   /*!< Transaction number of the

                    transaction; defined only if the log

                    is in a history list */

#define TRX_UNDO_DEL_MARKS  16  /*!< Defined only in an update undo

                    log: TRUE if the transaction may have

                    done delete markings of records, and

                    thus purge is necessary */

#define TRX_UNDO_LOG_START  18  /*!< Offset of the first undo log record

                    of this log on the header page; purge

                    may remove undo log record from the

                    log start, and therefore this is not

                    necessarily the same as this log

                    header end offset */

#define TRX_UNDO_XID_EXISTS 20  /*!< TRUE if undo log header includes

                    X/Open XA transaction identification

                    XID */

#define TRX_UNDO_DICT_TRANS 21  /*!< TRUE if the transaction is a table

                    create, index create, or drop

                    transaction: in recovery

                    the transaction cannot be rolled back

                    in the usual way: a ‘rollback’ rather

                    means dropping the created or dropped

                    table, if it still exists */

#define TRX_UNDO_TABLE_ID   22  /*!< Id of the table if the preceding

                    field is TRUE */

#define TRX_UNDO_NEXT_LOG   30  /*!< Offset of the next undo log header

                    on this page, 0 if none */

#define TRX_UNDO_PREV_LOG   32  /*!< Offset of the previous undo log

                    header on this page, 0 if none */

#define TRX_UNDO_HISTORY_NODE   34  /*!< If the log is put to the history

                    list, the file list node is here */

這是一個針對UNDO日誌的頭資訊,一個事務寫入一次UNDO日誌就會建立一個UNDO日誌單元,都會對應一個這樣的UNDO日誌頭資訊,用來管理這個日誌資訊的狀態,儲存一些相關的資訊以備恢復時使用,多個UNDO日誌之間,通過雙向連結串列連線起來(通過我們即將介紹的TRX_UNDO_NEXT_LOG及TRX_UNDO_PREV_LOG來管理)。

  1. TRX_UNDO_TRX_ID:用來儲存當前UNDO日誌對應的事務的事務ID號。
  2. TRX_UNDO_TRX_NO:事務序列號,在恢復時使用,這個序列號就是我們前面講的TRX_SYS_TRX_ID_STORE位置儲存的ID值。這個與上面的ID的區別是,NO用來在回滾時保持順序使用,而ID是在事務執行時使用的。
  3. TRX_UNDO_DEL_MARKS:用來表示當前UNDO日誌中有沒有通過打標誌刪除過記錄的操作,並決定是不是要做PURGE操作。
  4. TRX_UNDO_LOG_START:用來儲存當前頁面中,第一個UNDO日誌的開始位置。
  5. TRX_UNDO_XID_EXISTS:用來標誌當前日誌中有沒有包含Xid事務。
  6. TRX_UNDO_DICT_TRANS:用來標誌當前日誌對應的事務是不是DDL的,用來在回滾時判斷如何操作。
  7. TRX_UNDO_TABLE_ID:與上面一個相關,如果上面標誌是真的,則這個標誌的是DDL的表ID。
  8. TRX_UNDO_NEXT_LOG:用來連結當前UNDO段中所有的UNDO日誌,這個是指向下一個UNDO日誌。
  9. TRX_UNDO_PREV_LOG:與上一個對應,這個用來指向上一個UNDO日誌,從而構成雙向連結串列。
  10. TRX_UNDO_HISTORY_NODE:用來儲存在History List中的雙向連結串列指標。而這個連結串列的首地址,是在之前我們所介紹的TRX_RSEG_HISTORY位置,可以回到前面去檢視相關資訊。

到現在為止,關於具體一個UNDO段中每個頁面及頁面內容是如何管理的已經清楚了,當一個事務需要寫入UNDO日誌時,就可以直接從對應的UNDO段中找到一個頁面及對應的追加日誌的偏移位置,然後將對應的UNDO日誌寫入即可。

UNDO日誌

(還是昨天那個球,線下討論了關於會原始碼的作用,有很多人認為,一提到某人會原始碼,就覺得一般是用來裝一下的,並且實際上沒有幾家公司強大到放心地讓你寫原始碼去,所以覺得這不是一個名副其實的技能。我認為實則不然,會原始碼,99%的機會不是用來寫原始碼的,而是閱讀其實際方法及原理,儘可能的做到知MySQL,或者在出現問題之後,是一個很好的用來解決問題的方法,要知道,我不只一次碰到手冊上面所述內容是錯誤的,並且有很多問題網上也是沒有的,那可想而知,如果你會閱讀原始碼了,這樣的問題可以立刻迎刃而解)

篇外續

日誌這篇,以這樣的角度及思維模式來講述真的是沒有見過的,並且到今天已經是連載第七篇,

可以很確定的告訴大家,還有幾篇明天繼續發。

就在今天,關於這篇內容,我們的韓朱忠老師也說話了:

日誌

他也是我們本書的推薦序之一。在這裡謝謝他。

文章來自微信公眾號:DBAce