天天看點

MySQL · 引擎特性 · InnoDB redo log

一 序

       本文根據《MYSQL運維内參》第11章INNODB日志管理機制整理,本篇書上側重于原理說明日志的生成、格式、工作原理、刷盤機制等。限于篇幅,崩潰恢複的需要單獨整理。InnoDB 有兩塊非常重要的日志,一個是undo log,另外一個是redo log,前者用來保證事務的原子性以及InnoDB的MVCC,後者用來保證事務的持久性。解釋下redolog與事務持久性:redo log用來資料異常恢複和資料庫重新開機時頁資料同步恢複,redo log是建立在在mini transaction基礎上。資料庫在執行事務時,通過minitransaction産生redo log來保證事務的持久性。

     和大多數關系型資料庫一樣,InnoDB記錄了對資料檔案的實體更改,并保證總是日志先行,也就是所謂的WAL(log write ahead),即在持久化資料檔案前,保證之前的redo日志已經寫到磁盤。

      LSN(log sequence number) 用于記錄日志序号,它是一個不斷遞增的 unsigned long long 類型整數。在 InnoDB 的日志系統中,LSN 無處不在,它既用于表示修改髒頁時的日志序号,也用于記錄checkpoint,通過LSN,可以具體的定位到其在redo log檔案中的位置。LSN在引擎中定義的是一個dulint_t類型值。源碼在innobase/include/log0log.h

/* Type used for all log sequence number storage and arithmetics */
typedef	ib_uint64_t		lsn_t;
           

  LSN的含義是儲存引擎向重做日志系統寫入的日志量(位元組數),這個日志量包括寫入的日志位元組 + LOG_BLOCK_HDR_SIZE + LOG_BLOCK_TRL_SIZE;代碼參見函數log_write_low(源碼在innobase/log/log0log.cc)。在調用日志寫入函數LSN就一直随着寫入的日志長度增加,它是一個集實體意義與邏輯意義與一體的。     

      為了管理髒頁,在 Buffer Pool 的每個instance上都維持了一個flush list,flush list 上的 page 按照修改這些 page 的LSN号進行排序。是以定期做redo checkpoint點時,選擇的 LSN 總是所有 bp instance 的 flush list 上最老的那個page(擁有最小的LSN)。由于采用WAL的政策,每次事務送出時需要持久化 redo log 才能保證事務不丢。而延遲刷髒頁則起到了合并多次修改的效果,避免頻繁寫資料檔案造成的性能問題。

       innodb在5.6設計了3個層子產品,即redo log buffer(redo log的日志記憶體緩沖區)、group files( redo log檔案組)和archive files( 歸檔日志檔案)。由于 InnoDB 日志組的特性已經被廢棄(redo日志寫多份),歸檔日志(archive log)特性也在5.7被徹底移除,是以不做展開。

好吧,雖然上面都是常識,但是對于初次接觸的還是要了解下。因為百度出來的文章不一定是基于那個版本。

二 InnoDB 日志檔案

   InnoDB的redo log可以通過參數innodb_log_files_in_group配置成多個檔案,另外一個參數innodb_log_file_size表示每個檔案的大小。是以總的redo log大小為innodb_log_files_in_group * innodb_log_file_size。

   Redo log檔案以ib_logfile[number]命名,日志目錄可以通過參數innodb_log_group_home_dir控制。Redo log 以順序的方式寫入檔案檔案,寫滿時則回溯到第一個檔案,進行覆寫寫

MySQL · 引擎特性 · InnoDB redo log

      在InnoDB内部,邏輯上ib_logfile被當成了一個檔案,對應同一個space id。由于是使用512位元組block對齊寫入檔案,可以很友善的根據全局維護的LSN号計算出要寫入到哪一個檔案以及對應的偏移量。

       Redo log檔案是循環寫入的,在覆寫寫之前,總是要保證對應的髒頁已經刷到了磁盤。在非常大的負載下,Redo log可能産生的速度非常快,導緻頻繁的刷髒操作,進而導緻性能下降,通常在未做checkpoint的日志超過檔案總大小的76%之後,InnoDB 認為這可能是個不安全的點,會強制的preflush髒頁,導緻大量使用者線程stall住。

    除了redo log檔案外,InnoDB還有其他的日志檔案,例如為了保證truncate操作而産生的中間日志檔案,包括 truncate innodb 表以及truncate undo log tablespace,都會産生一個中間檔案,來辨別這些操作是成功還是失敗,如果truncate沒有完成,則需要在 crash recovery 時進行重做。

Log Block

   innodb在日志系統裡面定義了log block的概念,其實log block就是一個512位元組的資料塊,這個資料塊包括塊頭、日志資訊和塊的checksum.其結構如下:

MySQL · 引擎特性 · InnoDB redo log

      如果每個頁中産生的重做日志數量大于512位元組,那麼需要分割多個重做日志塊進行存儲,此外,由于重做日志快的大小和磁盤扇區大小一樣,都是512位元組,是以重做日志的寫入可以保證原子性,重做日志快除了日志本身之外,還由日志塊頭(log block header)及日志塊尾(log block tailer)兩部分組成。重做日志頭一共占用12位元組,重做日志尾占用4位元組。故每個重做日志塊實際可以存儲的大小為496位元組(512-12-4)。

      Block no 的最高位是描述block是否flush磁盤的辨別位.通過lsn可以blockno,具體的計算過程是lsn是多少個512的整數倍,也就是no = lsn / 512 + 1;為什麼要加1呢,因為所處no的塊算成clac_lsn一定會小于傳入的lsn.是以要+1。其實就是block的數組索引值。 

    LOG_BLOCK_HDR_DATA_LEN   2個位元組,表示目前頁面存儲日志的長度,通常是滿的0X200,因為日志在相連的塊上市連續存儲的,中間不會有空閑空間,如果不滿表示日志已經掃描完成(crash recovery的工作 )。

  LOG_BLOCK_FIRST_REC_GROUP 占用2個位元組,表示log block中第一個日志所在的偏移量。如果該值的大小和LOG_BLOCK_HDR_DATA_LEN相同,則表示目前log block不包含新的日志。如事務T1的重做日志1占用762位元組,事務T2的重做日志占用100位元組,。由于每個log block實際隻能儲存492位元組,是以其在log buffer的情況應該如圖所示

