平時的工作中,不知道你有沒有遇到過這樣的場景,一條 SQL 語句,正常執行的時候特別快,但是有時也不知道怎么回事,它就會變得特別慢,並且這樣的場景很難復現,它不只隨機,而且持續時間還很短。
當內存數據頁跟磁盤數據頁內容不一致的時候,我們稱這個內存頁為“臟頁”。內存數據寫入到磁盤后,內存和磁盤上的數據頁的內容就一致了,稱為“干凈頁”。
平時執行很快的更新操作,其實就是在寫內存和日志,而 MySQL 偶爾“抖”一下的那個瞬間,可能就是在刷臟頁(flush)。
那么,什么情況會引發數據庫的 flush 過程呢?
第一種場景是InnoDB 的 redo log 寫滿了,這時候系統會停止所有更新操作,把 checkpoint 往前推進,redo log 留出空間可以繼續寫。
第二種場景對應的就是系統內存不足。當需要新的內存頁,而內存不夠用的時候,就要淘汰一些數據頁,空出內存給別的數據頁使用。如果淘汰的是“臟頁”,就要先將臟頁寫到磁盤。
第三種場景對應的就是 MySQL 認為系統“空閑”的時候。當然,MySQL忙起來可是會很快就能把redo log 記滿的,所以要合理地安排時間,即使是忙的時候,也要見縫插針地找時間,只要有機會就刷一點“臟頁”。
第四種場景對應的就是 MySQL 正常關閉的情況。這時候,MySQL 會把內存的臟頁都 flush 到磁盤上,這樣下次 MySQL 啟動的時候,就可以直接從磁盤上讀數據,啟動速度會很快。
接下來,你可以分析一下上面四種場景對性能的影響。
其中,第三種情況是屬於 MySQL 空閑時的操作,這時系統沒什么壓力,而第四種場景是數據庫本來就要關閉了。這兩種情況下,你不會太關注“性能”問題。所以這里,我們主要來分析一下前兩種場景下的性能問題。
第一種是“redo log 寫滿了,要 flush 臟頁”,這種情況是 InnoDB 要盡量避免的。因為出現這種情況的時候,整個系統就不能再接受更新了,所有的更新都必須堵住。如果你從監控上看,這時候更新數會跌為 0。
第二種是“內存不夠用了,要先將臟頁寫到磁盤”,這種情況其實是常態。InnoDB 用緩沖池(buffer pool)管理內存,緩沖池中的內存頁有三種狀態:
- 第一種是,還沒有使用的;
- 第二種是,使用了並且是干凈頁;
- 第三種是,使用了並且是臟頁。
InnoDB 的策略是盡量使用內存,因此對於一個長時間運行的庫來說,未被使用的頁面很少。
而當要讀入的數據頁沒有在內存的時候,就必須到緩沖池中申請一個數據頁。這時候只能把最久不使用的數據頁從內存中淘汰掉:如果要淘汰的是一個干凈頁,就直接釋放出來復用;但如果是臟頁呢,就必須將臟頁先刷到磁盤,變成干凈頁后才能復用。
所以,刷臟頁雖然是常態,但是出現以下這兩種情況,都是會明顯影響性能的:
-
一個查詢要淘汰的臟頁個數太多,會導致查詢的響應時間明顯變長;
-
日志寫滿,更新全部堵住,寫性能跌為 0,這種情況對敏感業務來說,是不能接受的。
所以,InnoDB 需要有控制臟頁比例的機制,來盡量避免上面的這兩種情況。
InnoDB 刷臟頁的控制策略
首先,你要正確地告訴 InnoDB 所在主機的 IO 能力,這樣 InnoDB 才能知道需要全力刷臟頁的時候,可以刷多快。
這就要用到 innodb_io_capacity 這個參數了,它會告訴 InnoDB 你的磁盤能力。
現在你知道了,InnoDB 會在后台刷臟頁,而刷臟頁的過程是要將內存頁寫入磁盤。所以,無論是你的查詢語句在需要內存的時候可能要求淘汰一個臟頁,還是由於刷臟頁的邏輯會占用 IO 資源並可能影響到了你的更新語句,都可能是造成你從業務端感知到 MySQL“抖”了一下的原因。
要盡量避免這種情況,你就要合理地設置 innodb_io_capacity 的值,並且平時要多關注臟頁比例(參數 innodb_max_dirty_pages_pct),不要讓它經常接近 75%。