前面不少大侠都分享过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)