Mysql Buffer Pool

Buffer Pool

簡介:Innodb維護了一個緩存區域叫做Buffer Pool,用來緩存數據和索引在內存中。Buffer Pool可以用來加速數據的讀寫,如果Buffer Pool越大,那麼Mysql就越像一個內存數據庫,所以瞭解Buffer Pool的配置可以提高Buffer Pool的性能。

buffer pool的配置

innodb_buffer_pool_size:緩存區域的大小。
innodb_buffer_pool_chunk_size:當增加或減少innodb_buffer_pool_size時,操作以塊(chunk)形式執行。塊大小由innodb_buffer_pool_chunk_size配置選項定義,默認值128M。
innodb_buffer_pool_instances:當buffer pool比較大的時候(超過1G),innodb會把buffer pool劃分成幾個instances,這樣可以提高讀寫操作的併發,減少競爭。讀寫page都使用hash函數分配給一個instances。
當增加或者減少buffer pool大小的時候,實際上是操作的chunk。buffer pool的大小必須是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances,如果配置的innodb_buffer_pool_size不是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍數,buffer pool的大小會自動調整爲innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍數,自動調整的值不少於指定的值。
如果指定的buffer大小是9G,instances的個數是16,chunk默認的大小是128M,那麼buffer會自動調整爲10G。具體的配置可以參考mysql官網的介紹mysql reference
這裏寫圖片描述

LRU算法

爲了管理這些數據,innodb使用了一些鏈表。
lru鏈表:用來存儲內存中的緩存數據。
free鏈表:用來存放所有的空閒頁,每次需要數據頁存儲數據時,就首先檢測free中有沒有空閒的頁來分配。
flush鏈表:在內存中被修改但還沒有刷新到磁盤的數據頁列表,就是所謂的髒頁列表,內存中的數據跟對應的磁盤上的數據不一致,屬於該列表的頁面同樣存在於lru列表中,但反之未必。

防止緩存污染

思考一個問題,如果msyql做一次全表掃描,那麼全表掃面的數據就會放到buffer pool中,而且全表掃描的數據大部分都是用不到的,那麼之前的熱點數據也就被沖掉了。所以innodb有一些策略來防止緩存污染。
在Buffer Pool中,存儲數據的最小單位是頁,默認是16K,使用LRU算法的變體來進行頁數據的淘汰和置換。Buffer Pool把LRU鏈表分爲兩個部分,一個部分叫做頭部鏈表用來存儲熱點數據,一個部分叫做尾部鏈表,用來存儲即將淘汰的數據。頭部鏈表和尾部鏈表有一個分界點,默認是3/8,就是有3/8的空間用來存儲old頁面。在innodb中有個參數是innodb_old_blocks_pct,默認是37大概就是3/8,通過配置這個參數可以選擇頭部鏈表和尾部鏈表佔用的空間比例,innodb_old_blocks_pct的可配置的範圍是從5-95,值越大說明尾部鏈表佔用的空間越大,也就越接近LRU算法。當mysql從磁盤往緩存區存數據的時候,都會先把數據存儲在尾部鏈表,這樣一來,即使有全表掃描,那麼全表掃描的數據也只能進入尾部鏈表中,不會影響頭部鏈表的數據。
在innodb中還有一個參數也是用來防止緩存污染的,就是innodb_old_blocks_time。這個參數的默認值是1000ms,意思是,在把數據讀入尾部鏈表的1000ms之內,再次訪問相同的數據,這個數據頁不會進入到頭部鏈表。這個值越大,那麼數據進入頭部鏈表的機會就越少,那麼數據被淘汰的概率就越大。

預讀操作

預讀是mysql提高性能的一個重要的特性。預讀是指,在獲取一個頁面的數據時,在不久的時間裏面也會用到存儲數據頁面的後面的頁面(page)或者塊(extend)。在mysql中預讀有兩種。

Linear線性預讀

線性預讀的單位是extend,一個extend中有64個page。線性預讀的一個重要參數是innodb_read_ahead_threshold,是指在連續訪問多少個頁面之後,把下一個extend讀入到buffer pool中,不過預讀是一個異步的操作。當然這個參數不能超過64,因爲一個extend最多隻有64個頁面。
例如,innodb_read_ahead_threshold = 56,就是指在連續訪問了一個extend的56個頁面之後把下一個extend讀入到buffer pool中。在添加此參數之前,InnoDB僅計算當它在當前範圍的最後一頁中讀取時是否爲整個下一個範圍發出異步預取請求。

Random隨機預讀

隨機預讀方式則是表示當同一個extent中的一些page在buffer pool中發現時,Innodb會將該extent中的剩餘page一併讀到buffer pool中。由於隨機預讀方式給innodb code帶來了一些不必要的複雜性,同時在性能也存在不穩定性,在5.5中已經將這種預讀方式廢棄,默認是OFF。若要啓用此功能,即將配置變量設置innodb_random_read_ahead爲ON。

