前面不少大俠都分享過MySQL的InnoDB存儲引擎將數據刷新的各類狀況。咱們這篇文章從InnoDB往下,看看數據從InnoDB的內存到真正寫到存儲設備的介質上到底有哪些緩衝在起做用。php
咱們經過下圖看一下相關的緩衝:html
圖 1 innodb all buffersnode
從上圖中,咱們能夠看到,數據InnoDB到磁盤須要通過mysql
這裏咱們使用術語「緩衝」(通常爲buffer)來表示對數據寫的暫存,使用術語「緩存」(通常爲cache)來表示對數據讀的暫存。顧名思義,因爲底層存儲設備和內存之間速率的差別,緩衝是用來暫「緩」對底層存儲設備IO的「衝」擊。緩存主要是在內存中暫「存」從磁盤讀到的數據,以便接下來對這些數據的訪問不用再次訪問慢速的底層存儲設備。linux
buffer和cache的討論能夠參考彭立勳的:sql
http://www.penglixun.com/tech/system/buffer_and_cache_diff.html數據庫
下面咱們對這些緩衝自頂向下逐一進行詳細的介紹。windows
該層的緩衝都放在主機內存中,它的目的主要是在應用層管理本身的數據,避免慢速的讀寫操做影響了InnoDB的響應時間。緩存
InnoDB層主要包括兩個buffer:redo log buffer和innodb buffer pool。redo log buffer用來暫存對重作日誌redo log的日誌寫,InnoDB buffer pool存儲了從磁盤設備讀到的InnoDB數據,也緩衝了對InnoDB數據寫,即髒頁數據。若是主機掉電或者MySQL異常宕機,innodb buffer pool將沒法及時刷新到磁盤,那麼InnoDB就只能從上一個checkpoint使用redo log來前滾;而redo log buffer若是不能及時刷新到磁盤,那麼因爲redo log中數據的丟失,就算使用redo 前滾,用戶提交的事務因爲沒有真正的記錄到非易失型的磁盤介質中,就丟失掉了。安全
控制redo log buffer刷新時機的參數是innodb_flush_log_at_trx_commit,而控制redo log buffer和innodb buffer pool刷新方式的參數爲innodb_flush_method。針對這兩個參數詳細介紹的文章有很是多,咱們這裏主要從緩衝的角度來解析。
控制redo log buffer的innodb_flush_log_at_trx_commit目前支持3種不一樣的參數值0,1,2
圖 2 innodb_flush_log_at_trx_commit示意圖
這裏偷個懶,直接引用應元的圖。另外,更新一下innodb_flush_log_at_trx_commit=2時在5.6的變化:
< 5.6.6: 每隔一秒將redo log buffer中的數據刷新到磁盤
>= 5.6.6:每隔innodb_flush_log_at_timeout秒將數據刷新到磁盤中去。
咱們這裏再也不詳細討論這個問題,具體細節能夠參考MySQL數據丟失討論
控制innodb buffer pool的innodb_flush_method目前支持4種不一樣的參數值:
l fdatasync
l O_DSYNC
l O_DIRECT
l O_DIRECT_NO_FSYNC
這裏咱們注意到有幾個問題:
System Variable Name |
||
Variable Scope |
Global |
|
Dynamic Variable |
No |
|
|
Permitted Values (<= 5.6.6) |
|
Type (Linux) |
string |
|
Default |
fdatasync |
|
Valid Values |
O_DSYNC |
|
O_DIRECT |
表格 1 innodb_flush_method可選值
其實這裏是他故意的,由於fdatasync()和fsync()是不同的,就像O_DSYNC和O_SYNC的區別同樣。Fdatasync和O_DSYNC僅用於數據同步,fsync()和O_SYNC用於數據和元數據meta-data同步。可是MySQL用fdatasync參數值來指明「數據文件」和「日誌文件」是用fsync()打開的(注意:不是fdatasync()),這個是歷史緣由,因此5.6特地把它從可選值中去掉,避免誤解。固然你若是仍然要使用fsync()來同步,那就對innodb_flush_method什麼都不要指定就能夠了。
閒話少說,下面的一個表和一張圖可以更加直觀的說明問題:
從新加工了orczhou的刷新關係表:
Open log |
Flush log |
flush log |
Open datafile |
flush datafile |
fdatasync |
|
fsync() |
|
fsync() |
O_DSYNC |
O_SYNC |
|
|
fsync() |
O_DIRECT |
|
fsync() |
O_DIRECT |
fsync() |
O_DIRECT_NO_FSYNC |
fsync() |
O_DIRECT |
|
|
All_O_DIRECT (percona) |
O_DIRECT |
fsync() |
O_DIRECT |
fsync |
表格 2 innodb_flush_method數據文件和日誌刷新對應表
圖 3 innodb_flush_method數據文件和日誌刷新示意圖
該層的緩衝都放在主機內存中,它的目的主要是在操做系統層緩衝數據,避免慢速塊設備讀寫操做影響了IO的響應時間。
在前面redo log buffer和innodb buffer pool的討論中涉及到不少數據刷新和數據安全的問題,咱們在本節中,專門討論O_DIRECT/O_SYNC標籤的含義。
咱們打開一個文件並寫入數據,VFS和文件系統是怎麼把數據寫到硬件層列,下圖展現了關鍵的數據結構:
圖 4 VFS cache圖
該圖引用自The linux kernel’s VFS Layer。
圖中,咱們看到該層中主要有page_cache/buffer cache/Inode-cache/Directory cache。其中page_cache/buffer cache主要用於緩衝內存結構數據和塊設備數據。而inode-cache用於緩衝inode,directory-cache用於緩衝目錄結構數據。
根據文件系統和操做系統的不一樣,通常來講對一個文件的寫入操做包括兩部分,對數據自己的寫入操做,以及對文件屬性(metadata元數據)的寫入操做(這裏的文件屬性包括目錄,inode等)。
瞭解了這些之後,咱們就可以比較簡單的說清楚各個標誌的意義了:
page cache |
buffer cache |
inode cache |
dictory cache |
|
O_DIRECT |
write bypass |
write bypass |
write & no flush |
write & no flush |
O_DSYNC/fdatasync() |
write & flush |
write & flush |
write & no flush |
write & no flush |
O_SYNC/fsync() |
write & flush |
write & flush |
write & flush |
write & flush |
表格 3 VFS cache刷新表
l O_DSYNC和fdatasync()的區別在於:是在每個IO提交的時刻都針對對應的page cache和buffer cache進行刷新;仍是在必定數據的寫操做之後調用fdatasync()的時刻對整個page cache和buffer cache進行刷新。O_SYNC和fsync()的區別同理。
l page cache和buffer cache的主要區別在於一個是面向實際文件數據,一個是面向塊設備。在VFS上層使用open()方式打開那些使用mkfs作成文件系統的文件,你就會用到page cache和buffer cache,而若是你在Linux操做系統上使用dd這種方式來操做Linux的塊設備,你就只會用到buffer cache。
l O_DSYNC和O_SYNC的區別在於:O_DSYNC告訴內核,當向文件寫入數據的時候,只有當數據寫到了磁盤時,寫入操做纔算完成(write才返回成功)。O_SYNC比O_DSYNC更嚴格,不只要求數據已經寫到了磁盤,並且對應的數據文件的屬性(例如文件inode,相關的目錄變化等)也須要更新完成纔算write操做成功。可見O_SYNC較之O_DSYNC要多作一些操做。
l Open()的referense中還有一個O_ASYNC,它主要用於terminals, pseudoterminals, sockets, 和pipes/FIFOs,是信號驅動的IO,當設備可讀寫時發送一個信號(SIGIO),應用進程捕獲這個信號來進行IO操做。
l O_SYNC和O_DIRECT都是同步寫,也就是說只有寫成功了纔會返回。
回過頭來,咱們再來看innodb_flush_log_at_trx_commit的配置就比較好理解了。O_DIRECT直接IO繞過了page cache/buffer cache之後爲何還須要fsync()了,就是爲了把directory cache和inode cache元數據也刷新到存儲設備上。
而因爲內核和文件系統的更新,有些文件系統可以保證保證在O_DIRECT方式下不用fsync()同步元數據也不會致使數據安全性問題,因此InnoDB又提供了O_DIRECT_NO_FSYNC的方式。
固然,O_DIRECT對讀和對寫都是有效的,特別是對讀,它能夠保證讀到的數據是從存儲設備中讀到的,而不是緩存中的。避免緩存中的數據和存儲設備上的數據是不一致的狀況(好比你經過DRBD將底層塊設備的數據更新了,對於非分佈式文件系統,緩存中的內容和存儲設備上的數據就不一致了)。可是咱們這裏主要討論緩衝(寫buffer),就不深刻討論了。這個問題了。
在大部分的innodb_flush_method參數值的推薦中都會建議使用O_DIRECT,甚至在percona server分支中還提供了ALL_O_DIRECT,對日誌文件也使用了O_DIRECT方式打開。
l 節省操做系統內存:O_DIRECT直接繞過page cache/buffer cache,這樣避免InnoDB在讀寫數據少佔用操做系統的內存,把更多的內存留個innodb buffer pool來使用。
l 節省CPU。另外,內存到存儲設備的傳輸方式主要有poll,中斷和DMA方式。使用O_DIRECT方式提示操做系統儘可能使用DMA方式來進行存儲設備操做,節省CPU。
l 字節對齊。O_DIRECT方式要求寫數據時,內存是字節對齊的(對齊的方式根據內核和文件系統的不一樣而不一樣)。這就要求數據在寫的時候須要有額外的對齊操做。能夠經過/sys/block/sda/queue/logical_block_size知道對齊的大小,通常都是512個字節。
l 沒法進行IO合併。O_DIRECT繞過page cache/buffer cache直接寫存儲設備,這樣若是對同一塊數據進行重複寫就沒法在內存中命中,page cache/buffer cache合併寫的功能就沒法生效了。
l 下降順序讀寫效率。若是使用O_DIRECT打開文件,則讀/寫操做都會跳過cache,直接在存儲設備上讀/寫。由於沒有了cache,因此文件的順序讀寫使用O_DIRECT這種小IO請求的方式效率是比較低的。
總的來講,使用O_DIRECT來設置innodb_flush_method並非100%對全部應用和場景都是適用的。
該層的緩衝都放在存儲控制器的對應板載cache中,它的目的主要是在存儲控制器層緩衝數據,避免慢速塊設備讀寫操做影響了IO的響應時間。
當數據被fsync()等刷到存儲層時,首先會發送到存儲控制器層。常見的存儲控制器就是Raid卡,而目前大部分的Raid卡都有1G或者更大的存儲容量。這個緩衝通常爲易失性的存儲,經過板載電池/電容來保證該「易失性的存儲」的數據在機器斷電之後仍然會同步到底層的磁盤存儲介質上。
關於存儲控制器咱們有一些幾個方面須要注意的:
針對是否使用緩衝,通常的存儲控制器都提供write back和write through兩種方式。write back方式下,操做系統提交的寫數據請求直接寫入到緩衝中就返回成功;write through方式下,操做系統提交的寫數據請求必需要真正寫到底層磁盤介質上才返回成功。
爲了保證機器掉電之後在「易失性」緩衝中的數據可以及時刷新到底層磁盤介質上,存儲控制器上都有電池/電容來保證。普通的電池有容量衰減的問題,也就是說每隔一段時間,板載的電池都要被控制充放電一次,以保證電池的容量。在電池充放過程當中,被設置爲write-back的存儲控制器會自動變爲write through。這個充放電的週期(Learn Cycle週期)通常爲90天,LSI卡能夠經過MegaCli來查看:
#MegaCli -AdpBbuCmd -GetBbuProperties-aAll
BBU Properties for Adapter: 0
Auto Learn Period: 90 Days
Next Learn time: Tue Oct 14 05:38:43 2014
Learn Delay Interval:0 Hours
Auto-Learn Mode: Enabled
若是你每隔一段時間發現IO請求響應時間忽然慢下來了,就有多是這個問題哦。經過MegaCli -AdpEventLog -GetEvents -f mr_AdpEventLog.txt -aALL的日誌中的Event Description: Battery started charging就能夠肯定是否發生了發生了充放電的狀況。
因爲電池有這個問題,新的Raid卡會配置電容來保證「易失性」緩衝中的數據可以及時刷新到底層磁盤介質上,這樣就沒有充放電的問題了。
HP的smart array提供對cache的讀和寫的區別(Accelerator Ratio),
hpacucli ctrl all show config detail|grep 'Accelerator Ratio'
Accelerator Ratio: 25% Read / 75% Write
這樣你就能夠根據應用的實際狀況來設置用於緩存讀和緩衝寫的cache的比例了。
爲了可以讓上層的設備使用Direct IO方式來繞過raid卡,對Raid須要設置開啓DirectIO方式:
/opt/MegaRAID/MegaCli/MegaCli64 -LDSetProp -Direct -Immediate -Lall -aAll
上面咱們提到了「易失性」緩衝,若是咱們如今有一個非易失性的緩衝,而且容量達到幾百G,這樣的存儲控制器緩衝是否是更能給底層設備提速?做爲老牌的Raid卡廠商,LSI目前就有這樣的存儲控制器,使用write back方式和比較依賴存儲控制器緩衝的應用能夠考慮使用這種類型的存儲控制器。
目前raid卡的cache是否有電池或者電容保護對Linux來講是不可見的,因此Linux爲了保證日誌文件系統的一致性,默認會打開write barriers,也就是說,它會不斷的刷新「易失性」緩衝,這樣會大大下降IO性能。因此若是你確信底層的電池可以保證「易失性」緩衝會刷到底層磁盤設備的話,你能夠在磁盤mount的時候加上-o nobarrier。
該層的緩衝都放在磁盤控制器的對應板載cache中。存儲設備固件(firmware)會按規則排序將寫操做真正同步到介質中去。這裏主要是保證寫的順序性,對機械磁盤來講,這樣能夠儘可能讓一次磁頭的移動可以完成更多的磁碟寫入操做。
通常來講,DMA控制器也是放在磁盤這一層的,經過DMA控制器直接進行內存訪問,可以節省CPU的資源。
對於機械硬盤,由於通常的磁盤設備上並無電池電容等,沒法保證在機器掉電時磁盤cache裏面的全部數據可以及時同步到介質上,因此咱們強烈建議把disk cache關閉掉。
Disk cache能夠在存儲控制器層關閉。例如,使用MegaCli關閉的命令以下:
MegaCli -LDSetProp -DisDskCache -Lall -aALL
從InnoDB到最終的介質,咱們通過了各類緩衝,他們的目的其實很明確,就是爲了解決:內存和磁盤的速度不匹配的問題,或者說是磁盤的速度過慢的問題。
另外,其實最懂數據是否應該緩衝/緩存的仍是應用自己,VFS,存儲控制器和磁盤只能經過延遲寫入(以便合併重複IO,使隨機寫變成順序寫)來緩解底層存儲設備慢速形成的響應速度慢的問題。因此數據庫類型的應用都會來本身管理緩衝,而後儘可能避免操做系統和底層設備的緩衝。
可是其實因爲目前SSD固態硬盤和PCIe Flash卡的出現,內存和磁盤之間的速度差別被大大縮減了,這些緩衝是否必要,軟硬件哪些可改進的,對軟硬件工程師的一大挑戰。
參考:
http://www.codeproject.com/Articles/460057/HDD-FS-O_SYNC-Throughput-vs-Integrity
http://rdc.taobao.com/blog/dba/html/296_innodb_flush_method_performance.html
http://www.orczhou.com/index.php/2009/08/innodb_flush_method-file-io/
http://blog.csdn.net/yuyin86/article/details/8113305
https://www.usenix.org/legacy/event/usenix01/full_papers/kroeger/kroeger_html/node8.html
http://www.lsi.com/downloads/Public/Direct%20Assets/LSI/Benchmark_Tips.pdf
http://www.lsi.com/products/flash-accelerators/pages/default.aspx。
http://en.wikipedia.org/wiki/Direct_memory_access
http://en.wikipedia.org/wiki/Disk_buffer
7. 附錄
錯誤的方式:
import os
f = os.open('file', os.O_CREAT | os.O_TRUNC | os.O_DIRECT | os.O_RDWR)
s = ' ' * 1024
os.write(f, s)
Traceback (most recent call last):
File "", line 1, in
OSError: [Errno 22] Invalid argument
正確的方式:
import os
import mmap
f = os.open('file', os.O_CREAT | os.O_DIRECT | os.O_TRUNC | os.O_RDWR)
m = mmap.mmap(-1, 1024 * 1024)
s = ' ' * 1024 * 1024
m.write(s)
os.write(f, m)
os.close(f)