MySQL · 引擎特性 · InnoDB redo log

LOG_BLOCK_CHECKPOINT_NO  占用4位元組,表示該log block最後被寫入時的檢查點第4位元組的值。

源碼在innobase/include/log0log.h

/* Offsets of a log block header */
#define	LOG_BLOCK_HDR_NO	0	/* block number which must be > 0 and
					is allowed to wrap around at 2G; the
					highest bit is set to 1 if this is the
					first log block in a log flush write
					segment */
#define LOG_BLOCK_FLUSH_BIT_MASK 0x80000000UL
					/* mask used to get the highest bit in
					the preceding field */
#define	LOG_BLOCK_HDR_DATA_LEN	4	/* number of bytes of log written to
					this block */
#define	LOG_BLOCK_FIRST_REC_GROUP 6	/* offset of the first start of an
					mtr log record group in this log block,
					0 if none; if the value is the same
					as LOG_BLOCK_HDR_DATA_LEN, it means
					that the first rec group has not yet
					been catenated to this log block, but
					if it will, it will start at this
					offset; an archive recovery can
					start parsing the log records starting
					from this offset in this log block,
					if value not 0 */
#define LOG_BLOCK_CHECKPOINT_NO	8	/* 4 lower bytes of the value of
					log_sys->next_checkpoint_no when the
					log block was last written to: if the
					block has not yet been written full,
					this value is only updated before a
					log buffer flush */
#define LOG_BLOCK_HDR_SIZE	12	/* size of the log block header in
					bytes */

/* Offsets of a log block trailer from the end of the block */
#define	LOG_BLOCK_CHECKSUM	4	/* 4 byte checksum of the log block
					contents; in InnoDB versions
					< 3.23.52 this did not contain the
					checksum but the same value as
					.._HDR_NO */
#define	LOG_BLOCK_TRL_SIZE	4	/* trailer size in bytes */
           

checkpoint

      checkpoint是日志的檢查點,其作用就是在資料庫異常後,redo log是從這個點的資訊擷取到LSN,并對檢查點以後的日志和PAGE做重做恢複。那麼檢查點是怎麼生成的呢?當日志緩沖區寫入的日志LSN距離上一次生成檢查點的LSN達到一定差距的時候,就會開始建立檢查點,建立檢查點首先會将記憶體中的表的髒資料寫入到硬碟,讓後再将redo log buffer中小于本次檢查點的LSN的日志也寫入硬碟。在log_group_t中的checkpoint_buf,以下是它對應字段的解釋:

 LOG_CHECKPOINT_NO            checkpoint序号,

 LOG_CHECKPOINT_LSN           本次checkpoint起始的LSN

 LOG_CHECKPOINT_OFFSET        本次checkpoint相對group file的起始偏移量

 LOG_CHECKPOINT_LOG_BUF_SIZE  redo log buffer的大小,預設2M

 LOG_CHECKPOINT_ARCHIVED_LSN  目前日志歸檔的LSN

 LOG_CHECKPOINT_GROUP_ARRAY   每個log group歸檔時的檔案序号和偏移量,是一個數組

      關于checkpoint的作用:因為日志檔案不是無限大的。前面也提到過寫入日志是循環的,如果日志頭尾相遇日志就不完整了。是以引入了checkpoint機制。檢查點的保證資料庫完整的主體思想,是想讓日志失效,因為日志的作用就是讓buffer pool的page盡量少的刷磁盤,盡可能長的把頁面緩存起來,提高通路速度。buffer pool的page是最新的,隻是不一定寫入磁盤中。如果日志檔案不夠用了,隻需要将buffer pool的部分page寫入磁盤,其對應的日志就無效了。因為日志作用就是保證page沒有刷入時但資料庫挂了的情況下保證資料庫的完整性的。

三 MTR

      Innodb存儲引擎中的一個很重要的用來保證持久性的機制就是mini事務,在源碼中用mtr(Mini-transaction)來表示,本書把它稱做“實體事務”,這樣叫是相對邏輯事務而言的,對于邏輯事務,做熟悉資料庫的人都很清楚,它是資料庫差別于檔案系統的最重要特性之一,它具有四個特性ACID,用來保證資料庫的完整性——要麼都做修改,要麼什麼都沒有做。實體事務從名字來看,是實體的,因為在innodb存儲引擎中,隻要是涉及到檔案修改,檔案讀取等實體操作的,都離不開這個實體事務,可以說實體事務是記憶體與檔案之間的一個橋梁。

MySQL · 引擎特性 · InnoDB redo log

    前面已經介紹過innodb的頁面緩沖區系統,已經知道在通路一個檔案頁面的時候,系統都會将要通路的頁面載入到頁面緩沖區中,然後才可以通路這個頁面,此時可以讀取或者更新這個頁面,在再次将更新寫入到檔案中之前,這個頁面都會處理緩沖區中。在這個過程中,有一個機制一直扮演着很重要的角色,那就是實體事務。

3.1 結構

    實體事務既然被稱為事務,那它同樣有事務的開始與送出,在innodb中,實體事務的開始其實就是對實體事務的結構體mtr_t的初始化,其中包括下面一些成員:源碼在/innobase/include/mtr0mtr.h

/** Mini-transaction handle and buffer */
struct mtr_t {

	/** State variables of the mtr */
	struct Impl {

		/** memo stack for locks etc. */
		mtr_buf_t	m_memo;

