1. 程式人生 > >Mysql 三大特性詳解

Mysql 三大特性詳解

追蹤 方式 還需要 性能 arc 直接插入 解決 auto 依次

Mysql Innodb後臺線程

工作方式

首先Mysql進程模型是單進程多線程的。所以我們通過ps查找mysqld進程是只有一個。

體系架構

InnoDB存儲引擎的架構如下圖所以,是由多個內存塊組成的內存池,同時又多個後臺線程進行工作,文件是存儲磁盤上的數據。 技術分享圖片

後臺線程

上面看到一共有四種後臺線程,每種線程都在不停地做自己的工作,他們的分工如下:
  • Master Thread: 是最核心的線程,主要負責將緩沖池中的數據異步刷新的磁盤,保證數據的一致性,包括臟頁的刷新、合並插入緩沖(INSERT BUFFER),UNDO頁的回收等。下面幾個線程其實是為了分擔主線程的壓力而在最新的版本中添加的。
  • IO Thread: InnoDB使用大量的異步IO來處理請求。IO Thread的主要工作就是負責IO請求的回調(call back)處理。異步IO可以分為4個,分別是:write, read, insert buffer 和 log IO thread。
  • Purge Thread: undo log是用來保證事務的,當一個事務正常提交後,這個undo log可能就不再使用了。purge thread就是用來清除這部分log已經分配的undo頁的。
  • Page Cleaner Thread: 主要是把臟頁的刷新從主線程中拿到單獨的線程,減輕主線程的壓力,減少用戶查詢線程的阻塞,提高整體性能。

Mysql Innodb內存結構

技術分享圖片 具體來看緩沖池中緩存的數據頁類型有:
  • 索引頁: 緩存數據表索引
  • 數據頁: 緩存數據頁,占緩沖池的絕大部分
  • undo頁: undo頁是保存事務,為回滾做準備的。
  • 插入緩沖(Insert buffer): 上面提到的插入數據時要先插入到緩存池中。
  • 自適應哈希索引(adaptive hash index): 除了B+ Tree索引外,在緩沖池還會維護一個哈希索引,以便在緩沖池中快速找到數據頁。
  • InnoDB存儲的鎖信息(lock info):
  • 數據字典(data dictionary):
  • 內存中除了緩沖池外外還有:
  • 重做日誌緩沖redo log: 為了避免數據丟失的問題,當前數據庫系統普遍采用了write ahead log策略,既當事務提交時先寫重做日誌,再修改寫頁。當由於發生宕機而導致數據丟失時,可以通過重做日誌進行恢復。InnoDB先將重做日誌放到這個緩沖區,然後按照一定的頻率更新到重做日誌文件中。重做日誌一般在下列情況下會刷新內容到文件:
1.Master Thread每一秒將重做日誌緩沖刷新到重做日誌文件 2.每個事務提交時會將重做日誌緩沖刷新到重做日誌文件 3.當重做日誌緩沖池剩余空間小於1/2時,重做日誌緩沖刷新到重做日誌文件
  • 額外內存池: InnoDB存儲引擎中,對內存的管理師通過一種稱為內存堆的方式進行的,在對一些數據結構本身的內存進行分配時,需要從額外的內存池中進行申請,當該區域的內存不夠時,會從緩沖池中進行申請。
緩沖池是一個很大的內存區域,InnoDB是如何對這些內存進行管理的呢。答案就使用LRU list。
LRU(Latest Recent Used, 最近最少使用)算法默認的是最近使用的放到表頭,最早使用的放到表尾,依次排列。當有LRU填滿時有新的進來就把最早的淘汰掉。InnoDB則是在這個基礎上進行了修改:

