1. 程式人生 > >undo空間管理(可以收縮undo log回滾日誌物理檔案空間)

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的空間管理簡圖如下:

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);
  1. 使用round-robin的方式來分配rollback segment
  2. 如果有單獨設定undo表空間,就不使用system表空間中的undo segment
  3. 如果設定的是truncate的就不分配
  4. 一旦分配了,就設定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空間請求。