目錄
2.6、重做日誌緩存 redo log(與 binlog 日誌的區別)
1、4種後臺線程
MySQL數據庫實例是一個進程,包含了多個線程,這篇文章將介紹一下InnoDB引擎的4種線程。
1.1、Master Thread
主線程。是數據庫功能的核心提供者,如果主線程崩潰了,那數據庫就沒法正常使用了,除非重啓主線程。
1.2、IO Thread
這是負責數據庫讀取和寫入的線程,默認讀取線程有4個,寫入線程有4個,當然可以通過設置下圖的兩個參數來增加或者減少線程數。
1.3、Purge Thread
在提交了一個事務給數據庫時,爲了保證事務能夠回滾,會在內存中緩存下該事務未執行時的狀態(undo 日誌),如果事務執行不成功,那麼就恢復之前的狀態,如果執行成功,undo日誌就用不着了,要把它刪除以騰出空間。Purge 線程就是做這個刪除工作的。
默認有4個線程。
1.4、Page Cleaner Thread
這個線程專門用於將內存中的“髒頁”刷入磁盤,使得磁盤內的數據和內存中的數據一致。內存就相當於數據庫的緩存,同一個數據存在兩個地方,就會出現數據不一致的情況。
默認是一個刷髒頁線程,查看 performance_schema 庫裏面的 global_variables 表格,裏面存的全局變量,包含了刷髒頁線程的線程數量。
髒頁線程的線程號是13,後臺運行,線程名爲 thread/innodb/page_cleaner_thread 。
MySQL 5.7 版本以後,支持設置多個刷髒頁線程,提高髒頁處理性能。設置命令,比如:SET GLOBAL innodb_page_cleaner = 3
2、InnoDB 引擎緩存
2.1、盤面,磁道,扇區,塊,頁的概念
先弄明白幾個概念:磁盤(盤面,磁道,扇區),內存(塊,頁)。
下面是磁盤的結構圖(網上搜的),左圖是一個盤面,右圖是磁盤結構,磁盤有很多個盤,每個盤有2面,2面都能存儲數據,在每一個面裏,有很多同心圓圈,這些圈就是一條條磁道,然後每條磁道又被分爲均勻地分爲很多扇區,扇區是磁盤讀取數據的最小單元(通常是512B),最小單元的意思就是不管你存1B 還是 512B,都會佔用一個扇區,沒有說我只用半個扇區,另外半個留給其它的數據用。
還有,爲什麼很多地方都講:對連續的物理存儲數據進行讀取的時候,速度也和內存隨機讀取差不多。因爲在同一條磁道上,如果數據內容是連續的,那直接旋轉讀取就OK,之所以說磁盤的IO速度慢,其中一部分原因就在於數據存儲的位置不連續的話,磁針就要多次去尋道(找磁道,並定位起始扇區),磁盤又是機械的,所以速度相對於內存來說,是慢很多的。
塊:操作系統是軟件層面的,它不認識什麼扇區,它將磁盤空間進行了抽象(換成自己認識的概念),塊就是操作系統對磁盤讀取數據的最小單元,一個塊通常對應於磁盤的一個扇區或者幾個連續的扇區(這就看操作系統的粗細粒度)。
頁:這個概念涉及操作系統尋址的知識了,64位的尋址總線,要能夠管理更大的內存,就需要使用分頁技術,每一頁代表一段連續的內存空間,一個地址對應一頁,先用地址去找到對應頁,至於裏面的內容,就用偏移量去找。所以頁是內存層面的概念。
到此,就比較明瞭了,比如,我們的軟件要讀取一頁的數據,會將指令交給操作系統,操作系統會按照塊爲單位進行讀取,底層的驅動則會驅動磁盤讀取出對應數量的扇區的數據。一頁對應若干塊,一塊通常對應若干扇區。
InnoDB引擎是軟件,用緩存技術在內存中緩存一小部分數據庫裏的數據,提高性能,所以InnoDB的腦子裏只有頁,並且它也是將數據庫按照頁的方式進行管理的,頁的大小默認是16k = 16384 bytes。
2.2、InnoDB緩存的基本原理
由於InnoDB是基於磁盤的,但是磁盤的IO速度慢,爲了提升數據庫性能,InnoDB會在內存中緩存一部分數據,當用戶添加更新數據時,先看是否命中緩存,如果是,直接更新緩存中的數據,等到刷髒頁時機觸發時,再將緩存中的新數據寫入磁盤;當用戶讀取數據時,也是先看是否命中緩存,如果是,直接從緩存裏讀取數據即可。
InnoDB緩存在很多教材裏被叫作緩存池或者緩衝池,buffer_pool。在performance_schema庫的global_variables表裏,存放着全局變量,下圖是關於緩存池的配置參數。
innodb_buffer_poop_chunk_size:緩存池大小可以動態修改,但是要以innodb_buffer_poop_chunk_size爲單位,進行多少倍增加或者減少。
innodb_buffer_pool_instances:緩存池實例數量,默認爲1。建議設置爲CPU個數。
innodb_buffer_pool_size:所有緩存池的總大小,我這個版本的mysql默認爲 128M = 134217728 Bytes。如果要擴大,建議不要超過物理內存的80%,一般就設置在50% ~ 80%。
緩存池中存儲的數據類型:索引頁、數據頁、undo日誌頁、插入緩存、自適應哈希索引、鎖信息、數據字典信息等等,其中,索引頁和數據頁佔了大部分,InnoDB裏的索引是聚集索引,因此索引裏面既存了索引值,又存儲了記錄;而這裏的數據頁不是指用戶的數據,而是指 undo數據,系統數據,blob數據等等。
緩存池可以有多個,每個緩存池被稱爲緩存池實例,InnoDB可以有多個緩存池實例,每個頁根據哈希值取模來決定要存儲到那個緩存池實例(跟redis數據存儲於哪個槽有點像哦~~)。innodb_buffer_pool_instances 這個全局變量就是緩存池的數量,每個緩存池的大小 = innodb_buffer_pool_size / innodb_buffer_pool_instances。爲什麼要分爲多個緩存池呢?因爲要提升數據庫的併發性能,如果是一個緩存池的話,多個線程會競爭緩存池,影響性能,如果分爲多個緩存池實例的話,減小競爭,這也正好理解了“爲什麼建議緩存池實例數量設置爲CPU個數”。
每一個緩存池實例的狀態信息夠存儲在information_schema庫的 INNODB_BUFFER_POOL_STATS 裏,每個實例都有個Pool_id,用於區分緩存池實例。可以查看這個表。
一個緩存池實例的結構如下圖(借用的,好幾處文章都看見這個圖了,也不知道誰是原作者,勿噴),它不是單純地存儲每一個頁,試想,每一頁都存進去了,怎麼管理呢?哪一頁在哪個位置?這一頁數據代表什麼?因此,爲了方便管理,每一頁都有對應的一個控制塊,控制塊在緩存池的一端,緩存頁在另一端,塊裏的內容就是該頁所屬的表空間編號(space id)、頁號(page number)、頁在Buffer Pool中的地址,一些鎖信息以及LSN信息等等,每一個控制塊的大小都是相同的,每一頁的大小也是相同的,所以我們要好好計算 innodb_buffer_pool_instances 和 innodb_buffer_pool_size,以避免出現下圖的內存碎片。
緩存池看起來有點像一個列表。
2.3、緩存池空間管理——Free列表
當需要緩存一頁數據時,我們怎麼指定緩存池中哪些頁(緩存池的空間是按頁劃分管理的)是空白的,不然萬一存在某個有數據的頁空間去,覆蓋掉原先的數據就慘了。
所以,InnoDB用了一個Free列表去存儲緩存池中所有空白的頁空間情況, 比如每一頁的起始地址等等。每次需要在緩存池存儲頁的時候,都會看看Free列表是否有空閒的頁,如果沒有,那就只有清空一個頁,騰出一個位置給新頁。
一個緩存池實例對應一個Free表。
2.4、頁的管理算法——LRU算法
一個緩存池實例就那麼大點,存儲的頁有限,因此,有一個算法LRU(最近最少使用算法)被用來管理緩存池,決定哪些頁被清出緩存池,哪些頁被加入緩存池。InnoDB用了一個LRU列表來存儲緩存頁標識,列表裏對應的緩存頁代表目前還在緩存池中,越是靠近列表頭,說明該頁越是最近被頻繁使用,越在列表尾部,說明越是不怎麼被使用。並且,還定義了一個midpoint位置,這個位置之前代表new頁(頻繁被用),這個位置之後(包括midpoint本身)代表old頁(不怎麼被用),old頁容易被清出緩存池。
這個midpoint位置默認處於列表尾部的37%處,全局變量 innodb_old_blocks_pct 就代表這個位置。每當有一個頁需要存儲到緩存池時,不是放在列表頭,而是放在midpoint位置,如果此時緩存池已滿,就將列表末端頁刪除騰出一個位置給新頁,如果此時緩存還沒有滿,那就直接放在midpoint位置。
old頁就永遠是old頁嗎?不是,它轉爲new頁的時機需要根據innodb_old_blocking_time(單位毫秒),默認是1秒,當用戶讀取數據庫數據時,假如就讀取一頁,這一頁數據被緩存在緩存池中midpoint位置,從此刻開始的innodb_old_blocking_time時間內,不管用戶讀取它多少次,都不會變爲new頁放到列表前面去,只有當innodb_old_blocking_time之後,一旦用戶再次讀取它,它就會變爲new頁了。
我們可以調整下面2個參數來減小熱點數據被清出的概率。
另外,Free表大小 + LRU表大小 = 緩存池頁數量。 一個緩存池實例對應一個LRU表,在information_schema庫裏的INNODB_BUFFER_PAGE_LRU表裏可以查看LRU表格情況。
2.5、髒頁的管理——Flush列表
如果用戶要修改某數據,先是看數據是否命中緩存池,如果命中,會將新值更新到緩存池,此時,緩存池裏的數據和磁盤裏的數據不一致了,就需要刷髒頁,將緩存池裏的新數據寫入磁盤,使其一致。但是我們知道磁盤IO速度慢,如果用戶頻繁地更改數據,那豈不是影響性能。
因此InnoDB用了一個Flush列表存儲髒頁信息,每隔一段時間,或者時機到了,就會觸發一次刷髒頁,將Flush表的髒頁一起處理,Flush表的信息在information_schema庫的INNODB_BUFFER_PAGE_LRU裏,和LRU表信息放在一起的,只是Flush表的信息需要增加查詢條件:OLDEST_MODIFICATION > 0。
在刷髒頁的時候,是有專門的線程去執行的,我們考慮一下按什麼樣的順序去刷髒頁,我個人想法有2種:1、某一頁被修改的次數,次數越多,說明與磁盤的數據越不一致,越是急迫需要刷新(第一種好像是InnoDB的刷新策略);2、某一頁第一次被修改的時間,越早被修改過,說明磁盤裏的數據越不實時,越應該被優先刷新。
2.6、重做日誌緩存 redo log(與 binlog 日誌的區別)
先講講一個事務的執行過程吧,首先,事務開始,對要修改的數據上鎖,如果數據沒有在緩存池內,就需要將數據從磁盤裏讀到緩存池內,如果數據已經在緩存池就更好了,然後將事務對頁的修改進行redo日誌記錄,存放在redo日誌緩存中,緩存好了之後,纔開始修改緩存池中的數據,修改完畢後,事務被提交,表明事務執行完了,事務的提交會觸發將redo日誌緩存刷新到磁盤中的redo日誌文件中。
redo日誌緩存的作用:當用戶將要執行一個事務時,會在事務執行之前,將事務對頁的修改存入redo日誌緩存中,等事務修改了數據後,並提交事務,然後才觸發將redo日誌緩存裏的數據寫入磁盤中的redo日誌文件中,以保存起來。
爲什麼要redo日誌呢?因爲事務執行完畢並提交了事務後,修改的數據還在緩存池裏,磁盤還沒有刷新,如果此時系統崩潰了或者斷電了,緩存池裏的數據都沒了,該事務就等於沒有執行。如果有了redo日誌文件(磁盤裏的),我們就可以恢復事務執行的結果了。因此,一旦緩存池的頁被刷新到磁盤後,其對應的redo日誌數據就沒用了,因爲最新的數據已經持久化到磁盤了,不會丟失了,自然就用不着redo日誌了,這種redo日誌被叫作無用redo日誌。
那redo日誌緩存區到底有多大呢?我這個版本的Mysql給的默認值是16K = 16777216 bytes。其它資料裏說,8M基本上就夠存儲大部分應用的事務redo日誌了。
redo日誌緩存刷入redo日誌文件的時機:1、定期(master線程做); 2、每個事務提交時,就會觸發redo日誌緩存刷入,及時將該事務的redo日誌存入磁盤;3、當redo日誌緩存空間剩餘不到1/2時,也會觸發。
關於第二個時機,再具體講一點,redo 緩存內容會先寫入系統文件緩存中,在fsysn操作後,纔會同步到磁盤中,下面有個全局參數,默認爲1,表示每個事務提交時,都會觸發執行一個 fsysn 操作,即將 redo 日誌刷入磁盤,但是我們還可以將參數設置爲 0, 表示提交事務不觸發redo 日誌刷新, 設置爲 2, 表示事務提交時,僅會把 redo 日誌緩存寫入 系統文件緩存,不會執行 fsysn 操作,我想什麼時候 fsysn 操作會被執行呢? 可能這就是操作系統的策略了吧,如果數據庫崩了,那系統文件緩存裏還有redo數據,如果操作系統崩了或者重啓,那就沒了。
另外,還有一個binlog日誌,這個日誌是應用層面的,和數據庫引擎沒有關係,binlog 日誌的作用目的和redo log差不多,但是也有區別,binlog 用於存儲對數據庫的所有改動sql,存的sql級別的,而且是所有的,而 redo 日誌記錄的是物理內存級別上的修改,更加底層。
爲什麼要有2種呢,因爲 redo 日誌記錄的是底層級別的修改記錄,一旦用redo 日誌恢復起來,redo日誌裏的修改操作被執行起來的併發性更好,而 binlog 是應用層面的修改操作,執行起來效率更低。
2.7、額外內存池
緩存池用於存儲頁,對於每一頁,還需要存儲它的信息,比如這一頁對應磁盤裏的是那一部分的空間,這一頁在緩存池中的地址等等,另外還有LRU表,Free表,Flush表,這些內容都存放在哪裏呢?不是存在緩存池,而是存在額外內存池中。
因此,不要忽略額外內存池,因爲如果緩存池越大,額外內存池需要記錄的數據也越多,應該相應的擴大額外內存池的空間。
在MySQL 5.7.4 版本之前,全局參數 innodb_additional_mem_pool_size 表示額外內存池大小, innodb_use_sys_malloc 表示是否讓操作系統替它分配內存,ON表示要讓,OFF表示不讓,InnoDB自己分配。
在MySQL 5.7.4 版本之後, innodb_additional_mem_pool_size 和 innodb_use_sys_malloc 被取消了,直接交由操作系統來分配額外內存池,你要多大,操作系統就給你分配多大,不用我們去關心了。
3、Checkpoint技術——刷髒頁技術
上面已經簡單的介紹了刷髒頁,這一節詳細地說一說。
1、緩存某個頁的時候,如果某個緩衝池已經滿了,此時會根據LRU算法將LRU列表尾的頁清除掉,如果該頁是髒頁,在清除之前,要進行一次刷髒頁操作。 (換一種專業說法,首先查看Free列表是否有空閒頁,如果沒有,那就根據LRU算法將LRU列表末尾的頁刪除以騰出空間,在刪除之前,看看該末尾頁是否在Flush列表中,如果是,說明該末尾頁是在髒頁,觸發一次刷髒頁,刷新後,再刪除該末尾頁騰出空間)。
2、磁盤裏的redo日誌文件大小有限,因此redo日誌數據會循環地進行覆蓋存儲,但是隻能覆蓋那些無用redo日誌,如果此時沒有無用redo日誌給新的日誌覆蓋,怎麼辦呢?會立即觸發一次刷髒頁,部分髒頁被刷後,這些髒頁對應的redo日誌就沒用了,那新日誌就可以覆蓋了。
刷髒頁並不是每次都把所有的髒頁刷新,而是刷新一部分髒頁,保證數據庫的性能,如果每次都全部刷新的話,豈不是用戶要等數據庫刷完了才能進行訪問和操作。
InnoDB 有兩種刷髒頁方式(不是二選一,而是並存的):Sharp Checkpoint 和 Fuzzy CheckPoint。
Sharp Checkpoint 是在數據庫關閉的時候執行,默認把所有的髒頁刷新。 全局參數 innodb_fast_shutdown 代表了 Sharp Checkpoint執行的方式,有3個值(0,1,2)。0表示在innodb關閉的時候,需要purge all, merge insert buffer,flush dirty pages。這是最慢的一種關閉方式,但是restart的時候也是最快的。1表示在innodb關閉的時候,它不需要purge all,merge insert buffer,只需要flush dirty page。 2表示在innodb關閉的時候,它不需要purge all,merge insert buffer,也不進行flush dirty page,只將log buffer裏面的日誌flush到log files。因此等下進行恢復的時候它是最耗時的。
Fuzzy CheckPoint 是數據庫運行期間刷髒頁機制,前文基本上講得差不多了,總結爲:1、master thread 定期刷髒頁; 2、緩存池中的髒頁刷新騰出空間; 3、redo日誌文件無法繼續覆蓋地進行存儲,而刷髒頁。