		/** mini-transaction log */
		mtr_buf_t	m_log;

		/** true if mtr has made at least one buffer pool page dirty */
		bool		m_made_dirty;

		/** true if inside ibuf changes */
		bool		m_inside_ibuf;

		/** true if the mini-transaction modified buffer pool pages */
		bool		m_modifications;

		/** Count of how many page initial log records have been
		written to the mtr log */
		ib_uint32_t	m_n_log_recs;

		/** specifies which operations should be logged; default
		value MTR_LOG_ALL */
		mtr_log_t	m_log_mode;
#ifdef UNIV_DEBUG
		/** Persistent user tablespace associated with the
		mini-transaction, or 0 (TRX_SYS_SPACE) if none yet */
		ulint		m_user_space_id;
#endif /* UNIV_DEBUG */
		/** User tablespace that is being modified by the
		mini-transaction */
		fil_space_t*	m_user_space;
		/** Undo tablespace that is being modified by the
		mini-transaction */
		fil_space_t*	m_undo_space;
		/** System tablespace if it is being modified by the
		mini-transaction */
		fil_space_t*	m_sys_space;

		/** State of the transaction */
		mtr_state_t	m_state;

		/** Flush Observer */
		FlushObserver*	m_flush_observer;

#ifdef UNIV_DEBUG
		/** For checking corruption. */
		ulint		m_magic_n;
#endif /* UNIV_DEBUG */

		/** Owning mini-transaction */
		mtr_t*		m_mtr;
	};
           
變量名 描述
mtr_buf_t m_memo 用于存儲該mtr持有的鎖類型
mtr_buf_t m_log 存儲redo log記錄
bool m_made_dirty 是否産生了至少一個髒頁
bool m_inside_ibuf 是否在操作change buffer
bool m_modifications 是否修改了buffer pool page
ib_uint32_t m_n_log_recs 該mtr log記錄個數
mtr_log_t m_log_mode Mtr的工作模式,包括四種: MTR_LOG_ALL:預設模式,記錄所有會修改磁盤資料的操作;MTR_LOG_NONE:不記錄redo,髒頁也不放到flush list上;MTR_LOG_NO_REDO:不記錄redo,但髒頁放到flush list上;MTR_LOG_SHORT_INSERTS:插入記錄操作REDO,在将記錄從一個page拷貝到另外一個建立的page時用到,此時忽略寫索引資訊到redo log中。(參閱函數page_cur_insert_rec_write_log)
fil_space_t* m_user_space 目前mtr修改的使用者表空間
fil_space_t* m_undo_space 目前mtr修改的undo表空間
fil_space_t* m_sys_space 目前mtr修改的系統表空間
mtr_state_t m_state 包含四種狀态: MTR_STATE_INIT、MTR_STATE_COMMITTING、 MTR_STATE_COMMITTED

在修改或讀一個資料檔案中的資料時,一般是通過mtr來控制對對應page或者索引樹的加鎖,在5.7中,有以下幾種鎖類型(

mtr_memo_type_t

):

變量名 描述
MTR_MEMO_PAGE_S_FIX 用于PAGE上的S鎖
MTR_MEMO_PAGE_X_FIX 用于PAGE上的X鎖
MTR_MEMO_PAGE_SX_FIX 用于PAGE上的SX鎖,以上鎖通過mtr_memo_push 儲存到mtr中
MTR_MEMO_BUF_FIX PAGE上未加讀寫鎖,僅做buf fix
MTR_MEMO_S_LOCK S鎖,通常用于索引鎖
MTR_MEMO_X_LOCK X鎖,通常用于索引鎖
MTR_MEMO_SX_LOCK SX鎖,通常用于索引鎖,以上3個鎖,通過mtr_s/x/sx_lock加鎖,通過mtr_memo_release釋放鎖

注意:mysql5.7 與5.6不太一樣。另外書上這篇是側重原理,代碼講的比較少。感興趣可以自己去看,隻羅列下函數的入口。

3.2 原理

首先在系統将一個頁面載入到緩沖區的時候,需要新開始一個(mtr_start)或者一個已經開始的實體事務,載入時需要指定頁面的擷取方式,比如是用來讀取的還是用來修改的,這樣會影響實體事務對這個頁面的上鎖情況,如果用來修改,則上X鎖,否則上S鎖(當然還可以指定不上鎖)。在确定了擷取方式、這個頁面的表空間ID及頁面号之後,就可以通過函數buf_page_get來擷取指定頁面了,當找到相應頁面後,實體事務就要對它上指定的鎖,此時需要對這個頁面的上鎖情況進行檢查,一個頁面的上鎖情況是在結構體buf_block_struct中的lock中展現的,此時如果這個頁面還沒有上鎖,則這個實體事務直接對其上鎖,否則還需要考慮兩個鎖的相容性,隻有兩個鎖都是共享鎖(S)的情況下才是可以上鎖成功的,否則需要等待。當上鎖成功後,實體事務會将這個頁面的記憶體結構存儲到上面提到的memo動态數組中。然後這個實體事務就可以通路這個頁面了。

實體事務對頁面的通路包括兩種操作,一種是讀,另一種是寫,讀就是簡單讀取其指定頁面内偏移及長度的資料;寫則是指定從某一偏移開始寫入指定長度的新資料,同時如果這個實體事務是寫日志的(MTR_LOG_ALL),此時還需要對剛才的寫操作記下日志,這裡的日志就邏輯事務中提到的REDO日志。寫下相應的日志之後,同樣将其存儲到上面的log動态數組中,同時要将上面結構體中的n_log_recs自增,維護這個實體事務的日志計數值。

實體事務的讀寫過程主要就是上面介紹的内容,其最重要的是它的送出過程。實體事務的送出是通過mtr_commit來實作的,實體事務的送出主要是将所有這個實體事務産生的日志寫入到innodb的日志系統的日志緩沖區中,然後等待srv_master_thread線程定時将日志系統的日志緩沖區中的日志資料刷到日志檔案中,這一部分會單獨在其它章節點講述。

上面已經講過,實體事務和邏輯事務一樣,也是可以保證資料庫操作的完整性的,一般說來,一個操作必須要在一個實體事務中完成,也就是指要麼這個操作已經完成,要麼什麼也沒有做,否則有可能造成資料不完整的問題,因為在資料庫系統做REDO操作時是以一個實體事務為機關做的,如果一個實體事務的日志是不完整的,則它對應的所有日志都不會重做。那麼如何辨識一個實體事務是否完整呢?這個問題是在實體事務送出時用了個很巧妙的方法保證了,在送出前,如果發現這個實體事務有日志,則在日志最後再寫一些特殊的日志,這些特殊的日志就是一個實體事務結束的标志,那麼送出時一起将這些特殊的日志寫入,在重做時如果目前這一批日志資訊最後面存在這個标志,則說明這些日志是完整的,否則就是不完整的,則不會重做。

實體事務送出時還有一項很重要的工作就是處理上面結構體中動态數組memo中的内容,現在都已經知道這個數組中存儲的是這個實體事務所有通路過的頁面,并且都已經上了鎖,那麼在它送出時,如果發現這些頁面中有已經被修改過的,則這些頁面就成為了髒頁,這些髒頁需要被加入到innodb的buffer緩沖區中的更新連結清單中(講BUFFER時已經講過),當然如果已經在更新鍊中,則直接跳過(不能重複加入),svr_master_thread線程會定時檢查這個連結清單,将一定數目的髒頁刷到磁盤中,加入之後還需要将這個頁面上的鎖釋放掉,表示這個頁面已經處理完成;如果頁面沒有被修改,或者隻是用來讀取資料的,則隻需要直接将其共享鎖(S鎖)釋放掉即可。

上面的内容就是實體事務的一個完整的講述,因為它是比較底層的一個子產品,牽扯的東西比較多,這裡重點講述了實體事務的意義、操作原理、與BUFFER系統的關聯、日志的産生等内容。

3.3 源碼執行個體

