平時的工做中,不知道你有沒有遇到過這樣的場景,一條SQL語句,正常執行的時候特別快,可是有時也不知道怎麼回事,它就會變得特別慢,而且這樣的場景很難復現,它不僅隨機,並且持續時間還很短。mysql
看上去,這就像是數據庫「抖」了一下。今天,咱們就一塊兒來看一看這是什麼緣由。算法
在前面第2篇文章《日誌系統:一條SQL更新語句是如何執行的?》中,我爲你介紹了WAL機制。如今你知道了,InnoDB在處理更新語句的時候,只作了寫日誌這一個磁盤操做。這個日誌叫做redo log(重作日誌),也就是《孔乙己》裏咸亨酒店掌櫃用來記帳的粉板,在更新內存寫完redo log後,就返回給客戶端,本次更新成功。sql
作下類比的話,掌櫃記帳的帳本是數據文件,記帳用的粉板是日誌文件(redo log),掌櫃的記憶就是內存。數據庫
掌櫃總要找時間把帳本更新一下,這對應的就是把內存裏的數據寫入磁盤的過程,術語就是flush。在這個flush操做執行以前,孔乙己的賒帳總額,其實跟掌櫃手中帳本里面的記錄是不一致的。由於孔乙己今天的賒帳金額還只在粉板上,而帳本里的記錄是老的,還沒把今天的賒帳算進去。工具
當內存數據頁跟磁盤數據頁內容不一致的時候,咱們稱這個內存頁爲「髒頁」。內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲「乾淨頁」。性能
不管是髒頁仍是乾淨頁,都在內存中。在這個例子裏,內存對應的就是掌櫃的記憶。測試
接下來,咱們用一個示意圖來展現一下「孔乙己賒帳」的整個操做過程。假設原來孔乙己欠帳10文,此次又要賒9文。優化
回到文章開頭的問題,你不難想象,平時執行很快的更新操做,其實就是在寫內存和日誌,而MySQL偶爾「抖」一下的那個瞬間,可能就是在刷髒頁(flush)。spa
那麼,什麼狀況會引起數據庫的flush過程呢?線程
咱們仍是繼續用咸亨酒店掌櫃的這個例子,想想:掌櫃在什麼狀況下會把粉板上的賒帳記錄改到帳本上?
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髒頁的控制策略,以及和這些策略相關的參數。
首先,你要正確地告訴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的刷盤速度就是要參考這兩個因素:一個是髒頁比例,一個是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寫入速度算出來的兩個值。
如今你知道了,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,只是它用了最簡單的轉換規則:字符串轉數字的規則,而恰好咱們設定的這個背景,能夠保證這個轉換後結果的惟一性。
評論區中,也有其餘一些很不錯的看法。
評論用戶@封建的風 說,一個學校的總人數這種數據量,50年才100萬學生,這個表確定是小表。爲了業務簡單,直接存原來的字符串。這個答覆裏面包含了「優化成本和收益」的思想,我以爲值得at出來。
@小潘 同窗提了另一個極致的方向。若是碰到表數據量特別大的場景,經過這種方式的收益是很不錯的。