最近使用的不放到表頭,而是根據配置放到一定比例處,這個地方叫做midpoint, midpoint之前的成為new列表,之後的成為old列表。淘汰的同樣是表尾的頁。
為了保證new列表的不經常使用時能夠淘汰,設置了一個超時時間:innodb_old_blocks_time,當數據在midpoint(我理解應該是在old列表中,不然這個點的頁就一個,變化也比較頻繁)的時間超過找個時間時就會被提升到表頭,new列表的表尾頁則被置換到old列表中。
這麽做的原因主要是因為常見的索引或數據的掃描操作會連續讀取大量的頁,甚至是全表掃描。如果采用原來的LRU算法就會更新全部的緩沖池,其他查詢需要的熱點數據就會被沖走,導致更多的磁盤讀取操作,降低數據庫的性能。
LRU是用來管理已經讀取的頁,當數據庫啟動時LRU是空列表,既只有表頭,沒有內容。這時頁都放在Free List中。當需要有數據讀寫時要進行需要獲取分頁,這時要從Free List中刪除分頁,然後添加到LRU list中。到一定時間Free List中的分頁就會被分配完畢,這時候就正常使用上面的LRU策略。
LRU列表中的頁被修改後,稱該頁為臟頁(dirty page),既緩沖池中的數據和磁盤上的數據產生了不一致,這時臟頁會被加入到一個Flush 列表中(註意,同時存在兩個列表中)。然後根據刷新的機制定時的刷新到磁盤中。

三大特性之一插入緩沖

1.1聚簇索引與非聚簇索引的區別

聚集索引的葉子節點存儲的是數據,而且是按照物理順序存儲的;非聚集索引葉子節點是地址(也就是聚集索引鍵地址),是按照邏輯順序存儲的(以上言論是從網上了解到的,但是本書P194特別指出,聚集索引也不是按照物理地址連續的,而是邏輯上連續的)。

1.2高並發後的insert會發生什麽?

  1. mysql> create table t ( id int auto_increment,
  2. name varchar(30),primary key (id),key(name));
  3. Query OK, 0 rows affected (0.21 sec)
我們知道,主鍵是行唯一的標識符,在應用程序中行記錄的插入順序是按照主鍵遞增的順序進行插入的。因此,插入聚集索引一般是順序的,不需要磁盤的隨機讀取,速度會很快,但是我們看到上一個表還有一個叫做name的索引字段,這樣的情況下產生了一個非聚集的並且不是唯一的索引。在進行插入操作時,數據頁的存放還是按主鍵id的執行順序存放,但是對於非聚集索引,葉子節點的插入不再是順序的了。這時就需要離散地訪問非聚集索引頁,插入性能在這裏變低了。然而這並不是這個name字段上索引的錯誤,是因為B+樹的特性決定了非聚集索引插入的離散性。

1.3插入緩沖的實現

技術分享圖片 在innodb的1.0x版本開始,引入了change buffer,可以把它看成insert buffer的升級版,innodb可以對DML操作-INSERT/DELETE/UPDATE都進行緩沖。 1.將一個輔助索引插入到頁(space,offset)
2.檢查這個頁是否在緩沖池中
在:直接插入
不在:繼續 3.緩存進入insert buffer
4.構造一個search key
5.查詢insert buffer樹
6.生成邏輯記錄並插入樹中

1.3.1insert buffer內部實現原理

在mysql4.1版本之後,insert buffer是通過一全局唯一的一個B+樹進行管理所有表的輔助索引。而這顆樹存放在共享表空間中,格式為ibdata1,所以如果試圖通過獨立表空間idb文件恢復表中數據的時候,往往會導致CHECK TABLE失敗,這是因為表的輔助索引中的數據可能還在INSERT BUFFER中,也就是共享表空間中,所以通過ibd文件進行恢復後,還需要進行REPAIR TABLE 操作來重建表上所有的輔助索引。 insert buffer的b+樹的非葉子節點存放的是查詢的search key(鍵值),其構造如圖: 技術分享圖片
  • space為表空間id
  • marker用來兼容老版本
  • offset表示頁所在偏移量

葉子節點會比非葉子節點多倆數據,一個是metadata,一個是secondary index record。

技術分享圖片

  • metadata 記錄的每一列的類型,長度
  • secondary index record 記錄的具體值
為了保證每個輔助索引頁Merge Insert Buffer的B+樹必須成功,還需要有一個特殊的頁用來標記每個輔助索引頁(space,page_no)的可用空間。這個頁的類型為Insert Buffer Bitmap。它會標記16385個輔助索引頁,每個輔助索引頁會在其中占用4bit的位置來記錄信息,具體信息如下: 技術分享圖片 Merge Insert Buffer的操作可能發生在以下幾種情況下:
  • 輔助索引頁被讀取到緩沖池時;
  • Insert Buffer Bitmap頁追蹤到該輔助索引頁已無可用空間時;
  • Master Thread。
