平時的工做中,不知道有沒有遇到過這樣的場景,一條SQL語句,正常執行的時候特別快,可是有時也不知道怎麼回事,它就會變得特別慢,而且這樣的場景很難復現,它不僅隨機,並且持續時間還很短。mysql
如今你知道了,InnoDB在處理更新語句的時候,只作了寫日誌這一個磁盤操做。這個日誌叫做redo log(重作日誌),也就是《孔乙己》裏咸亨酒店掌櫃用來記帳的粉板,在更新內存寫完redo log後,就返回給客戶端,本次更新成功。算法
作下類比的話,掌櫃記帳的帳本是數據文件,記帳用的粉板是日誌文件(redo log),掌櫃的記憶就是內存。sql
掌櫃總要找時間把帳本更新一下,這對應的就是把內存裏的數據寫入磁盤的過程,術語就是flush。在這個flush操做執行以前,孔乙己的賒帳總額,其實跟掌櫃手中帳本里面的記錄是不一致的。由於孔乙己今天的賒帳金額還只在粉板上,而帳本里的記錄是老的,還沒把今天的賒帳算進去。數據庫
當內存數據頁跟磁盤數據頁內容不一致的時候,咱們稱這個內存頁爲「髒頁」。內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲「乾淨頁」。工具
不管是髒頁仍是乾淨頁,都在內存中。在這個例子裏,內存對應的就是掌櫃的記憶。性能
平時執行很快的更新操做,其實就是在寫內存和日誌,而MySQL偶爾「抖」一下的那個瞬間,可能就是在刷髒頁(flush)。測試
什麼狀況纔會引起數據庫的flush的過程呢優化
仍是繼續使用孔乙己酒店掌櫃的例子,掌櫃在什麼狀況下會把粉板上的賒帳記錄改到帳本上?線程
第一種場景是,粉板滿了,記不下了。這時候若是再有人來賒帳,掌櫃就只得放下手裏的活兒,將粉板上的記錄擦掉一些,留出空位以便繼續記帳。固然在擦掉以前,他必須先將正確的帳目記錄到帳本中才行。3d
這個場景,對應的就是InnoDB的redo log寫滿了。這時候系統會中止全部更新操做,把checkpoint往前推動,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的策略是儘可能使用內存,所以對於一個長時間運行的庫來講,未被使用的頁面不多。
而當要讀入的數據頁沒有在內存的時候,就必須到緩衝池中申請一個數據頁。這時候只能把最久不使用的數據頁從內存中淘汰掉:若是要淘汰的是一個乾淨頁,就直接釋放出來複用;但若是是髒頁呢,就必須將髒頁先刷到磁盤,變成乾淨頁後才能複用。
因此,刷髒頁雖然是常態,可是出現如下這兩種狀況,都是會明顯影響性能的:
一個查詢要淘汰的髒頁個數太多,會致使查詢的響應時間明顯變長;
日誌寫滿,更新所有堵住,寫性能跌爲0,這種狀況對敏感業務來講,是不能接受的。
因此,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怎麼控制引擎按照「全力」的百分比來刷髒頁。
考慮刷髒頁的速度,參考的因素。
主要是有兩個因素:一個是髒頁比例,一個是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%來控制刷髒頁的速度。
如今你知道了,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了。
利用WAL技術,數據庫將隨機寫轉換成了順序寫,大大提高了數據庫的性能。
可是,由此也帶來了內存髒頁的問題。髒頁會被後臺線程自動flush,也會因爲數據頁淘汰而觸發flush,而刷髒頁的過程因爲會佔用資源,可能會讓你的更新和查詢語句的響應時間長一些。在文章裏,我也給你介紹了控制刷髒頁的方法和對應的監控方式。