數據頁訪問流程

  1. 當訪問的頁面在緩存池在命中的話,直接返回該頁。爲了避免掃描LRU,innodb爲每個instances維護了一個page hash,通過space id和page no可以直接找到對應的page。一般情況下,當我餓你需要讀入一個Page時,首先根據space id和page no找到對應的instances,然後再查詢page hash,如果page hash中沒有,則需要從磁盤中讀取。
  2. 如果沒有命中,則需要把頁面從磁盤加載到緩存池中,因此需要在緩存池中找到一個空閒的內存塊來緩存這個頁面。
  3. 如果空閒內存被使用完,也就是free鏈表上沒有內存塊了。則需要在生產一個空閒的內存塊。
  4. 首先去LRU列表中找可以替換的內存頁面,查找的方向是從列表的尾部開始找,如果找到可以替換的頁面,將其從LRU列表中摘除,加入空閒列表,然後再去空閒列表中找空閒的內存塊。第一查找最多值掃描100個頁面,循環進行到第二次時,會掃描整個LRU列表。
  5. 如果在LRU列表中沒有找到可以替換的頁,則進行單頁刷新,將髒頁刷新到磁盤之後,然後將釋放的內存塊加入到空閒列表。然後再去空閒列表中取。爲什麼只做單頁刷新呢?因爲這個函數的目的是獲取空閒內存頁,進行髒頁刷新是不得已而爲之,所以只會進行一個頁面的刷新,目的是爲了儘快的獲取空閒內存塊。

通過數據頁訪問機制,可以知道其中當無空閒頁時產生空閒頁就成爲一個必須要做的事情了。如果需要刷新髒頁來產生空閒頁面或者需要掃描整個LRU列表來產生空閒頁面的時候,查找空閒內存塊的時間就會延長,這個是一個bad case,是我們希望儘量避免的。因此,innodb buffer pool中存在大量可以替換的頁面,或者free列表中一直存在着空閒內存塊,對快速獲取到空閒內存塊起決定性的作用。

緩存池刷新策略

InnoDB會在後臺執行某些任務,包括從緩衝池刷新髒頁(那些已更改但尚未寫入數據庫文件的頁)。

當啓用innodb_max_dirty_pages_pct_lwm(默認值0)參數時,表示啓用了髒頁面預刷新行爲,以控制髒頁面佔比。也是爲了防止髒頁佔有率超過innodb_max_dirty_pages_pct(默認值75%)的設定值。默認禁用“預刷新”行爲。如果當髒頁的佔有率達到了innodb_max_dirty_pages_pct的設定值的時候,InnoDB就會強制刷新buffer pool pages。另外當free列表小於innodb_lru_scan_depth值時也會觸發刷新機制,innodb_lru_scan_depth控制LRU列表中可用頁的數量,該值默認爲1024。

後臺刷新的動作由後臺刷新協調線程觸發,該線程的所有工作內容均由buf_flush_page_cleaner_coordinator函數完成,我們後面簡稱它爲協調函數。接下來,來看後臺刷新協調函數的主體流程。

  1. 調用page_cleaner_flush_pages_recommendation建議函數,對每個緩衝池實例生成髒頁刷新數量的建議。在執行刷新之前,會用建議函數生成每個buffer pool需要刷新多少個髒頁的建議。

  2. 生成刷新建議之後,通過設置事件的方式,向刷新線程(Page Cleaner線程)發出刷新請求。後臺刷新線程在收到請求刷新的事件後,會執行pc_flush_slot函數對某個緩存池進行刷新,刷新的過程首先是對lru列表進行刷新,執行的函數爲buf_flush_LRU_list,完成LRU列表的刷新之後,就會根據建議函數生成的建議對髒頁列表進行刷新,執行的函數爲buf_flush_do_batch。

  3. 後臺刷新的協調線程會作爲刷新調度總負責人的角色,它會確保每個buffer pool都已經開始執行刷新。如果哪個buffer pool的刷新請求還沒有被處理,則由刷新協調線程親自刷新,且直到所有的buffer pool instance都已開始/進行了刷新,才退出這個while循環。

  4. 當所有的buffer pool instance的刷新請求都已經開始處理之後,協調函數(或協調線程)就等待所有buffer pool instance的刷新的完成,等待函數爲pc_wait_finished。如果這次刷新的總耗時超過4000ms,下次循環之前,會在數據庫的錯誤日誌記錄相關的超時信息。它期望每秒鐘對buffer pool進行一次刷新調度。如果相鄰兩次刷新調度的間隔超過4000ms ,也就是4秒鐘,MySQL的錯誤日誌中會記錄相關信息,意思就是“本來預計1000ms的循環花費了超過4000ms的時間。

前面我們反覆講到,每個buffer pool需要刷新多少頁面是由建議函數生成的,它在做刷新建議的時候,具體考慮了哪些因素?現在我們來詳細解析。

在講這段內容之前,我們先來了解兩個參數:innodb_io_capacity與innodb_io_capacity_max,這兩個參數大部分朋友都不陌生,設置這個參數的目的,是告訴MySQL數據庫,它所在服務器的磁盤的隨機IO能力。MySQL數據庫目前還沒有去自己評估服務器磁盤IO能力的功能,所以磁盤io能力大小由這個參數提供,以便讓數據庫知道磁盤的實際IO能力。這個參數將直接影響建議刷新的頁面的數量。

建議函數它會計算當前的髒頁刷新平均速度(也就是一秒鐘刷新了多少髒頁)以及重做日誌的生成平均速度。但這個函數並不是每次被調用時,都計算一次平均速度。它是多久計算一次的呢?這個是由數據庫參數innodb_flushing_avg_loops來決定的,默認是30,當這個函數被調用了30次之後或者經過30秒之後,重新計算一次平均值。我們暫且簡單理解爲30秒鐘。計算規則是當前的平均速度加上最近30秒鐘期間的平均速度再除以2得出新的平均速度。兩個平均值相加再平均,得出新的平均值。這樣的平均值能明顯的體現出最近30秒的速度的變化。

<參考>
http://www.ywnds.com/?p=9886
https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html#innodb-buffer-pool-lru

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章