第一種情況為當輔助索引頁被讀取到緩沖池中時,例如這在執行正常的SELECT查詢操作,這時需要檢查Insert Buffer Bitmap頁,然後確認該輔助索引頁是否有記錄存放於Insert Buffer B+樹中。若有,則將Insert Buffer B+樹中該頁的記錄插入到該輔助索引頁中。可以看到對該頁多次的記錄操作通過一次操作合並到了原有的輔助索引頁中,因此性能會有大幅提高。
Insert Buffer Bitmap頁用來追蹤每個輔助索引頁的可用空間,並至少有1/32頁的空間。若插入輔助索引記錄時檢測到插入記錄後可用空間會小於1/32頁,則會強制進行一個合並操作,即強制讀取輔助索引頁,將Insert Buffer B+樹中該頁的記錄及待插入的記錄插入到輔助索引頁中。這就是上述所說的第二種情況。
還有一種情況,之前在分析Master Thread時曾講到,在Master Thread線程中每秒或每10秒會進行一次Merge Insert Buffer的操作,不同之處在於每次進行merge操作的頁的數量不同。

1.4緩沖的限制條件

插入緩沖的啟用需要滿足一下兩個條件:
1)索引是輔助索引(secondary index)
2)索引不適合唯一的 原因是因為插入緩沖本身就是為了解決二級索引離散插入的問題,所以建立一個緩沖區將部分離散的索引數據合並,使用一次大的IO操作統一刷到磁盤,如果索引是唯一的,那這麽做將失去意義,而且每次還需要去詢問數據頁是否已經存在,還會增加額外的IO操作。

1.5插入緩沖性能影響

任何一項技術在帶來好處的同時,必然也帶來壞處。插入緩沖主要帶來如下兩個壞處:
1)可能導致數據庫宕機後實例恢復時間變長。如果應用程序執行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出現宕機,這時就有大量內存中的插入緩沖區數據沒有合並至索引頁中,導致實例恢復時間會很長。
2)在寫密集的情況下,插入緩沖會占用過多的緩沖池內存,默認情況下最大可以占用1/2,這在實際應用中會帶來一定的問題。

1.6觀察

  1. show engine innodb status\g;
  2. -------------------------------------
  3. INSERT BUFFER AND ADAPTIVE HASH INDEX
  4. -------------------------------------
  5. Ibuf: size 1, free list len 0, seg size 2, 2 merges
  6. merged operations:
  7. insert 0, delete mark 0, delete 0
  8. discarded operations:
  9. insert 0, delete mark 0, delete 0
  10. Hash table size 34679, node heap has 0 buffer(s)
  11. Hash table size 34679, node heap has 0 buffer(s)
  12. Hash table size 34679, node heap has 1 buffer(s)
  13. Hash table size 34679, node heap has 0 buffer(s)
  14. Hash table size 34679, node heap has 1 buffer(s)
  15. Hash table size 34679, node heap has 0 buffer(s)
  16. Hash table size 34679, node heap has 0 buffer(s)
  17. Hash table size 34679, node heap has 0 buffer(s)
  18. 0.00 hash searches/s, 0.00 non-hash searches/s

從上面可以看到其中有一部分叫做INSERT BUFFER AND ADAPTIVE HASH INDEX,其中的seg size指的就是當前insert buffer的大小,具體計算方式為seg_size*16KB =32KB,free list len表示空閑列表的長度,size表示已經合並記錄頁的數量。

三大特性之二double write

2.1、基本概念

2.1.1頁斷裂(partial write)

所謂頁斷裂是數據庫宕機時(OS重啟,或主機掉電重啟),數據庫頁面只有部分寫入磁盤,導致頁面出現不一致的情況。那麽為什麽會不一樣呢?因為數據庫,OS和磁盤讀寫的基本單位是塊,也可以稱之為(page size)block size。我們知道數據庫的塊一般為8K,16K;而OS的塊則一般為4K;IO塊則更小,linux內核要求IO block size<=OS block size。磁盤IO除了IO block size,還有一個概念是扇區(IO sector),扇區是磁盤物理操作的基本單位,而IO 塊是磁盤操作的邏輯單位,一個IO塊對應一個或多個扇區,扇區大小一般為512個字節。所以各個塊大小的關系可以梳理如下: DB block > OS block >= IO block > 磁盤 sector,而且他們之間保持了整數倍的關系。所以說當數據庫突然宕機,就會造成部分DB block的數據實際上並未寫入到磁盤的sector中,出現了頁斷裂的情況,進而導致數據不一致的現象。

