undo空間管理(可以收縮undo log回滾日誌物理檔案空間)
1. 背景
InnoDB儲存引擎中,undo在完成事務回滾和MVCC之後,就可以purge掉了,但undo在事務執行過程中,進行的空間分配如何回收,就變成了一個問題。 我們親歷使用者的小例項,因為一個大事務,導致ibdata file到800G大小。
我們先大致看下InnoDB的undo在不同的版本上的一些演進:
MySQL 5.5的版本上
InnoDB undo是放在系統表空間即ibdata file檔案中,這樣如果有比較大的事務(即需要生成大量undo的),會撐大ibdata資料檔案,
雖然空間可以重用, 但檔案大小不能更改。
關於回滾段的,只有這個主要的引數,用來設定多少個rollback segment。
mysql> show global variables like '%rollback_segment%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_rollback_segments | 128 |
+----------------------------+-------+
MySQL 5.6的版本上
InnoDB undo支援獨立表空間, 增加如下引數:
+-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | innodb_undo_directory | . | | innodb_undo_logs | 128 | | innodb_undo_tablespaces | 1 | +-------------------------+-------+
這樣,在install的時候,就會在data目錄下增加undo資料檔案,來組成undo獨立表空間,但檔案變大之後的空間回收還是成為問題。
MySQL 5.7的版本上
InnoDB undo在支援獨立表空間的基礎上,支援表空間的truncate功能,增加了如下引數:
mysql> show global variables like '%undo%'; +--------------------------+------------+ | Variable_name | Value | +--------------------------+------------+ | innodb_max_undo_log_size | 1073741824 | | innodb_undo_directory | ./ | | innodb_undo_log_truncate | OFF | | innodb_undo_logs | 128 | | innodb_undo_tablespaces | 3 | +--------------------------+------------+ mysql> show global variables like '%truncate%'; +--------------------------------------+-------+ | Variable_name | Value | +--------------------------------------+-------+ | innodb_purge_rseg_truncate_frequency | 128 | | innodb_undo_log_truncate | OFF | +--------------------------------------+-------+
InnoDB的purge執行緒,會根據innodb_undo_log_truncate開關的設定,和innodb_max_undo_log_size設定的檔案大小閾值,以及truncate的頻率來進行空間回收和rollback segment的重新初始化。
innodb_undo_log_truncate引數設定為1,即開啟線上回收(收縮)undo log日誌檔案,支援動態設定。
innodb_undo_tablespaces引數必須大於或等於2,即回收(收縮)一個undo log日誌檔案時,要保證另一個undo log是可用的。
innodb_undo_logs: undo回滾段的數量, 至少大於等於35,預設128。
innodb_max_undo_log_size:當超過這個閥值(預設是1G),會觸發truncate回收(收縮)動作,truncate後空間縮小到10M。
innodb_purge_rseg_truncate_frequency:控制回收(收縮)undo log的頻率。undo log空間在它的回滾段沒有得到釋放之前不會收縮,
想要增加釋放回滾區間的頻率,就得降低innodb_purge_rseg_truncate_frequency設定值。
innodb_undo_directory:undo檔案存放的位置;
接下來我們詳細看下5.7的InnoDB undo的管理:
2. undo表空間建立
設定innodb_undo_tablespaces的個數, 在mysql install的時候,建立指定數量的表空間。
InnoDB支援128個undo logs,這裡特別說明下,從5.7開始,innodb_rollback_segments的名字改成了innodb_undo_logs,但表示的都是回滾段的個數。
從5.7.2開始,其中32個undo logs為臨時表的事務分配的,因為這部分undo不記錄redo,不需要recovery,另外從33-128一共96個是redo-enabled undo。
rollback segment的分配如下:
Slot-0: reserved for system-tablespace.
Slot-1....Slot-N: reserved for temp-tablespace.
Slot-N+1....Slot-127: reserved for system/undo-tablespace. */
其中如果是臨時表的事務,需要分配兩個undo logs,其中一個是non-redo undo logs;這部分用於臨時表資料的回滾。
另外一個是redo-enabled undo log,是為臨時表的元資料準備的,需要recovery。
而且, 其中32個rollback segment建立在臨時表空間中,並且臨時表空間中的回滾段在每次server start的時候,需要重建。
每一個rollback segment可以分配1024個slot,也就是可以支援96*1024個併發的事務同時, 但如果是臨時表的事務,需要佔用兩個slot。
InnoDB undo的空間管理簡圖如下:
注核心結構說明:
1. rseg slot
rseg slot一共128個,儲存在ibdata系統表空間中,其位置在:
/*!< the start of the array of rollback segment specification slots */
#define TRX_SYS_RSEGS (8 + FSEG_HEADER_SIZE)
每一個slot儲存著rollback segment header的位置。包括space_id + page_no,佔用8個bytes。其巨集定義:
/* Rollback segment specification slot offsets */
/*-------------------------------------------------------------*/
#define TRX_SYS_RSEG_SPACE 0 /* space where the segment
header is placed; starting with
MySQL/InnoDB 5.1.7, this is
UNIV_UNDEFINED if the slot is unused */
#define TRX_SYS_RSEG_PAGE_NO 4 /* page number where the segment
header is placed; this is FIL_NULL
if the slot is unused */
/* Size of a rollback segment specification slot */
#define TRX_SYS_RSEG_SLOT_SIZE 8
2. rseg header
rseg header在undo表空間中,每一個rseg包括1024個undo segment slot,每一個slot儲存著undo segment header的位置,包括page_no,暫用4個bytes,因為undo segment不會跨表空間,所以space_id就沒有必要了。
其巨集定義如下:
/* Undo log segment slot in a rollback segment header */
/*-------------------------------------------------------------*/
#define TRX_RSEG_SLOT_PAGE_NO 0 /* Page number of the header page of
an undo log segment */
/*-------------------------------------------------------------*/
/* Slot size */
#define TRX_RSEG_SLOT_SIZE 4
3. undo segment header
undo segment header page即段內的第一個undo page,其中包括四個比較重要的結構:
undo segment header | 進行段內空間的管理 |
undo page header | page內空間的管理,page的型別:FIL_PAGE_UNDO_LOG |
undo header | 包含undo record的連結串列,以便安裝事務的反順序,進行回滾 |
undo record | 剩下的就是undo記錄了。 |
3. undo段的分配
undo段的分配比較簡單,其過程如下:
首先是rollback segment的分配:
trx->rsegs.m_redo.rseg = trx_assign_rseg_low(
srv_undo_logs, srv_undo_tablespaces,
TRX_RSEG_TYPE_REDO);
- 使用round-robin的方式來分配rollback segment
- 如果有單獨設定undo表空間,就不使用system表空間中的undo segment
- 如果設定的是truncate的就不分配
- 一旦分配了,就設定trx_ref_count,不允許truncate。
具體程式碼參考:
/******************************************************************//**
Get next redo rollback segment. (Segment are assigned in round-robin fashion).
@return assigned rollback segment instance */
static
trx_rseg_t*
get_next_redo_rseg(
/*===============*/
ulong max_undo_logs, /*!< in: maximum number of UNDO logs to use */
ulint n_tablespaces) /*!< in: number of rollback tablespaces */
其次是undo segment的建立:
從rollback segment裡邊選擇一個free的slot,如果沒有,就會報錯,通常是併發的事務太多。
錯誤日誌如下:
ib::warn() << "Cannot find a free slot for an undo log. Do"
" you have too many active transactions running"
" concurrently?";
如果有free,就建立一個undo的segment。
核心的程式碼如下:
/***************************************************************//**
Creates a new undo log segment in file.
@return DB_SUCCESS if page creation OK possible error codes are:
DB_TOO_MANY_CONCURRENT_TRXS DB_OUT_OF_FILE_SPACE */
static
dberr_t
trx_undo_seg_create(
/*================*/
trx_rseg_t* rseg __attribute__((unused)),/*!< in: rollback segment */
trx_rsegf_t* rseg_hdr,/*!< in: rollback segment header, page
x-latched */
ulint type, /*!< in: type of the segment: TRX_UNDO_INSERT or
TRX_UNDO_UPDATE */
ulint* id, /*!< out: slot index within rseg header */
page_t** undo_page,
/*!< out: segment header page x-latched, NULL
if there was an error */
mtr_t* mtr) /*!< in: mtr */
/* fputs(type == TRX_UNDO_INSERT
? "Creating insert undo log segment\n"
: "Creating update undo log segment\n", stderr); */
slot_no = trx_rsegf_undo_find_free(rseg_hdr, mtr);
if (slot_no == ULINT_UNDEFINED) {
ib::warn() << "Cannot find a free slot for an undo log. Do"
" you have too many active transactions running"
" concurrently?";
return(DB_TOO_MANY_CONCURRENT_TRXS);
}
4. undo的truncate
undo的truncate主要由下面兩個引數控制:innodb_purge_rseg_truncate_frequency,innodb_undo_log_truncate。
1. innodb_undo_log_truncate是開關引數。
2. innodb_purge_rseg_truncate_frequency預設128,表示purge undo輪詢128次後,進行一次undo的truncate。
當設定innodb_undo_log_truncate=ON的時候, undo表空間的檔案大小,如果超過了innodb_max_undo_log_size, 就會被truncate到初始大小,但有一個前提,就是表空間中的undo不再被使用。
其主要步驟如下:
1. 超過大小了之後,會被mark truncation,一次會選擇一個
2. 選擇的undo不能再分配新給新的事務
3. purge執行緒清理不再需要的rollback segment
4. 等所有的回滾段都釋放了後,truncate操作,使其成為install db時的初始狀態。
預設情況下, 是purge觸發128次之後,進行一次rollback segment的free操作,然後如果全部free就進行一個truncate。
但mark的操作需要幾個依賴條件需要滿足:
1. 系統至少得有兩個undo表空間,防止一個offline後,至少另外一個還能工作
2. 除了ibdata裡的segment,還至少有兩個segment可用
3. undo表空間的大小確實超過了設定的閾值
其核心程式碼參考:
/** Iterate over all the UNDO tablespaces and check if any of the UNDO
tablespace qualifies for TRUNCATE (size > threshold).
@param[in,out] undo_trunc undo truncate tracker */
static
void
trx_purge_mark_undo_for_truncate(
undo::Truncate* undo_trunc)
因為,只要你設定了truncate = on,MySQL就儘可能的幫你去truncate所有的undo表空間,所以它會迴圈的把undo表空間加入到mark列表中。
最後,迴圈所有的undo段,如果所屬的表空間是marked truncate,就把這個rseg標誌位不可分配,加入到trunc佇列中,在purge的時候,進行free rollback segment。
注意:
如果是線上庫,要注意影響,因為當一個undo tablespace在進行truncate的時候,不再承擔undo的分配。只能由剩下的undo 表空間的rollback segment接受事務undo空間請求。