Ⅰ、緩沖池介紹
innodb存儲引擎緩沖池(buffer pool) ,類似於oracle的sga,里面放着數據頁 、索引頁 、change buffer 、自適應哈希 、鎖(5.5之前)等內容
綜上所示:
- 每次讀寫數據都是通過Buffer Pool
- 當Buffer Pool中沒有用戶所需要的數據時才去硬盤中獲取
- 通過innodb_buffer_pool_size進行設置總容量,該值設置的越大越好
Ⅱ、緩沖池性能問題
2.1 性能線性擴展
假設服務器72核,ht超線程后,144個邏輯核,跑測試按道理144個核應該跑滿,如果跑不滿,就說明並發有瓶頸,我加核了,卻用不上,性能上不去
5.1之前這個問題經常被吐槽,現在不存在這個問題了
1G空間下面如果有65536個page,對這些page進行管理,每次都要對bp加鎖(latch),如果bp大了,就有瓶頸,這里說的鎖是bp的latch,和數據庫的lock不是一回事
qps達到1w,每秒鍾要獲得至少1w次latch(就看bp的latch,不談釋放和喚醒latch),開銷比較大
核比較多,latch或者並發設計的不好,性能則不能線性擴展 ,而這個bp對於擴展性非常重要,所有的熱點的page都在里面,每次訪問這些page都要獲得bp的latch
2.2 如何提升上述緩沖池性能問題
調整innodb_buffer_pool_instances參數,設置為cpu的數量
默認5.5為1,5.6和5.7是8
假設開始這個值是1,現調整為4,原來1個bp管理65536個頁,現在4個bp,每個bp管理16384個頁,拆成4個分片,將熱點打散,latch變少了,並發性能提升了,這是非常常見的內核層對並發調優的手段,經測試,不調整與調整后性能相差30%
tips:
設置多個緩沖池的時候,必須滿足每個池子大於1G才生效,否則,即使my.cnf中設置了innodb_buffer_pool_instances,重啟看看是沒用的
Ⅲ、buffer pool中熱點數據的管理
3.1 buffer pool的組成
緩沖池中的熱點是以page為單位來管理,並不是三種List加起來等於總的bp大小,而是Free List + LRU List(Flush List是包含在LRU list里面的)
- Free List 放空白的page
buffer Pool剛啟動時,有一個個16K的空白的頁,這些頁就存放(鏈表串聯)在Free List中
- LRU List 包括LRU和unzip_LRU
當讀取一個數據頁的時候,就從Free List中取出一個頁,存入數據,並將該頁讀到LRU List中
當Free List給一個頁給LRU List時,這個過程中需要一個並發控制,也就是之前說的latch,假設現在有兩個線程都讀到磁盤上這個頁,則都需要問Free List來申請空閑頁,誰先來先給誰,latch就是對這三個List進行並發控制訪問的
- Flush List包含臟頁(數據經過修改,但是未刷入磁盤的頁),根據oldest_lsn進行排序
假設被讀到的頁,馬上被更新,這個頁就叫臟頁,會被放入到Flush List列表中,但只是放了一個指針,而不是實際的頁(只要修改過,就放入,不管修改幾次)
如何查看緩沖池中的臟頁?
SELECT pool_id, lru_position, space, page_number, table_name, oldest_modification, newest_modification FROM information_schema.INNODB_BUFFER_PAGE_LRU WHERE oldest_modification <> 0 AND oldest_modification <> newest_modification; 結果集為空,則表示沒有臟頁,線上小心,不要亂執行,此sql消耗比較大
tips:
Flush list 中存放的不是一個頁,而是頁的指針(page number)
小結:
LRU List存放的是所有已經使用的頁,里面既有干凈頁也有臟頁,Flush List中只有指向臟頁的指針
3.2 查看buffer pool的狀態
方法1:show engine innodb status\G
...
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992 Dictionary memory allocated 303387 Buffer pool size 8192 #緩沖池中共8192個page Free buffers 7772 #空白頁(Free List),線上很可能是0 Database pages 420 #在使用的頁(LRU List) Old database pages 0 #LRU中教冷的page Modified db pages 0 #臟頁 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young 0, not young 0 0.00 youngs/s, 0.00 non-youngs/s #youngs表示old變為new Pages read 368, created 52, written 322 0.00 reads/s, 0.00 creates/s, 0.00 writes/s No buffer pool page gets since the last printout Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 420, unzip_LRU len: 0 I/O sum[0]:cur[0], unzip sum[0]:cur[0] ... 如果設置了多個buffer pool 找到individual buffer pool info看每一個bp的情況
方法2:看兩張元數據表
先說下,這東西比較大,看起來不是很方便,不太推薦
root@localhost) [(none)]> SELECT
-> pool_id, -> pool_size, -> free_buffers, -> database_pages, -> old_database_pages, -> modified_database_pages -> FROM -> information_schema.innodb_buffer_pool_stats\G *************************** 1. row *************************** pool_id: 0 pool_size: 8192 free_buffers: 7772 database_pages: 420 old_database_pages: 0 modified_database_pages: 0 1 row in set (0.00 sec) (root@localhost) [(none)]> SELECT -> space, page_number, newest_modification, oldest_modification -> FROM -> information_schema.innodb_buffer_page_lru -> LIMIT 1\G *************************** 1. row *************************** space: 0 page_number: 7 newest_modification: 5330181742 #該頁最近一次(最新)被修改的LSN值 oldest_modification: 0 #該頁在Buffer Pool中第一次被修改的LSN值,FLush List是根據該值進行排序的,該值越小,表示該頁應該最先被刷新 1 row in set (0.01 sec)
3.2 LRU算法解析
MySQL中使用了midpoint LRU算法來管理LRU List
- 當該頁被第一次讀取時,將該頁先放在mid point的位置(因為無法保證一定是活躍)
- 當被讀到第二次時才將改頁放入到new page的首部
- innodb_old_blocks_pct參數控制mid point的位置,默認是37,即3/8的位置
3.3 緩沖池防污染
有一種場景,某個page一下子被掃了n次,但其實他並不是熱頁,這時候如果按照之前說的,這個page會被放到new里面去,這其實就污染了緩沖池
那什么時候會出現一個page每秒被讀n次呢?
scan的時候,select * from tb_name;如果這個page里有10條記錄,這個page就會被讀10次
我們可以通過將一個page固定在midpoint位置一定的時間來解決這個問題
set global innodb_old_blocks_time=1; 通常 select * 掃描操作不會高於1秒,一個頁很快就被掃完了
無論讀多少次,在innodb_old_blocks_time的時間內都不管(都視作只讀取了一次),等這個時間過去了(時間到),如果該頁還是被讀取了,才把這個頁放到new page的首部,如果設為0,則表示讀到第二次就放到new里去
如果開發有個scan操作,就需要設置一下,操作完后再改回來。最好的方案是放到從機上,避免掃描語句污染LRU
tips:
①如果一個page中10條記錄一次讀,讀這十條記錄的時候這個頁就會被鎖成只讀,那其他線程對這個頁的操作就不被允許了,數據庫是一個並發系統,這是不合理的,這樣讀一個頁hold住鎖的時間會長,所以是每讀一條記錄去讀一次頁,然后馬上釋放,把讀到的位置————游標(這個游標和數據庫的游標不是一回事)保存下來,下次再要讀的時候,從打開這個游標繼續讀,但是位置可能會變化,所以會重新去讀這個頁,以此確保各個線程公平調度
②myisam緩存data是交給操作系統緩存 ,和pg一樣
3.4 buffer pool的預熱
背景:
在MySQL啟動后(MySQL5.6之前),Buffer Pool中頁的數據是空的,需要大量的時間才能把磁盤中的頁讀入到內存中,導致啟動后的一段時間性能很差
例:啟動的時候load
64GB BP 10M/s讀取 100min
預熱策略:將LRU列表dump出來,通過較順序讀取的方式預熱50M~200M
預熱方法:
select count(1) from table force index(primary)
select count(1) from index
說明:
上面兩種方法很痤。並沒有預熱真正的熱點數據,只是把數據讀進來了,粒度非常粗,比如你數據100G,bp10G,那真正的熱點很大部分不是熱點數據
網易試過共享內存來做,數據庫重啟bp不清,不過操作系統重啟了也就白搭了
好辦法:
MySQL5.6 開始有辦法了
(root@172.16.0.10) [(none)]> show variables like 'innodb_buffer_pool%'; +-------------------------------------+----------------+ | Variable_name | Value | +-------------------------------------+----------------+ | innodb_buffer_pool_chunk_size | 134217728 | | innodb_buffer_pool_dump_at_shutdown | ON | #在停機時dump出buffer pool中的(space,page) | innodb_buffer_pool_dump_now | OFF | #set一下,表示現在就從buffer pool中dump | innodb_buffer_pool_dump_pct | 25 | #dump的bp的前百分之多少,是每個buffer pool最近使用的頁數,而不是整體,可寫到[mysqld-5.7]中 | innodb_buffer_pool_filename | ib_buffer_pool | #dump出的文件的名字 | innodb_buffer_pool_instances | 1 | | innodb_buffer_pool_load_abort | OFF | | innodb_buffer_pool_load_at_startup | ON | #啟動時加載dump的文件,恢復到buffer pool中 | innodb_buffer_pool_load_now | OFF | #set一下,表示現在加載 dump的文件 | innodb_buffer_pool_size | 1879048192 | +-------------------------------------+----------------+ 10 rows in set (0.00 sec)
- 關閉數據庫之前把bp中的space和page_no給dump出來(不是整個bp,5.6還沒正式發布的時候就是dump所有)
- 重啟的時候會把dump出來的內容load進bp,dump出來是無序的,load之前根據space和pageno進行排序,load是異步的,返回速度還可以,對bp基本沒影響
- dump的越多,啟動的越慢
- 頻繁dump會導致Buffer Pool中的數據越來越少,是因為設置了innodb_buffer_pool_dump_pct,默認25,姜總用的40
- 如果做了高可用,可以定期dump,然后將該dump的文件傳送到slave上,然后直接load(slave上的(Space,Page)和Master上的 大致相同 )
簡單演示一把:
(root@localhost) [(none)]> set global innodb_buffer_pool_dump_now = 1; Query OK, 0 rows affected (0.00 sec) (root@localhost) [(none)]> show status like 'Innodb_buffer_pool_dump_status'; +--------------------------------+--------------------------------------------------+ | Variable_name | Value | +--------------------------------+--------------------------------------------------+ | Innodb_buffer_pool_dump_status | Buffer pool(s) dump completed at 180302 16:57:45 | +--------------------------------+--------------------------------------------------+ 1 row in set (0.00 sec) 進入數據目錄 [root@VM_0_5_centos data3306]# ll *pool -rw-r----- 1 mysql mysql 604 Mar 2 16:59 ib_buffer_pool [root@VM_0_5_centos data3306]# head ib_buffer_pool 0,568 0,567 0,566 0,565 0,278 0,564 0,563 0,562 164,3 164,2 停止服務 [root@VM_0_5_centos data3306]# mysqld_multi stop 3306 截取錯誤日志 2018-03-02T09:01:10.292549Z 0 [Note] InnoDB: Starting shutdown... 2018-03-02T09:01:10.392851Z 0 [Note] InnoDB: Dumping buffer pool(s) to /mdata/data3306/ib_buffer_pool 2018-03-02T09:01:10.393059Z 0 [Note] InnoDB: Buffer pool(s) dump completed at 180302 17:01:10 啟動服務,加載熱數據 [root@VM_0_5_centos data3306]# mysqld_multi start 3306 (root@localhost) [(none)]> set global innodb_buffer_pool_load_now = 1; Query OK, 0 rows affected (0.00 sec) 再截取錯誤日志 2018-03-02T09:06:40.526294Z 0 [Note] InnoDB: Loading buffer pool(s) from /mdata/data3306/ib_buffer_pool 2018-03-02T09:06:40.526487Z 0 [Note] InnoDB: Buffer pool(s) load completed at 180302 17:06:40
tips:
注意一下innodb_buffer_pool_dump_pct這個參數,先看下下面這個流程
(root@localhost) [(none)]> set global innodb_buffer_pool_dump_pct=100; Query OK, 0 rows affected (0.00 sec) (root@localhost) [(none)]> set global innodb_buffer_pool_dump_now = 1; Query OK, 0 rows affected (0.00 sec) [root@VM_0_5_centos data3306]# cat ib_buffer_pool |wc -l 576 (root@localhost) [(none)]> set global innodb_buffer_pool_dump_pct=20; Query OK, 0 rows affected (0.00 sec) (root@localhost) [(none)]> set global innodb_buffer_pool_dump_now = 1; Query OK, 0 rows affected (0.00 sec) [root@VM_0_5_centos data3306]# cat ib_buffer_pool |wc -l 115
看上去沒啥問題,但要注意的是,當你有多個緩沖池的時候,比如有4個,每個里面有100個page,它不是整體來dump前百分之25,而是dump每個緩沖池里面最前面的15個page
Ⅳ、異步讀
發現全表掃描,如果已經掃了一部分內容,innodb會異步讀取這部分內容后面的一部分,即使你沒讀到,異步讀有兩種情況,如下:
隨機預讀
innodb_random_read_ahead
線性預讀
innodb_read_ahead_threshold 該參數目前缺省值為0
- 線性預讀放到以extent為單位,而隨機預讀放到以extent中的page為單位
- 線性預讀是將下一個extent提前讀取到buffer pool中,隨機預讀是將當前extent中的剩余的page提前讀取到buffer pool中