你的 SQL 語句為什么變“慢”了
平時執行很快的更新操作,其實就是在寫內存和日志,而 MySQL 偶爾“抖”一下的那個瞬間,可能就是在刷臟頁(flush)。
當內存數據頁跟磁盤數據頁內容不一致的時候,我們稱這個內存頁為“臟頁”。內存數據寫入到磁盤后,內存和磁盤上的數據頁的內容就一致了,稱為“干凈頁”
什么情況會引發數據庫的 flush 過程呢?
InnoDB 的 redo log 寫滿了
這時候系統會停止所有更新操作,把 checkpoint 往前推進,redo log 留出空間可以繼續寫,checkpoint 往前推進時,會把對應的所有臟頁都 flush 到磁盤上
系統內存不足
當需要新的內存頁,而內存不夠用的時候,就要淘汰一些數據頁,空出內存給別的數據頁使用。如果淘汰的是“臟頁”,就要先將臟頁寫到磁盤。
刷臟頁一定會寫盤,就保證了每個數據頁有兩種狀態:
a. 一種是內存里存在,內存里就肯定是正確的結果,直接返回;
b.另一種是內存里沒有數據,就可以肯定數據文件上是正確的結果,讀入內存后返回。這樣的效率最高。
MySQL 認為系統“空閑”的時候
MySQL 正常關閉的情況
MySQL 會把內存的臟頁都 flush 到磁盤上,這樣下次 MySQL 啟動的時候,就可以直接從磁盤上讀數據,啟動速度會很快
四種場景對性能的影響
“redo log 寫滿了,要 flush 臟頁”
這種情況是 InnoDB 要盡量避免的。因為出現這種情況的時候,整個系統就不能再接受更新了,所有的更新都必須堵住。如果你從監控上看,這時候更新數會跌為 0。
“內存不夠用了,要先將臟頁寫到磁盤”
這種情況其實是常態。InnoDB 用緩沖池(buffer pool)管理內存,緩沖池中的內存頁有三種狀態:
a. 第一種是,還沒有使用的;
b. 第二種是,使用了并且是干凈頁;
c. 第三種是,使用了并且是臟頁。
InnoDB 的策略是盡量使用內存,因此對于一個長時間運行的庫來說,未被使用的頁面很少。而當要讀入的數據頁沒有在內存的時候,就必須到緩沖池中申請一個數據頁。這時候只能把最久不使用的數據頁從內存中淘汰掉:如果要淘汰的是一個干凈頁,就直接釋放出來復用;但如果是臟頁呢,就必須將臟頁先刷到磁盤,變成干凈頁后才能復用。
所以,刷臟頁雖然是常態,但是出現以下這兩種情況,都是會明顯影響性能的:
a. 一個查詢要淘汰的臟頁個數太多,會導致查詢的響應時間明顯變長;
b. 日志寫滿,更新全部堵住,寫性能跌為 0,這種情況對敏感業務來說,是不能接受的。
所以,InnoDB 需要有控制臟頁比例的機制,來盡量避免上面的這兩種情況。
InnoDB 刷臟頁的控制策略
innodb_io_capacity 參數會告訴 InnoDB 你的磁盤能力,這樣 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_max_dirty_pages_pct 參數是臟頁比例上限,默認值是 75%。InnoDB 會根據當前的臟頁比例(假設為 M),算出一個范圍在 0 到 100 之間的數字 F1(M) ,InnoDB 每次寫入的日志都有一個序號,當前寫入的序號跟 checkpoint 對應的序號之間的差值,我們假設為 N。InnoDB 會根據這個 N 算出一個范圍在 0 到 100 之間的數字 F2(N),根據上述算得的 F1(M) 和 F2(N) 兩個值,取其中較大的值記為 R,之后引擎就可以按照 innodb_io_capacity 定義的能力乘以 R% 來控制刷臟頁的速度。
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;
MySQL 在準備刷一個臟頁的時候,如果這個數據頁旁邊的數據頁剛好是臟頁,就會把這個“鄰居”也帶著一起刷掉;而且這個把“鄰居”拖下水的邏輯還可以繼續蔓延,也就是對于每個鄰居數據頁,如果跟它相鄰的數據頁也還是臟頁的話,也會被放到一起刷。
innodb_flush_neighbors 參數就是用來控制這個行為的,值為 1 的時候會有上述的“連坐”機制,值為 0 時表示不找鄰居,自己刷自己的
找“鄰居”這個優化在機械硬盤時代是很有意義的,可以減少很多隨機 IO。機械硬盤的隨機 IOPS 一般只有幾百,相同的邏輯操作減少隨機 IO 就意味著系統性能的大幅度提升。
而如果使用的是 SSD 這類 IOPS 比較高的設備的話,我就建議你把 innodb_flush_neighbors 的值設置成 0。因為這時候 IOPS 往往不是瓶頸,而“只刷自己”,就能更快地執行完必要的刷臟頁操作,減少 SQL 語句響應時間。
在 MySQL 8.0 中,innodb_flush_neighbors 參數的默認值已經是 0 了。