最近看了《MySQL技術(shù)內(nèi)幕InnoDB存儲(chǔ)引擎》一書,受益良多,對(duì)Mysql InnoDB有了進(jìn)一步的了解。于是根據(jù)自己理解和搜集的資料,寫了一系列深入InnoDB的文章,其中不少知識(shí)來著《MySQL技術(shù)內(nèi)幕InnoDB存儲(chǔ)引擎》以及《MYSQL內(nèi)核:INNODB存儲(chǔ)引擎》,感謝這兩本書的作者,也向各位推薦這兩本書。
這一系列文章都是從MySql的使用和設(shè)計(jì)的出發(fā),不會(huì)涉及源碼,希望可以幫助大家更深入理解InnoDB的設(shè)計(jì)和實(shí)現(xiàn)。
概念區(qū)分
先明確兩個(gè)概念,雖然平常我們并不需要嚴(yán)格區(qū)分他們
數(shù)據(jù)庫:存儲(chǔ)數(shù)據(jù)的物理操作系統(tǒng)文件或其他形式文件的集合
數(shù)據(jù)庫實(shí)例:MySql數(shù)據(jù)庫實(shí)例由后臺(tái)線程以及一個(gè)共享內(nèi)存區(qū)組成,負(fù)責(zé)操作數(shù)據(jù)庫文件。
MySql是一個(gè)單進(jìn)程多線程架構(gòu)的數(shù)據(jù)庫,MySql數(shù)據(jù)庫實(shí)例在系統(tǒng)上表現(xiàn)就是一個(gè)進(jìn)程。
MySql架構(gòu)
MySql架構(gòu)圖如下
MySql中有以下組件
- 連接池組件 -- Connection Pool
- 管理服務(wù)和工具組件 -- Management Services & utilities
- SQL接口組件 -- SQL Interface
- 查詢分析器組件 -- Praser
- 優(yōu)化器組件 -- Optimizer
- 緩沖組件 -- Caches & Buffers
- 插件式表存儲(chǔ)引擎 -- Pluggable Storage Enginess
- 物理文件 -- Files & Logs
MySQL區(qū)別于其他數(shù)據(jù)庫的一個(gè)最重要的特點(diǎn)是插件式存儲(chǔ)引擎。它是基于表的,而不是數(shù)據(jù)庫。MySql常用存儲(chǔ)引擎如下:
MyISAM存儲(chǔ)引擎
- 不支持事務(wù)
- 緩沖池只緩存索引文件,不緩沖數(shù)據(jù)文件
- 由MYD和MYI文件組成,MYD用來存放數(shù)據(jù)文件,MYI用來存放索引文件,
InnoDB存儲(chǔ)引擎
獨(dú)立表空間,支持MVCC,行鎖設(shè)計(jì),提供一致性非鎖定讀
支持外鍵,插入緩沖,二次寫,自適應(yīng)哈希索引,預(yù)讀
使用聚集的方式存儲(chǔ)數(shù)據(jù),每張表的存儲(chǔ)都是按主鍵順序存放。
此外還有NDB,Memory,Archive,F(xiàn)ederated,Marai等存儲(chǔ)引擎。
InnoDB架構(gòu)
InnoDB架構(gòu)圖如下
以下主要從內(nèi)存和線程的角度分析InnoDB的架構(gòu)。
內(nèi)存池
主要工作:
- 維護(hù)所有進(jìn)程/線程需要使用的多個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu)
- 緩存磁盤上的數(shù)據(jù),方便快速地讀取,同時(shí)對(duì)磁盤文件數(shù)據(jù)修改之前在這里緩存
- 重做日志緩存
InnoDB內(nèi)存池主要有以下部分
緩沖池
InnoDB是基于磁盤存儲(chǔ)的,并將其中的記錄按照頁的方式進(jìn)行管理。
而緩沖池就是一塊內(nèi)存區(qū)域,主要緩沖數(shù)據(jù)頁和索引頁。
InnoDB中對(duì)頁的讀取操作,首先判斷該頁是否在緩沖池中,若在,直接讀取該頁,若不在則從磁盤讀取頁數(shù)據(jù),并存放在緩沖池中。
對(duì)頁的修改操作,首先修改在緩沖池中的頁,再以一定的頻率(Checkpoint機(jī)制)刷新到磁盤。
參數(shù):innodb_buffer_pool_size設(shè)置緩沖池大小
緩沖池通過LRU(Latest Recent Used,最近最少使用)算法進(jìn)行管理。最頻繁使用的頁在LRU列表前端,最少使用的頁在尾端,當(dāng)緩沖池不能存放新讀取的頁時(shí),首先釋放LRU列表尾端的頁(頁數(shù)據(jù)刷新到磁盤,并從緩沖池中刪除)。
InnoDB對(duì)于新讀取的頁,不是放到LRU列表最前端,而是放到midpoint位置(默認(rèn)為5/8處)。
這是因?yàn)橐恍㏒QL操作會(huì)訪問大量的頁(如全表掃描),讀取大量非熱點(diǎn)數(shù)據(jù),如果直接放到首部,可能導(dǎo)致真正的熱點(diǎn)數(shù)據(jù)被移除。
關(guān)于頁的概念會(huì)在存儲(chǔ)篇解釋,這里就理解為InnoDB將表數(shù)據(jù)拆分為若干固定大小的頁,每頁保存若干表記錄。
重做日志緩存
重做日志先放到這個(gè)緩沖區(qū),然后按一定頻率刷新到重做日志文件。
參數(shù):innodb_log_buffer_size
刷新規(guī)則:
- Master Thread每秒將一部分重做日志緩沖刷新到重做日志文件
- 每一事務(wù)提交時(shí)會(huì)將重做日志刷新到重做日志文件(如果配置了)
- 重做日志緩沖區(qū)使用空間大于1/2
額外的內(nèi)存池
內(nèi)存堆,對(duì)InnoDB內(nèi)部使用的數(shù)據(jù)結(jié)構(gòu)對(duì)象進(jìn)行管理
Checkpoint機(jī)制
InnoDB對(duì)于對(duì)于DML語句操作(如Update或Delete),事務(wù)提交時(shí)只需在緩沖池中中完成操作,然后再通過Checkpoint將修改后的臟頁數(shù)據(jù)刷新到磁盤。
InnoDB有兩種Checkpoint
Sharp Checkpoint:數(shù)據(jù)庫關(guān)閉是將所有臟頁刷新回磁盤
Fuzzy Checkpoint:
Master Thread Checkpoint
Master Thread每個(gè)1秒或10秒按一定比例將緩存池的臟頁列表刷新回磁盤FLUSH LRU LIST Checkpoint
Page Cleaner線程發(fā)現(xiàn)LRU列表中可用頁數(shù)量少于innodb_lru_scan_depth(1024),就將LRU列表尾端移除,如果這些頁中有臟頁,就需要CheckpointAsync/Sync Flush Checkpoint
重做日志文件空間不可以用時(shí),將一部分臟頁刷新到磁盤。Dirty Page too much Checkpoint:
臟頁數(shù)量太多(超過比例innodb_max_dirty_pages_pct,默認(rèn)75),執(zhí)行Checkpoint。
重做日志
重做日志是為了保證事務(wù)的原子性,持久性。InnoDB采用Write Ahread Log策略,事務(wù)提交時(shí),先寫重做日志,再修改頁。
數(shù)據(jù)庫宕機(jī)重啟時(shí)通過執(zhí)行重做日志恢復(fù)數(shù)據(jù)。
但由于Checkpoint機(jī)制,數(shù)據(jù)庫宕機(jī)重啟并不需要重做所有的日志,因?yàn)镃heckpoint之前的頁都刷新到磁盤了,只需執(zhí)行最新一次Checkpoint后的重做日志進(jìn)行恢復(fù),這樣可以縮短數(shù)據(jù)庫的恢復(fù)時(shí)間。
InnoDB中重做日志文件是循環(huán)使用的。當(dāng)頁被Checkpoint刷新到磁盤后,對(duì)應(yīng)的重做日志就不需要使用 ,其空間可以被覆蓋重用。
如果待寫入的重做日志文件空間不可用(臟頁還沒有刷新到磁盤),就需要強(qiáng)制產(chǎn)生Checkpoint,將緩沖池中的頁至少刷新到當(dāng)前重做日志的位置。
InnoDB 1.2.x(MySql 5.6)后,F(xiàn)LUSH LRU LIST Checkpoint以及Async/Sync Flush Checkpoint操作放到Page Cleaner線程,以免阻塞用戶線程。
線程
主要作用:
- 負(fù)責(zé)刷新內(nèi)存池中的數(shù)據(jù),保證緩沖池的內(nèi)存緩沖的是最近的數(shù)據(jù)
- 已修改的數(shù)據(jù)文件刷新到磁盤文件
- 保證數(shù)據(jù)庫發(fā)生異常的情況下InnoDB能恢復(fù)到正常狀態(tài)。
InnoDB運(yùn)行時(shí)主要有以下線程
Master Thread
負(fù)責(zé)將緩沖池中的數(shù)據(jù)異步刷新到磁盤,保證數(shù)據(jù)的一致性,包括臟頁的刷新,合并插入緩沖(INSERT BUFFER),UNDO頁的回收等。
IO Thread
負(fù)責(zé)AIO請(qǐng)求的回調(diào)處理。
參數(shù):innodb_read_io_threads,innodb_write_io_threads
Purge Thread
事務(wù)提交后,undo log可能不再需要,由Purge Thread負(fù)責(zé)回收并重新分配的這些已經(jīng)使用的undo頁。
注意:Purge Thread需要離散地讀取undo頁。
Page Cleaner Thread
InnoDB 1.2.x引入,將Master Threader中刷新臟頁的工作移至該線程,如上面說的FLUSH LRU LIST Checkpoint以及Async/Sync Flush Checkpoint。
Master Thread
Master Thread具有最高的線程優(yōu)先級(jí)別,內(nèi)部由多個(gè)循環(huán)組成:主循環(huán)(loop),后臺(tái)循環(huán)(backgroup loop),刷新循環(huán)(flush loop),暫停循環(huán)(suspend loop),Master Thread根據(jù)數(shù)據(jù)庫運(yùn)行狀態(tài)在以上循環(huán)切換。
Master Thread主要流程偽代碼如下
void master_thread() {
goto loop;
// 主循環(huán)
loop:
for(int i = 0; i < 10; i++) {
// 每秒一次操作
thread_sleep(1)
// 日志緩沖刷新到磁盤,即使這個(gè)事務(wù)沒提交
do log buffer flush to disk
// 合并插入緩沖(如果前一秒IO次數(shù)少于5次,InnoDB認(rèn)為IO壓力很小,執(zhí)行該操作)
if(last_one_second_ios < 5)
do merge at most 5 insert buffer
// 至多刷新100個(gè)InnoDB的臟頁到磁盤(臟頁比例超過innodb_max_dirty_pages_pct)
if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
do buffer poll flush 100 dirty page
// 沒有用戶活動(dòng),跳轉(zhuǎn)到 backgroupo loop
if(no user activity)
goto backgroupo loop
}
// 每10秒操作
// 刷新100個(gè)臟頁到磁盤(過去10秒內(nèi)IO操作小于200次)
if(last_ten_second_ios < 200)
do buffer pool flush 100 dirty page
// 合并最多5個(gè)插入緩沖
do merge at most 5 insert buffer
// 合并最多5個(gè)插入緩沖
do log buffer flush to disk
// 刪除無用的Undo頁(最多20個(gè)undo頁)
do full purge
//臟頁比例超過innodb_max_dirty_pages_pct,刷新100個(gè)臟頁到磁盤,否則刷新10個(gè)臟頁
if(buf_get_modified_ratio_pct > 70%)
do buffer pool flush 100 dirty page
else
do buffer pool flush 10 dirty page
goto loop
// 后臺(tái)循環(huán)
backgroup loop:
// 刪除無用的Undo
do full purge
// 合并20個(gè)插入緩沖
do merge 20 insert buffer
if not idel:
goto loop
else
goto flush loop
// 刷新循環(huán)
flush loop:
// 刷新100個(gè)臟頁到磁盤,直到臟頁比例小于innodb_max_dirty_pages_pct
do buffer pool flush 100 dirty page
if(buf_get_modified_ratio_pct>innodb_max_dirty_pages_pct)
goto flush loop
goto suspend loop
// 暫停循環(huán)
suspend loop:
// 暫停線程
suspend_thread()
// 等待事件
waiting event;
goto loop;
}
如上所示,主循環(huán)有兩大操作,每秒操作和十秒操作。
InnoDB1.0.x優(yōu)化:
在每秒操作中,Master Thread每次最多刷新100個(gè)臟頁(臟頁比例超過innodb_max_dirty_pages_pct),合并20個(gè)插入緩沖,如果在寫入密集的應(yīng)用,處理速度可能太慢了。
從InnoDB 1.0.x開始,提供了通過innodb_io_capacity參數(shù)
每秒操作中合并插入緩沖數(shù)量為innodb_io_capacity * 5%
刷新臟頁數(shù)量為innodb_io_capacity
而默認(rèn)innodb_max_dirty_pages_pct參數(shù)值從90調(diào)整為75
引入以下參數(shù)
innodb_adaptive_flushing:自適應(yīng)刷新
臟頁比例小于innodb_max_dirty_pages_pct,也會(huì)刷新一定量的臟頁(由InnoDB控制刷新策略和數(shù)量)
innodb_purge_batch_size:控制每次full purge回收Undo頁,默認(rèn)還是20
InnoDB1.2.x優(yōu)化:
- InnoDB空閑時(shí),執(zhí)行原來的10秒一次操作,繁忙時(shí),執(zhí)行原來的每秒一次操作
- 刷新臟頁操作,分離到單獨(dú)Page Cleaner Thread
InnoDB關(guān)鍵特性
插入緩沖
插入聚集索引一般是順序的,不需要磁盤的隨機(jī)讀取
但插入非聚集索引葉子節(jié)點(diǎn)不是順序的,需要離散訪問非聚集索引頁,速度較慢。
對(duì)于非聚集索引的插入或更新,先判斷插入的非聚集索引頁是否在緩存池中,若在,直接插入,或不在,先放到一個(gè)Inser Buffer對(duì)象中,
然后根據(jù)一些算法將Insert Buffer緩存的記錄通過后臺(tái)線程慢慢合并刷新回輔助索引。
插入緩沖將多次插入合并為一次操作,減少磁盤的離散操作。
使用Insert Buffer需滿足兩個(gè)條件:
索引是輔助索引
索引不是唯一的(不需要查找索引頁判斷唯一性)
InnoDB從1.0.x引入Change Buffer,對(duì)INSERT,DELETE,UPDATE都進(jìn)行緩沖。
參數(shù):innodb_change_buffer_max_size,Change Buffer最多使用緩沖池內(nèi)存空間。
兩次寫 doublewrite
部分寫失效:頁數(shù)據(jù)寫入到磁盤時(shí)只寫了一部分(如16K數(shù)據(jù)只寫了2K),數(shù)據(jù)庫就宕機(jī)了,導(dǎo)致頁數(shù)據(jù)損壞,這時(shí)無法使用重做日志恢復(fù)。(執(zhí)行重做日志時(shí)需要利用頁的一些變量,如checksum)
因此在使用重做日志恢復(fù)數(shù)據(jù)庫,需要有一個(gè)頁的副本,當(dāng)發(fā)生寫失效時(shí),先通過頁的副本還原該頁,再進(jìn)行重做。于是InnoDB實(shí)現(xiàn)了doublewrite技術(shù)。
doublewrite有兩部分,一部分是內(nèi)存中的doublewrite buffer,大小為2MB,另一部分是磁盤共享表空間連續(xù)的128個(gè)頁,也是2MB。
doublewrite要求刷新緩沖池的臟頁時(shí)執(zhí)行以下步驟
- 通過memcpy函數(shù)將臟頁復(fù)制到內(nèi)存的doublewrite buffer
- doublewrite buffer分兩次,每次1MB順序?qū)懭牍蚕肀砜臻g
- 調(diào)用fsync函數(shù)同步磁盤,避免緩沖寫帶來問題,確保數(shù)據(jù)刷新到共享表空間(順序?qū)懀_銷小)
- 將上述的臟頁數(shù)據(jù)寫入各個(gè)表空間文件(離散寫)
自適應(yīng)哈希索引
InnoDB會(huì)監(jiān)控對(duì)表上各索引頁的查詢執(zhí)行情況,如發(fā)現(xiàn)建立哈希索引可以提升速度,則建立哈希索引,這是過程不需要用戶干預(yù)。
參數(shù):innodb_adaptive_hash_index,默認(rèn)AHI為開啟狀態(tài)
異步IO
InnoDB使用異步IO操作磁盤,避免同步IO導(dǎo)致阻塞,也可以進(jìn)行IO Merge操作,將多個(gè)IO操作合并為一個(gè)IO操作。
刷新鄰接頁
當(dāng)刷新一個(gè)臟頁時(shí),InnoDB會(huì)檢測(cè)該頁所在區(qū)的所有頁,如果是臟頁,一起刷新,這是可以通過AIO將多個(gè)IO寫入操作合并為一個(gè)IO操作。
參數(shù):innodb_flush_neighbors,控制開關(guān)
文件
參數(shù)文件
在MySql實(shí)例啟動(dòng)時(shí)指定某些初始化參數(shù),如數(shù)據(jù)庫文件目錄,內(nèi)存池大小等。
參看參數(shù)文件所在目錄:mysql --help|grep my.cnf
linux下默認(rèn)目錄為/etc/mysql/my.cnf
/etc/mysql/conf.d/mysql.cnf為客戶端參數(shù)文件
/etc/mysql/mysql.conf.d/mysql.cnf為服務(wù)端參數(shù)文件
參數(shù)類型:
動(dòng)態(tài)參數(shù),可以在MySql實(shí)例運(yùn)行中進(jìn)行更改
SET [global | session] system_var_name = expr
SET [@@golbal. | @@session. | @@]system_var_name = expr靜態(tài)參數(shù):只能在實(shí)例停止時(shí)修改
注意:datadir參數(shù)指定MySql數(shù)據(jù)目錄,它是數(shù)據(jù)文件,日志文件默認(rèn)存放目錄。
日志文件
錯(cuò)誤日志
遇到問題應(yīng)該查看該文件以便定位問題。
參數(shù):log-error
慢查詢?nèi)罩?/strong>
記錄執(zhí)行時(shí)間超過某一閥值的所有SQL
參數(shù):
log_slow_queries:記錄慢SQL的開關(guān)
long_query_time:慢SQl的閥值,默認(rèn)為10,單位秒
log_queries_not_using_indexes:是否記錄沒有使用索引的查詢
log_throttle_queries_not_using_indexs:每分鐘最多記錄沒使用索引的SQL的數(shù)量
slow-query-log-file:指定目錄,默認(rèn)在data目錄
log_output:輸出格式,默認(rèn)為FILE,可以配置為TABLE(記錄到slow_log表)
可以使用mysqldumpslow分析慢日志
查詢?nèi)罩?/strong>
記錄所有對(duì)MySql數(shù)據(jù)庫的請(qǐng)求信息。
默認(rèn)文件名為主機(jī)名.log
,也可以輸出到mysql架構(gòu)的general_log表中。
二進(jìn)制日志
記錄對(duì)MySql數(shù)據(jù)庫執(zhí)行更改的所有操作,不包括select,show這類操作。
可用于恢復(fù),復(fù)制,審計(jì)(判斷是否有對(duì)數(shù)據(jù)庫進(jìn)行注入攻擊)
MySql官方手冊(cè)測(cè)試表明,開啟二進(jìn)制日志會(huì)使性能下降1%,但考慮到復(fù)制,point-in-time的恢復(fù)等功能,完成可以接受,建議開啟。
參數(shù):
log_bin:是否開啟二進(jìn)制日志,默認(rèn)No
log-bin:指定日志名稱,默認(rèn)為主機(jī)名,后綴為二進(jìn)制日志序列號(hào),如bin_log.000001
max_binlog_size:?jiǎn)蝹€(gè)二進(jìn)制日志文件最大值
binlog_cache_size:二進(jìn)制日志緩沖區(qū)大小,基于會(huì)話
sync_binlog:1 表示使用同步寫磁盤方法寫入二進(jìn)制日志,默認(rèn)為0
binlog_format:二進(jìn)制日志格式,可以為STATEMENT,ROW,MIXED
-- STATEMENT:記錄SQL語句
-- ROW:記錄表的行更改情況
-- MIXED:默認(rèn)使用STATEMENT,一些特定情況使用ROW
使用ROW可以為數(shù)據(jù)庫的恢復(fù)和復(fù)制帶來更好的可靠性,但會(huì)導(dǎo)致二進(jìn)制文件大小增加。復(fù)制的網(wǎng)絡(luò)開銷也有所增加。
可以使用mysqlbinlog可以分析二進(jìn)制日志
套接字文件
當(dāng)使用UNIX域套接字方式進(jìn)行連接時(shí)需要的文件。
參數(shù):socket
pid文件
MySql實(shí)例的進(jìn)程ID文件。
參數(shù):pid_file
表結(jié)構(gòu)定義文件
存放MySql表結(jié)構(gòu)定義。
在數(shù)據(jù)目錄下,每一個(gè)表都有一個(gè)子目錄,存放對(duì)應(yīng)的表結(jié)構(gòu)定義文件,后綴為frm
MySql8.0后,移除了.frm文件,表結(jié)構(gòu)定義存放到數(shù)據(jù)庫系統(tǒng)表中。
每個(gè)存儲(chǔ)引擎都可以自行定義的文件保存引擎所需的數(shù)據(jù)。InnoDB存儲(chǔ)引擎定義了以下文件:
表空間文件
InnoDB將所有數(shù)據(jù)(表數(shù)據(jù),索引,插入緩沖索引頁,回滾信息,插入緩沖索引頁,系統(tǒng)事務(wù)信息,二次寫緩沖等等)邏輯地放在一個(gè)空間中,稱為共享表空間。
表空間數(shù)據(jù)默認(rèn)保證在數(shù)據(jù)目錄下的ibdata1文件,該文件初始大小為10M。
一個(gè)表空間可以由多個(gè)文件組成。
參數(shù):innodb_data_file_path,可以通過多個(gè)文件組成一個(gè)表空間,同時(shí)指定文件屬性,如
/db/ibdata1:2000M;/db/ibdata2:2000M:autoextend
,autoextend表示文件可以自動(dòng)擴(kuò)容,只有最后一個(gè)文件可以被指定為自動(dòng)擴(kuò)容。
開啟參數(shù)innodb_file_per_table后,每個(gè)表都產(chǎn)生一個(gè)獨(dú)立的表空間,文件命名為:表名.ibd,該表的數(shù)據(jù),索引和插入緩沖BITMAP等信息到保存到獨(dú)立表空間,但其它數(shù)據(jù)(如回滾信息,插入緩沖索引頁,系統(tǒng)事務(wù)信息,二次寫緩沖)還是存放在默認(rèn)的表空間。
重做日志文件
每個(gè)InnoDB存儲(chǔ)引擎至少有1個(gè)重做日志文件組,每個(gè)文件組下至少有2個(gè)重做日志文件,默認(rèn)為ib_logfile0,ib_logfile1,一個(gè)文件寫滿后,再寫另一個(gè)文件,循環(huán)復(fù)用。
Undo日志文件
undo log保證事務(wù)的原子性, 幫助事務(wù)回滾以及MVCC功能。
在InnoDB 1.2.x(MySQL 5.6)前,undo日志保存在共享表空間ibdata1文件中的,隨著數(shù)據(jù)庫的運(yùn)行時(shí)間的不斷增長(zhǎng),會(huì)導(dǎo)致共享表空間越來越大,
從InnoDB 1.2.x(MySQL 5.6)起,Undo日志被分離出來,由單獨(dú)的Undo表空間管理,這樣可以避免處理Undo日志的IO過于集中,有助于分散IO負(fù)載。
Undo日志默認(rèn)保存數(shù)據(jù)目錄下的undo_001,undo_002文件中。
MySQL 5.7,提供undo log在線回收功能
MySQL 8.0,可以通過SQL語句非常方便的管理 Undo 表空間
關(guān)于重做日志和undo日志會(huì)在事務(wù)篇詳細(xì)解析。
InnoDB配置參數(shù)文檔:InnoDB Startup Options and System Variables
如果您覺得本文不錯(cuò),歡迎關(guān)注我的微信公眾號(hào),您的關(guān)注是我堅(jiān)持的動(dòng)力!