2.1.2數據庫日誌的三種格式

數據庫系統實現日誌主要有三種格式,邏輯日誌(logical logging),物理日誌(physical logging),物理邏輯日誌(physiological logging),邏輯日誌,記錄一個個邏輯操作,不涉及物理存儲位置信息,比如mysql的binlog;物理日誌,則是記錄一個個具體物理位置的操作,比如在2號表空間,1號文件,48頁的233這個offset地方寫入了8個字節的數據,通過(group_id,file_id,page_no,offset)4元組,就能唯一確定數據存儲在磁盤的物理位置;物理邏輯日誌是物理日誌和邏輯日誌的混合,如果一個數據庫操作(DDL,DML,DCL)產生的日誌跨越了多個頁面,那麽會產生多個物理頁面的日誌,但對於每個物理頁面日誌,裏面記錄則是邏輯信息。這裏我舉一個簡單的INSERT操作來說明幾種日誌形式。
比如innodb表T(c1,c2, key key_c1(c1)),插入記錄row1(1,’abc’)
邏輯日誌:
<insert OP, T, 1,’abc’>
邏輯物理日誌:因為表T含有索引key_c1, 一次插入操作至少涉及兩次B樹操作,二次B樹必然涉及至少兩個物理頁面,因此至少有兩條日誌
  1. <insert OP, page_no_1, log_body>
  2. <insert OP, page_no_2, log_body>
物理理日誌:由於一次INSERT操作,物理上來說要修改頁頭信息(如,頁內的記錄數要加1),要修改相鄰記錄裏的鏈表指針,要修改Slot屬性等,因此對應邏輯物理日誌的每一條日誌,都會有N條物理日誌產生。
  1. < group_id,file_id,page_no,offset1, value1>
  2. < group_id,file_id,page_no,offset2, value2>
  3. ……
  4. < group_id,file_id,page_no,offsetN, valueN>
因此對於上述一個INSERT操作,會產生一條邏輯日誌,二條邏輯物理日誌,2*N條物理日誌。從上面簡單的分析可以看出,邏輯日誌的日誌量最小,而物理日誌的日誌量最大;物理日誌是純物理的;而邏輯物理日誌則頁間物理,頁內邏輯,所謂physical-to-a-page, logical-within-a-page。


2.2 數據一致性

2.2.1頁斷裂和數據一致性

前面我們分析了異常重啟導致頁斷裂的原因,而頁斷裂就意味著數據庫頁面不完整,那麽數據庫頁面不完整就意味著數據庫不一致。我們知道,數據庫異常重啟時,自身有異常恢復機制,主流數據庫基本原理類似:第一階段重做redo日誌,恢復數據頁和undo頁到異常crash時的狀態;第二階段,根據undo頁的內容,回滾沒有提交事務的修改。通過兩個階段保證了數據庫的一致性。對於mysql而言,在第一階段,若出現頁斷裂問題,則無法通過重做redo日誌恢復,進而導致恢復中斷,數據庫不一致。這裏大家可能會有疑問,數據庫的redo不是記錄了所有的變更,並且是物理的嗎?理論上來說,無論頁面是否斷裂,從上一個檢查點對應的redo位置開始,一直重做redo,頁面自然能恢復到正常狀態。對嗎?

2.2.2redo格式與數據一致性

回到“發生頁斷裂後,是否會影響數據庫一致性”的問題,發生頁斷裂後,對於利用純物理日誌實現redo的數據庫不受影響,因為每一條redo日誌完全不依賴物理頁的狀態,並且是冪等的(執行一次與N次,結果是一樣的),而邏輯物理日誌則不行,比如修改頁頭信息,頁內記錄數加1,slot信息修改等都依賴於頁面處於一個一致狀態,否則就無法正確重做redo。而mysql正是采用這種日誌類型,另外要說明一點,redo日誌的頁大小一般設計為512個字節,因此redo日誌頁本身不會發生頁斷裂。所以發生頁面斷裂時,異常恢復就會出現問題,需要借助於double write技術來輔助處理。

2.3 doubleWrite的實現