     mtr log生成

InnoDB的redo log都是通過mtr産生的,先寫到mtr的cache中,然後再送出到公共buffer中,本小節以INSERT一條記錄對page産生的修改為例,闡述一個mtr的典型生命周期。

入口函數在row_ins_clust_index_entry_low,innobase/row/row0ins.cc

row_ins_clust_index_entry_low(
/*==========================*/
	ulint		flags,	/*!< in: undo logging and locking flags */
	ulint		mode,	/*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE,
				depending on whether we wish optimistic or
				pessimistic descent down the index tree */
	dict_index_t*	index,	/*!< in: clustered index */
	ulint		n_uniq,	/*!< in: 0 or index->n_uniq */
	dtuple_t*	entry,	/*!< in/out: index entry to insert */
	ulint		n_ext,	/*!< in: number of externally stored columns */
	que_thr_t*	thr,	/*!< in: query thread */
	bool		dup_chk_only)
				/*!< in: if true, just do duplicate check
				and return. don't execute actual insert. */
{
	btr_pcur_t	pcur;
	btr_cur_t*	cursor;
	dberr_t		err		= DB_SUCCESS;
	big_rec_t*	big_rec		= NULL;
	mtr_t		mtr;
	mem_heap_t*	offsets_heap	= NULL;
	ulint           offsets_[REC_OFFS_NORMAL_SIZE];
	ulint*          offsets         = offsets_;
	rec_offs_init(offsets_);

	DBUG_ENTER("row_ins_clust_index_entry_low");

	ut_ad(dict_index_is_clust(index));
	ut_ad(!dict_index_is_unique(index)
	      || n_uniq == dict_index_get_n_unique(index));
	ut_ad(!n_uniq || n_uniq == dict_index_get_n_unique(index));
	ut_ad(!thr_get_trx(thr)->in_rollback);

	mtr_start(&mtr);
	mtr.set_named_space(index->space);
           

mtr_start(&mtr);

mtr.set_named_space(index->space);

就是開啟mtr。

mtr_start主要包括:

  • 初始化mtr的各個狀态變量
  • 預設模式為MTR_LOG_ALL,表示記錄所有的資料變更
  • mtr狀态設定為ACTIVE狀态(MTR_STATE_ACTIVE)
  • 為鎖管理對象和日志管理對象初始化記憶體(mtr_buf_t),初始化對象連結清單

插入資料

在插入資料過程中,包含大量的redo寫cache邏輯,例如更新二級索引頁的max trx id、寫undo log産生的redo(嵌套另外一個mtr)、修改資料頁産生的日志。這裡我們隻讨論修改資料頁産生的日志,進入函數page_cur_insert_rec_write_log:

源碼在innobase/page/page0cur.cc

/***********************************************************//**
Writes the log record of a record insert on a page. */
static
void
page_cur_insert_rec_write_log(
/*==========================*/
	rec_t*		insert_rec,	/*!< in: inserted physical record */
	ulint		rec_size,	/*!< in: insert_rec size */
	rec_t*		cursor_rec,	/*!< in: record the
					cursor is pointing to */
	dict_index_t*	index,		/*!< in: record descriptor */
	mtr_t*		mtr)		/*!< in: mini-transaction handle */
{
	ulint	cur_rec_size;
	ulint	extra_size;
	ulint	cur_extra_size;
	const byte* ins_ptr;
	const byte* log_end;
	ulint	i;

	/* Avoid REDO logging to save on costly IO because
	temporary tables are not recovered during crash recovery. */
	if (dict_table_is_temporary(index->table)) {
		byte*	log_ptr = mlog_open(mtr, 0);
		if (log_ptr == NULL) {
			return;
		}
		mlog_close(mtr, log_ptr);
		log_ptr = NULL;
	}

	ut_a(rec_size < UNIV_PAGE_SIZE);
	ut_ad(mtr->is_named_space(index->space));
	ut_ad(page_align(insert_rec) == page_align(cursor_rec));
	ut_ad(!page_rec_is_comp(insert_rec)
	      == !dict_table_is_comp(index->table));

	{
		mem_heap_t*	heap		= NULL;
		ulint		cur_offs_[REC_OFFS_NORMAL_SIZE];
		ulint		ins_offs_[REC_OFFS_NORMAL_SIZE];

		ulint*		cur_offs;
		ulint*		ins_offs;

		rec_offs_init(cur_offs_);
		rec_offs_init(ins_offs_);

		cur_offs = rec_get_offsets(cursor_rec, index, cur_offs_,
					   ULINT_UNDEFINED, &heap);
		ins_offs = rec_get_offsets(insert_rec, index, ins_offs_,
					   ULINT_UNDEFINED, &heap);

		extra_size = rec_offs_extra_size(ins_offs);
		cur_extra_size = rec_offs_extra_size(cur_offs);
		ut_ad(rec_size == rec_offs_size(ins_offs));
		cur_rec_size = rec_offs_size(cur_offs);

		if (UNIV_LIKELY_NULL(heap)) {
			mem_heap_free(heap);
		}
	}

	ins_ptr = insert_rec - extra_size;

	i = 0;

	if (cur_extra_size == extra_size) {
		ulint		min_rec_size = ut_min(cur_rec_size, rec_size);

		const byte*	cur_ptr = cursor_rec - cur_extra_size;

		/* Find out the first byte in insert_rec which differs from
		cursor_rec; skip the bytes in the record info */

		do {
			if (*ins_ptr == *cur_ptr) {
				i++;
				ins_ptr++;
				cur_ptr++;
			} else if ((i < extra_size)
				   && (i >= extra_size
				       - page_rec_get_base_extra_size
				       (insert_rec))) {
				i = extra_size;
				ins_ptr = insert_rec;
				cur_ptr = cursor_rec;
			} else {
				break;
			}
		} while (i < min_rec_size);
	}

	byte*	log_ptr;

	if (mtr_get_log_mode(mtr) != MTR_LOG_SHORT_INSERTS) {

		if (page_rec_is_comp(insert_rec)) {
			log_ptr = mlog_open_and_write_index(
				mtr, insert_rec, index, MLOG_COMP_REC_INSERT,
				2 + 5 + 1 + 5 + 5 + MLOG_BUF_MARGIN);
			if (UNIV_UNLIKELY(!log_ptr)) {
				/* Logging in mtr is switched off
				during crash recovery: in that case
				mlog_open returns NULL */
				return;
			}
		} else {
			log_ptr = mlog_open(mtr, 11
					    + 2 + 5 + 1 + 5 + 5
					    + MLOG_BUF_MARGIN);
			if (UNIV_UNLIKELY(!log_ptr)) {
				/* Logging in mtr is switched off
				during crash recovery: in that case
				mlog_open returns NULL */
				return;
			}

			log_ptr = mlog_write_initial_log_record_fast(
				insert_rec, MLOG_REC_INSERT, log_ptr, mtr);
		}

		log_end = &log_ptr[2 + 5 + 1 + 5 + 5 + MLOG_BUF_MARGIN];
		/* Write the cursor rec offset as a 2-byte ulint */
		mach_write_to_2(log_ptr, page_offset(cursor_rec));
		log_ptr += 2;
	} else {
		log_ptr = mlog_open(mtr, 5 + 1 + 5 + 5 + MLOG_BUF_MARGIN);
		if (!log_ptr) {
			/* Logging in mtr is switched off during crash
			recovery: in that case mlog_open returns NULL */
			return;
		}
		log_end = &log_ptr[5 + 1 + 5 + 5 + MLOG_BUF_MARGIN];
	}

	if (page_rec_is_comp(insert_rec)) {
		if (UNIV_UNLIKELY
		    (rec_get_info_and_status_bits(insert_rec, TRUE)
		     != rec_get_info_and_status_bits(cursor_rec, TRUE))) {

			goto need_extra_info;
		}
	} else {
		if (UNIV_UNLIKELY
		    (rec_get_info_and_status_bits(insert_rec, FALSE)
		     != rec_get_info_and_status_bits(cursor_rec, FALSE))) {

			goto need_extra_info;
		}
	}

	if (extra_size != cur_extra_size || rec_size != cur_rec_size) {
need_extra_info:
		/* Write the record end segment length
		and the extra info storage flag */
		log_ptr += mach_write_compressed(log_ptr,
						 2 * (rec_size - i) + 1);

		/* Write the info bits */
		mach_write_to_1(log_ptr,
				rec_get_info_and_status_bits(
					insert_rec,
					page_rec_is_comp(insert_rec)));
		log_ptr++;

		/* Write the record origin offset */
		log_ptr += mach_write_compressed(log_ptr, extra_size);

		/* Write the mismatch index */
		log_ptr += mach_write_compressed(log_ptr, i);

		ut_a(i < UNIV_PAGE_SIZE);
		ut_a(extra_size < UNIV_PAGE_SIZE);
	} else {
		/* Write the record end segment length
		and the extra info storage flag */
		log_ptr += mach_write_compressed(log_ptr, 2 * (rec_size - i));
	}

	/* Write to the log the inserted index record end segment which
	differs from the cursor record */

	rec_size -= i;

	if (log_ptr + rec_size <= log_end) {
		memcpy(log_ptr, ins_ptr, rec_size);
		mlog_close(mtr, log_ptr + rec_size);
	} else {
		mlog_close(mtr, log_ptr);
		ut_a(rec_size < UNIV_PAGE_SIZE);
		mlog_catenate_string(mtr, ins_ptr, rec_size);
	}
}
           

關鍵函數: mlog_open_and_write_index 記錄索引相關資訊

mach_write_to_2(log_ptr, page_offset(cursor_rec));   寫入記錄在page上的偏移量,占兩個位元組

mach_write_compressed 處理rec_size 、extra_size;

memcpy(log_ptr, ins_ptr, rec_size); 将插入的記錄拷貝到redo檔案

mlog_close(mtr, log_ptr + rec_size); 關閉mlog

通過上述流程,我們寫入了一個類型為MLOG_COMP_REC_INSERT的日志記錄。由于特定類型的記錄都基于約定的格式,在崩潰恢複時也可以基于這樣的約定解析出日志。

更多的redo log記錄類型參見enum mlog_id_t  源碼在innobase/include/mtr0types.h

在這個過程中産生的redo log都記錄在mtr.m_impl.m_log中,隻有顯式送出mtr時,才會寫到公共buffer中。

送出mtr log

當送出一個mini transaction時,需要将對資料的更改記錄送出到公共buffer中,并将對應的髒頁加到flush list上。

入口函數為mtr_t::commit(),當修改産生髒頁或者日志記錄時,調用mtr_t::Command::execute 源碼在innobase/mtr/mtr0mtr.cc

/** Write the redo log record, add dirty pages to the flush list and release
the resources. */
void
mtr_t::Command::execute()
{
	ut_ad(m_impl->m_log_mode != MTR_LOG_NONE);

	if (const ulint len = prepare_write()) {
		finish_write(len);
	}

	if (m_impl->m_made_dirty) {
		log_flush_order_mutex_enter();
	}

	/* It is now safe to release the log mutex because the
	flush_order mutex will ensure that we are the first one
	to insert into the flush list. */
	log_mutex_exit();

	m_impl->m_mtr->m_commit_lsn = m_end_lsn;

	release_blocks();

	if (m_impl->m_made_dirty) {
		log_flush_order_mutex_exit();
	}

	release_latches();

	release_resources();
}
           

四 Redo 寫盤操作

有幾種場景可能會觸發redo log寫檔案:

  1. Redo log buffer空間不足時
  2. 事務送出
  3. 背景線程
  4. 做checkpoint
  5. 執行個體shutdown時
  6. binlog切換時

我們所熟悉的參數

innodb_flush_log_at_trx_commit

 作用于事務送出時,這也是最常見的場景:

  • 當設定該值為1時,每次事務送出都要做一次fsync,這是最安全的配置,即使當機也不會丢失事務;
  • 當設定為2時,則在事務送出時隻做write操作,隻保證寫到系統的page cache,是以執行個體crash不會丢失事務,但當機則可能丢失事務;
  • 當設定為0時,事務送出不會觸發redo寫操作,而是留給背景線程每秒一次的刷盤操作,是以執行個體crash将最多丢失1秒鐘内的事務。

下圖表示了不同配置值的持久化程度:

MySQL · 引擎特性 · InnoDB redo log

顯然對性能的影響是随着持久化程度的增加而增加的。通常我們建議在日常場景将該值設定為1,但在系統高峰期臨時修改成2以應對大負載。

由于各個事務可以交叉的将事務日志拷貝到log buffer中,因而一次事務送出觸發的寫redo到檔案,可能隐式的幫别的線程“順便”也寫了redo log,進而達到group commit的效果。

寫redo log的入口函數為log_write_up_to,源碼在innobase/log/log0log.cc

淘寶的說mysql 5.7版本相對5.6 優化了可讀性,同時消除一次多餘的擷取

log_sys->mutex。沒去看過源碼。

log write ahead

上面已經介紹過,InnoDB以512位元組一個block的方式對齊寫入ib_logfile檔案,但現代檔案系統一般以4096位元組為一個block機關。如果即将寫入的日志檔案塊不在OS Cache時,就需要将對應的4096位元組的block讀入記憶體,修改其中的512位元組,然後再把該block寫回磁盤。

為了解決這個問題,MySQL 5.7引入了一個新參數:innodb_log_write_ahead_size。當目前寫入檔案的偏移量不能整除該值時,則補0,多寫一部分資料。這樣當寫入的資料是以磁盤block size對齊時,就可以直接write磁盤,而無需read-modify-write這三步了。

注意innodb_log_write_ahead_size的預設值為8196,你可能需要根據你的系統配置來修改該值,以獲得更好的效果。

五日志格式

log_sys對象

log_sys

是InnoDB日志系統的中樞及核心對象,控制着日志的拷貝、寫入、checkpoint等核心功能。它同時也是大寫入負載場景下的熱點子產品,是連接配接InnoDB日志檔案及log buffer的樞紐,對應結構體為

log_t

。 源碼在innobase/include/log0log.h

/** Redo log buffer */
struct log_t{
	char		pad1[CACHE_LINE_SIZE];
					/*!< Padding to prevent other memory
					update hotspots from residing on the
					same memory cache line */
	lsn_t		lsn;		/*!< log sequence number */
	ulint		buf_free;	/*!< first free offset within the log
					buffer in use */
#ifndef UNIV_HOTBACKUP
	char		pad2[CACHE_LINE_SIZE];/*!< Padding */
	LogSysMutex	mutex;		/*!< mutex protecting the log */
	LogSysMutex	write_mutex;	/*!< mutex protecting writing to log
					file and accessing to log_group_t */
	char		pad3[CACHE_LINE_SIZE];/*!< Padding */
	FlushOrderMutex	log_flush_order_mutex;/*!< mutex to serialize access to
					the flush list when we are putting
					dirty blocks in the list. The idea
					behind this mutex is to be able
					to release log_sys->mutex during
					mtr_commit and still ensure that
					insertions in the flush_list happen
					in the LSN order. */
#endif /* !UNIV_HOTBACKUP */
	byte*		buf_ptr;	/*!< unaligned log buffer, which should
					be of double of buf_size */
	byte*		buf;		/*!< log buffer currently in use;
					this could point to either the first
					half of the aligned(buf_ptr) or the
					second half in turns, so that log
					write/flush to disk don't block
					concurrent mtrs which will write
					log to this buffer */
	bool		first_in_use;	/*!< true if buf points to the first
					half of the aligned(buf_ptr), false
					if the second half */
	ulint		buf_size;	/*!< log buffer size of each in bytes */
	ulint		max_buf_free;	/*!< recommended maximum value of
					buf_free for the buffer in use, after
					which the buffer is flushed */
	bool		check_flush_or_checkpoint;
					/*!< this is set when there may
					be need to flush the log buffer, or
					preflush buffer pool pages, or make
					a checkpoint; this MUST be TRUE when
					lsn - last_checkpoint_lsn >
					max_checkpoint_age; this flag is
					peeked at by log_free_check(), which
					does not reserve the log mutex */
	UT_LIST_BASE_NODE_T(log_group_t)
			log_groups;	/*!< log groups */

#ifndef UNIV_HOTBACKUP
	/** The fields involved in the log buffer flush @{ */

	ulint		buf_next_to_write;/*!< first offset in the log buffer
					where the byte content may not exist
					written to file, e.g., the start
					offset of a log record catenated
					later; this is advanced when a flush
					operation is completed to all the log
					groups */
	volatile bool	is_extending;	/*!< this is set to true during extend
					the log buffer size */
	lsn_t		write_lsn;	/*!< last written lsn */
	lsn_t		current_flush_lsn;/*!< end lsn for the current running
					write + flush operation */
	lsn_t		flushed_to_disk_lsn;
					/*!< how far we have written the log
					AND flushed to disk */
	ulint		n_pending_flushes;/*!< number of currently
					pending flushes; incrementing is
					protected by the log mutex;
					may be decremented between
					resetting and setting flush_event */
	os_event_t	flush_event;	/*!< this event is in the reset state
					when a flush is running; a thread
					should wait for this without
					owning the log mutex, but NOTE that
					to set this event, the
					thread MUST own the log mutex! */
	ulint		n_log_ios;	/*!< number of log i/os initiated thus
					far */
	ulint		n_log_ios_old;	/*!< number of log i/o's at the
					previous printout */
	time_t		last_printout_time;/*!< when log_print was last time
					called */
	/* @} */

	/** Fields involved in checkpoints @{ */
	lsn_t		log_group_capacity; /*!< capacity of the log group; if
					the checkpoint age exceeds this, it is
					a serious error because it is possible
					we will then overwrite log and spoil
					crash recovery */
	lsn_t		max_modified_age_async;
					/*!< when this recommended
					value for lsn -
					buf_pool_get_oldest_modification()
					is exceeded, we start an
					asynchronous preflush of pool pages */
	lsn_t		max_modified_age_sync;
					/*!< when this recommended
					value for lsn -
					buf_pool_get_oldest_modification()
					is exceeded, we start a
					synchronous preflush of pool pages */
	lsn_t		max_checkpoint_age_async;
					/*!< when this checkpoint age
					is exceeded we start an
					asynchronous writing of a new
					checkpoint */
	lsn_t		max_checkpoint_age;
					/*!< this is the maximum allowed value
					for lsn - last_checkpoint_lsn when a
					new query step is started */
	ib_uint64_t	next_checkpoint_no;
					/*!< next checkpoint number */
	lsn_t		last_checkpoint_lsn;
					/*!< latest checkpoint lsn */
	lsn_t		next_checkpoint_lsn;
					/*!< next checkpoint lsn */
	mtr_buf_t*	append_on_checkpoint;
					/*!< extra redo log records to write
					during a checkpoint, or NULL if none.
					The pointer is protected by
					log_sys->mutex, and the data must
					remain constant as long as this
					pointer is not NULL. */
	ulint		n_pending_checkpoint_writes;
					/*!< number of currently pending
					checkpoint writes */
	rw_lock_t	checkpoint_lock;/*!< this latch is x-locked when a
					checkpoint write is running; a thread
					should wait for this without owning
					the log mutex */
#endif /* !UNIV_HOTBACKUP */
	byte*		checkpoint_buf_ptr;/* unaligned checkpoint header */
	byte*		checkpoint_buf;	/*!< checkpoint header is read to this
					buffer */
	/* @} */
};
           

其中與 redo log 檔案相關的成員變量包括:

變量名 描述
log_groups 日志組,目前版本僅支援一組日志,對應類型為 

log_group_t

 ,包含了目前日志組的檔案個數、每個檔案的大小、space id等資訊
lsn_t log_group_capacity 表示目前日志檔案的總容量,值為:(Redo log檔案總大小 - redo 檔案個數 * LOG_FILE_HDR_SIZE) * 0.9,LOG_FILE_HDR_SIZE 為 4*512 位元組
lsn_t max_modified_age_async 異步 preflush dirty page 點
lsn_t max_modified_age_sync 同步 preflush dirty page 點
lsn_t max_checkpoint_age_async 異步 checkpoint 點
lsn_t max_checkpoint_age 同步 checkpoint 點

上述幾個sync/async點的計算方式可以參閱函數

log_calc_max_ages

,以如下執行個體配置為例:

innodb_log_files_in_group=4
innodb_log_file_size=4G
總檔案大小: 17179869184
           

各個成員變量值及占總檔案大小的比例:

log_sys->log_group_capacity = 15461874893 (90%)

log_sys->max_modified_age_async = 12175607164 (71%)

log_sys->max_modified_age_sync = 13045293390 (76%)

log_sys->max_checkpoint_age_async = 13480136503 (78%)

log_sys->max_checkpoint_age = 13914979615 (81%)
           

通常的:

當目前未刷髒的最老lsn和目前lsn的距離超過

max_modified_age_async

(71%)時,且開啟了選項

innodb_adaptive_flushing

時,page cleaner線程會去嘗試做更多的dirty page flush工作,避免髒頁堆積。 當目前未刷髒的最老lsn和目前Lsn的距離超過

max_modified_age_sync

(76%)時,使用者線程需要去做同步刷髒,這是一個性能下降的臨界點,會極大的影響整體吞吐量和響應時間。 當上次checkpoint的lsn和目前lsn超過

max_checkpoint_age

(81%),使用者線程需要同步地做一次checkpoint,需要等待checkpoint寫入完成。 當上次checkpoint的lsn和目前lsn的距離超過

max_checkpoint_age_async

(78%)但小于

max_checkpoint_age

(81%)時,使用者線程做一次異步checkpoint(背景異步線程執行CHECKPOINT資訊寫入操作),無需等待checkpoint完成。

log_group_t

結構體主要成員如下表所示:

變量名 描述
ulint n_files Ib_logfile的檔案個數
lsn_t file_size 檔案大小
ulint space_id Redo log 的space id, 固定大小,值為SRV_LOG_SPACE_FIRST_ID
ulint state LOG_GROUP_OK 或者 LOG_GROUP_CORRUPTED
lsn_t lsn 該group内寫到的lsn
lsn_t lsn_offset 上述lsn對應的檔案偏移量
byte** file_header_bufs Buffer區域,用于設定日志檔案頭資訊,并寫入ib logfile。當切換到新的ib_logfile時,更新該檔案的起始lsn,寫入頭部。 頭部資訊還包含: LOG_GROUP_ID, LOG_FILE_START_LSN(目前檔案起始lsn)、LOG_FILE_WAS_CREATED_BY_HOT_BACKUP(函數log_group_file_header_flush)
lsn_t scanned_lsn 用于崩潰恢複時輔助記錄掃描到的lsn号
byte* checkpoint_buf Checkpoint緩沖區,用于向日志檔案寫入checkpoint資訊(下文較長的描述)

與redo log 記憶體緩沖區相關的成員變量包括:

變量名 描述
ulint buf_free Log buffer中目前空閑可寫的位置
byte* buf Log buffer起始位置指針
ulint buf_size Log buffer 大小,受參數innodb_log_buffer_size控制,但可能會自動extend
ulint max_buf_free 值為log_sys->buf_size / LOG_BUF_FLUSH_RATIO - LOG_BUF_FLUSH_MARGIN, 其中: LOG_BUF_FLUSH_RATIO=2, LOG_BUF_FLUSH_MARGIN=(4 * 512 + 4* page_size) ,page_size預設為16k,當buf_free超過該值時,可能觸發使用者線程去寫redo;在事務拷redo 到buffer後,也會判斷該值,如果超過buf_free,設定log_sys->check_flush_or_checkpoint為true
ulint buf_next_to_write Log buffer偏移量,下次寫入redo檔案的起始位置,即本次寫入的結束位置
volatile bool is_extending Log buffer是否正在進行擴充 (防止過大的redo log entry無法寫入buffer), 實際上,當寫入的redo log長度超過buf_size/2時,就會去調用函數log_buffer_extend,一旦擴充Buffer,就不會在縮減回去了!
ulint write_end_offset 本次寫入的結束位置偏移量(從邏輯來看有點多餘,直接用log_sys->buf_free就行了)

和Checkpoint檢查點相關的成員變量:

變量名 描述
ib_uint64_t next_checkpoint_no 每完成一次checkpoint遞增該值
lsn_t last_checkpoint_lsn 最近一次checkpoint時的lsn,每完成一次checkpoint,将next_checkpoint_lsn的值賦給last_checkpoint_lsn
lsn_t next_checkpoint_lsn 下次checkpoint的lsn(本次發起的checkpoint的lsn)
mtr_buf_t* append_on_checkpoint 5.7新增,在做DDL時(例如增删列),會先将包含MLOG_FILE_RENAME2日志記錄的buf挂到這個變量上。 在DDL完成後,再清理掉。(log_append_on_checkpoint),主要是防止DDL期間crash産生的資料詞典不一緻。 該變量在如下commit加上: a5ecc38f44abb66aa2024c70e37d1f4aa4c8ace9
ulint n_pending_checkpoint_writes 大于0時,表示有一個checkpoint寫入操作正在進行。使用者發起checkpoint時,遞增該值。背景線程完成checkpoint寫入後,遞減該值(log_io_complete)
rw_lock_t checkpoint_lock checkpoint鎖,每次寫checkpoint資訊時需要加x鎖。由異步io線程釋放該x鎖
byte* checkpoint_buf Checkpoint資訊緩沖區,每次checkpoint前,先寫該buf,再将buf刷到磁盤

其他狀态變量

變量名 描述
bool check_flush_or_checkpoint 當該變量被設定時,使用者線程可能需要去檢查釋放要刷log buffer、或是做preflush、checkpoint等以防止Redo 空間不足
lsn_t write_lsn 最近一次完成寫入到檔案的LSN
lsn_t current_flush_lsn 目前正在fsync到的LSN
lsn_t flushed_to_disk_lsn 最近一次完成fsync到檔案的LSN
ulint n_pending_flushes 表示pending的redo fsync,這個值最大為1
os_event_t flush_event 若目前有正在進行的fsync,并且本次請求也是fsync操作,則需要等待上次fsync操作完成

log_sys與日志檔案和日志緩沖區的關系可用下圖來表示:

MySQL · 引擎特性 · InnoDB redo log

總結:

這塊知識點較多,還得結合bufferpoll,page,插入記錄等綜合來了解。

http://mysql.taobao.org/monthly/2015/05/01/