平時的工做中,不知道你有沒有遇到過這樣的場景,一條 SQL 語句,正常執行的時候特別快,可是有時也不知道怎麼回事,它就會變得特別慢,而且這樣的場景很難復現,它不僅
隨機,並且持續時間還很短。mysql
看上去,這就像是數據庫「抖」了一下。今天,咱們就一塊兒來看一看這是什麼緣由。算法
在前面第 2 篇文章《日誌系統:一條 SQL 更新語句是如何執行的?》中,我爲你介紹了WAL 機制。如今你知道了,InnoDB 在處理更新語句的時候,只作了寫日誌這一個磁盤操
做。這個日誌叫做 redo log(重作日誌),也就是《孔乙己》裏咸亨酒店掌櫃用來記帳的粉板,在更新內存寫完 redo log 後,就返回給客戶端,本次更新成功。sql
三、髒頁&乾淨頁數據庫
不管是髒頁仍是乾淨頁,都在內存中。在這個例子裏,內存對應的就是掌櫃的記憶。bash
接下來,咱們用一個示意圖來展現一下「孔乙己賒帳」的整個操做過程。假設原來孔乙己欠帳 10 文,此次又要賒 9 文。工具
圖 1 「孔乙己賒帳」更新和 flush 過程性能
回到文章開頭的問題,你不難想象,平時執行很快的更新操做,其實就是在寫內存和日誌,而 MySQL 偶爾「抖」一下的那個瞬間,可能就是在刷髒頁(flush)。測試
那麼,什麼狀況會引起數據庫的 flush 過程呢?優化
咱們仍是繼續用咸亨酒店掌櫃的這個例子,想想:掌櫃在什麼狀況下會把粉板上的賒帳記錄改到帳本上?spa
第一種場景是,粉板滿了,記不下了。這時候若是再有人來賒帳,掌櫃就只得放下手裏的活兒,將粉板上的記錄擦掉一些,留出空位以便繼續記帳。固然在擦掉以前,他必須
先將正確的帳目記錄到帳本中才行。
這個場景,對應的就是 InnoDB 的 redo log 寫滿了。這時候系統會中止全部更新操做,把 checkpoint 往前推動,redo log 留出空間能夠繼續寫。我在第二講畫了一個
redo log 的示意圖,這裏我改爲環形,便於你們理解。
圖 2 redo log 狀態圖
checkpoint 可不是隨便往前修改一下位置就能夠的。好比圖 2 中,把 checkpoint 位置從 CP 推動到 CP’,就須要將兩個點之間的日誌(淺綠色部分),對應的全部髒頁都
flush 到磁盤上。以後,圖中從 write pos 到 CP’之間就是能夠再寫入的 redo log 的區域
第二種場景是,這一天生意太好,要記住的事情太多,掌櫃發現本身快記不住了,趕忙找出帳本把孔乙己這筆帳先加進去。
這種場景,對應的就是系統內存不足。當須要新的內存頁,而內存不夠用的時候,就要淘汰一些數據頁,空出內存給別的數據頁使用。若是淘汰的是「髒頁」,就要先將髒頁寫到磁盤。
你必定會說,這時候難道不能直接把內存淘汰掉,下次須要請求的時候,從磁盤讀入數據頁,而後拿 redo log 出來應用不就好了?這裏實際上是從性能考慮的。若是刷髒頁一
定會寫盤,就保證了每一個數據頁有兩種狀態:
第三種場景是,生意不忙的時候,或者打烊以後。這時候櫃檯沒事,掌櫃閒着也是閒着,不如更新帳本。
這種場景,對應的就是 MySQL 認爲系統「空閒」的時候。固然,MySQL「這家酒店」的生意好起來但是會很快就能把粉板記滿的,因此「掌櫃」要合理地安排時間,即
使是「生意好」的時候,也要見縫插針地找時間,只要有機會就刷一點「髒頁」。
第四種場景是,年末了咸亨酒店要關門幾天,須要把帳結清一下。這時候掌櫃要把全部帳都記到帳本上,這樣過完年從新開張的時候,就能就着帳本明確帳目狀況了。
這種場景,對應的就是 MySQL 正常關閉的狀況。這時候,MySQL 會把內存的髒頁都flush 到磁盤上,這樣下次 MySQL 啓動的時候,就能夠直接從磁盤上讀數據,啓動速
度會很快。
其中,第三種狀況是屬於 MySQL 空閒時的操做,這時系統沒什麼壓力,而第四種場景是數據庫原本就要關閉了。這兩種狀況下,你不會太關注「性能」問題。因此這裏,咱們主
要來分析一下前兩種場景下的性能問題。
第一種是「redo log 寫滿了,要 flush 髒頁」,這種狀況是 InnoDB 要儘可能避免的。由於出現這種狀況的時候,整個系統就不能再接受更新了,全部的更新都必須堵住。若是你從
監控上看,這時候更新數會跌爲 0。
第二種是「內存不夠用了,要先將髒頁寫到磁盤」,這種狀況實際上是常態。InnoDB 用緩衝池(buffer pool)管理內存,緩衝池中的內存頁有三種狀態:
InnoDB 的策略是儘可能使用內存,所以對於一個長時間運行的庫來講,未被使用的頁面不多。
而當要讀入的數據頁沒有在內存的時候,就必須到緩衝池中申請一個數據頁。這時候只能把最久不使用的數據頁從內存中淘汰掉:若是要淘汰的是一個乾淨頁,就直接釋放出來復
用;但若是是髒頁呢,就必須將髒頁先刷到磁盤,變成乾淨頁後才能複用。
因此,刷髒頁雖然是常態,可是出現如下這兩種狀況,都是會明顯影響性能的
1. 一個查詢要淘汰的髒頁個數太多,會致使查詢的響應時間明顯變長;
2. 日誌寫滿,更新所有堵住,寫性能跌爲 0,這種狀況對敏感業務來講,是不能接受的。
因此,InnoDB 須要有控制髒頁比例的機制,來儘可能避免上面的這兩種狀況。
接下來,我就來和你說說 InnoDB 髒頁的控制策略,以及和這些策略相關的參數。
首先,你要正確地告訴 InnoDB 所在主機的 IO 能力,這樣 InnoDB 才能知道須要全力刷髒頁的時候,能夠刷多快。
這就要用到 innodb_io_capacity 這個參數了,它會告訴 InnoDB 你的磁盤能力。這個值我建議你設置成磁盤的 IOPS。磁盤的 IOPS 能夠經過 fio 這個工具來測試,下面的語句是
我用來測試磁盤隨機讀寫的命令:
fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
其實,由於沒能正確地設置 innodb_io_capacity 參數,而致使的性能問題也比比皆是。
以前,就曾有其餘公司的開發負責人找我看一個庫的性能問題,說 MySQL 的寫入速度很慢,TPS 很低,可是數據庫主機的 IO 壓力並不大。通過一番排查,發現罪魁禍首就是這
個參數的設置出了問題。
他的主機磁盤用的是 SSD,可是 innodb_io_capacity 的值設置的是 300。因而,InnoDB 認爲這個系統的能力就這麼差,因此刷髒頁刷得特別慢,甚至比髒頁生成的速度
還慢,這樣就形成了髒頁累積,影響了查詢和更新性能。
雖然咱們如今已經定義了「全力刷髒頁」的行爲,但平時總不能一直是全力刷吧?畢竟磁盤能力不能只用來刷髒頁,還須要服務用戶請求。因此接下來,咱們就一塊兒看看 InnoDB
怎麼控制引擎按照「全力」的百分比來刷髒頁。
根據我前面提到的知識點,試想一下,若是你來設計策略控制刷髒頁的速度,會參考哪些因素呢?
這個問題能夠這麼想,若是刷太慢,會出現什麼狀況?首先是內存髒頁太多,其次是 redolog 寫滿。
因此,InnoDB 的刷盤速度就是要參考這兩個因素:一個是髒頁比例,一個是 redo log 寫盤速度。
InnoDB 會根據這兩個因素先單獨算出兩個數字。
參數 innodb_max_dirty_pages_pct 是髒頁比例上限,默認值是 75%。InnoDB 會根據當前的髒頁比例(假設爲 M),算出一個範圍在 0 到 100 之間的數字,計算這個數字的
僞代碼相似這樣:
F1(M) { if M>=innodb_max_dirty_pages_pct then return 100; return 100*M/innodb_max_dirty_pages_pct; }
InnoDB 每次寫入的日誌都有一個序號,當前寫入的序號跟 checkpoint 對應的序號之間的差值,咱們假設爲 N。InnoDB 會根據這個 N 算出一個範圍在 0 到 100 之間的數字,
這個計算公式能夠記爲 F2(N)。F2(N) 算法比較複雜,你只要知道 N 越大,算出來的值越大就行了。
而後,根據上述算得的 F1(M) 和 F2(N) 兩個值,取其中較大的值記爲 R,以後引擎就能夠按照 innodb_io_capacity 定義的能力乘以 R% 來控制刷髒頁的速度。
上述的計算流程比較抽象,不容易理解,因此我畫了一個簡單的流程圖。圖中的 F一、F2就是上面咱們經過髒頁比例和 redo log 寫入速度算出來的兩個值。
圖 3 InnoDB 刷髒頁速度策略
如今你知道了,InnoDB 會在後臺刷髒頁,而刷髒頁的過程是要將內存頁寫入磁盤。因此,不管是你的查詢語句在須要內存的時候可能要求淘汰一個髒頁,仍是因爲刷髒頁的邏輯會佔用 IO 資源並可能影響到了你的更新語句,均可能是形成你從業務端感知到MySQL「抖」了一下的緣由。
要儘可能避免這種狀況,你就要合理地設置 innodb_io_capacity 的值,而且平時要多關注髒頁比例,不要讓它常常接近 75%。
其中,髒頁比例是經過Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 獲得的,具體的命令參考下面的代碼:
mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty'; select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total'; select @a/@b;
接下來,咱們再看一個有趣的策略。
一旦一個查詢請求須要在執行過程當中先 flush 掉一個髒頁時,這個查詢就可能要比平時慢了。而 MySQL 中的一個機制,可能讓你的查詢會更慢:在準備刷一個髒頁的時候,若是
這個數據頁旁邊的數據頁恰好是髒頁,就會把這個「鄰居」也帶着一塊兒刷掉;並且這個把「鄰居」拖下水的邏輯還能夠繼續蔓延,也就是對於每一個鄰居數據頁,若是跟它相鄰的數據頁也仍是髒頁的話,刷。
也會被放到一塊兒
在 InnoDB 中,innodb_flush_neighbors 參數就是用來控制這個行爲的,值爲 1 的時候會有上述的「連坐」機制,值爲 0 時表示不找鄰居,本身刷本身的。
找「鄰居」這個優化在機械硬盤時代是頗有意義的,能夠減小不少隨機 IO。機械硬盤的隨機 IOPS 通常只有幾百,相同的邏輯操做減小隨機 IO 就意味着系統性能的大幅度提高。
而若是使用的是 SSD 這類 IOPS 比較高的設備的話,我就建議你把innodb_flush_neighbors 的值設置成 0。由於這時候 IOPS 每每不是瓶頸,而「只刷本身」,就能更快地執行完必要的刷髒頁操做,
減小 SQL 語句響應時間。
在 MySQL 8.0 中,innodb_flush_neighbors 參數的默認值已是 0 了。
今天這篇文章,我延續第 2 篇中介紹的 WAL 的概念,和你解釋了這個機制後續須要的刷髒頁操做和執行時機。利用 WAL 技術,數據庫將隨機寫轉換成了順序寫,大大提高了數
據庫的性能。
可是,由此也帶來了內存髒頁的問題。髒頁會被後臺線程自動 flush,也會因爲數據頁淘汰而觸發 flush,而刷髒頁的過程因爲會佔用資源,可能會讓你的更新和查詢語句的響應時間
長一些。在文章裏,我也給你介紹了控制刷髒頁的方法和對應的監控方式。
文章最後,我給你留下一個思考題吧。
一個內存配置爲 128GB、innodb_io_capacity 設置爲 20000 的大規格實例,正常會建議你將 redo log 設置成 4 個 1GB 的文件。
但若是你在配置的時候不慎將 redo log 設置成了 1 個 100M 的文件,會發生什麼狀況呢?又爲何會出現這樣的狀況呢?
你能夠把你的分析結論寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期我留給你的問題是,給一個學號字段建立索引,有哪些方法。
因爲這個學號的規則,不管是正向仍是反向的前綴索引,重複度都比較高。由於維護的只是一個學校的,所以前面 6 位(其中,前三位是所在城市編號、第四到第六位是學校編
號)實際上是固定的,郵箱後綴都是 @gamil.com,所以能夠只存入學年份加順序編號,它們的長度是 9 位。
而其實在此基礎上,能夠用數字類型來存這 9 位數字。好比 201100001,這樣只須要佔4 個字節。其實這個就是一種 hash,只是它用了最簡單的轉換規則:字符串轉數字的規則,而恰好咱們設定的這個背景,能夠保證這個轉換後結果的惟一性。評論區中,也有其餘一些很不錯的看法。