在InnoDB將BP中的Dirty Page刷(flush)到磁盤上時,首先會將(memcpy函數)Page刷到InnoDB tablespace的一個區域中,我們稱該區域為Double write Buffer(大小為2MB,每次寫入1MB,128個頁,每個頁16k,其中120個頁為後臺線程的批量刷Dirty Page,還有8個也是為了前臺起的sigle Page Flash線程,用戶可以主動請求,並且能迅速的提供空余的空間)。在向Double write Buffer寫入成功後,第二步、再將數據分別刷到一個共享空間和真正應該存在的位置。具體的流程如下圖所示: 技術分享圖片

2.4doubleWrite的保護機制

下面來看下在不同的寫入階段,操作系統crash後,double write帶來的保護機制: 技術分享圖片 階段一:copy過程中,操作系統crash,重啟之後,臟頁未刷到磁盤,但更早的數據並沒有發生損壞,重新寫入即可階段二:write到共享表空間過程中,操作系統crash,重啟之後,臟頁未刷到磁盤,但更早的數據並沒有發生損壞,重新寫入即可階段三:write到獨立表空間過程中,操作系統crash,重啟之後,發現:(1)數據文件內的頁損壞:頭尾checksum值不匹配(即出現了partial page write的問題)。從共享表空間中的doublewrite segment內恢復該頁的一個副本到數據文件,再應用redo log;(2)若頁自身的checksum匹配,但與doublewrite segment中對應頁的checksum不匹配,則統一可以通過apply redo log來恢復。)階段X:recover過程中,操作系統crash,重啟之後,innodb面對的情況同階段三一樣(數據文件損壞,但共享表空間內有副本),再次應用redo log即可。

2.5doubleWrite對性能的影響

系統需要將數據寫兩份,一般認為,Double Write是會降低系統性能的。peter猜測可能會有5-10%的性能損失,但是因為實現了數據的一致,是值得的。Mark Callaghan認為這應該是存儲層面應該解決的問題,放在數據庫層面無疑是犧牲了很多性能的。事實上,Double Write對性能影響並沒有你想象(寫兩遍性能應該降低了50%吧?)的那麽大。在BP中一次性往往會有很多的Dirty Page同時被flush,Double Write則把這些寫操作,由隨機寫轉化為了順序寫。而在Double Write的第二個階段,因為Double Write Buffer中積累了很多Dirty Page,所以向真正的數據文件中寫數據的時候,可能有很多寫操作可以合並,這樣有可能會降低Fsync的調用次數。基於上面的原因,Double Write並沒有想象的那麽糟。最後發現打開和關閉Double Write對效率的影響並不大。

三大特性之三自適應哈希索引

哈希索引只有Memory, NDB兩種引擎支持,Memory引擎默認支持哈希索引,如果多個hash值相同,出現哈希碰撞,那麽索引以鏈表方式存儲。但是,Memory引擎表只對能夠適合機器的內存切實有限的數據集。要使InnoDB或MyISAM支持哈希索引,可以通過偽哈希索引來實現,但是innodb還實現了一種叫做自適應哈希索引來達到目的。 InnoDB存儲引擎會監控對表上各索引頁的查詢。如果觀察到建立哈希索引可以帶來速度提升,則建立哈希索引,稱之為自適應哈希索引(Adaptive Hash Index, AHI)。AHI是通過緩沖池的B+樹頁構造而來,因此建立的速度很快,而且不需要對整張表構建哈希索引。InnoDB存儲引擎會自動根據訪問的頻率和模式來自動地為某些熱點頁建立哈希索引。

3.1、狀態監控

  1. mysql> show engine innodb status\G
  2. ……
  3. Hash table size 34673, node heap has 0 buffer(s)
  4. 0.00 hash searches/s, 0.00 non-hash searches/s

1、34673:字節為單位,占用內存空間總量   2、通過hash searches、non-hash searches計算自適應hash索引帶來的收益以及付出,確定是否開啟自適應hash索引

3.2、限制

  1、只能用於等值比較,例如=, <=>,in
  2、無法用於排序
  3、有沖突可能
  4、MySQL自動管理,人為無法幹預。

3、自適應哈希索引的控制

  由於innodb不支持hash索引,但是在某些情況下hash索引的效率很高,於是出現了adaptive hash index功能,但是通過上面的狀態監控,可以計算其收益以及付出,控制該功能開啟與否。 技術分享圖片

Mysql 三大特性詳解