前言
InnoDB做为一款成熟的跨平台数据库引擎,其实现了一套高效易用的IO接口,包括同步异步IO,IO合并等。本文简单介绍一下其内部实现,主要的代码集中在os0file.cc这个文件中。本文的分析默认基于MySQL 5.6,CentOS 6,gcc 4.8,其他版本的信息会另行指出。
基础知识
WAL技术 : 日志先行技术,基本所有的数据库,都使用了这个技术。简单的说,就是需要写数据块的时候,数据库前台线程把对应的日志先写(批量顺序写)到磁盘上,然后就告诉客户端操作成功,至于真正写数据块的操作(离散随机写)则放到后台IO线程中。使用了这个技术,虽然多了一个磁盘写入操作,但是由于日志是批量顺序写,效率很高,所以客户端很快就能得到相应。此外,如果在真正的数据块落盘之前,数据库奔溃,重启时候,数据库可以使用日志来做崩溃恢复,不会导致数据丢失。
数据预读 : 与数据块A“相邻”的数据块B和C在A被读取的时候,B和C也会有很大的概率被读取,所以可以在读取B的时候,提前把他们读到内存中,这就是数据预读技术。这里说的相邻有两种含义,一种是物理上的相邻,一种是逻辑上的相邻。底层数据文件中相邻,叫做物理上相邻。如果数据文件中不相邻,但是逻辑上相邻(id=1的数据和id=2的数据,逻辑上相邻,但是物理上不一定相邻,可能存在同一个文件中不同的位置),则叫逻辑相邻。
文件打开模式 : Open系统调用常见的模式主要三种:O_DIRECT,O_SYNC以及default模式。O_DIRECT模式表示后续对文件的操作不使用文件系统的缓存,用户态直接操作设备文件,绕过了内核的缓存和优化,从另外一个角度来说,使用O_DIRECT模式进行写文件,如果返回成功,数据就真的落盘了(不考虑磁盘自带的缓存),使用O_DIRECT模式进行读文件,每次读操作是真的从磁盘中读取,不会从文件系统的缓存中读取。O_SYNC表示使用操作系统缓存,对文件的读写都经过内核,但是这个模式还保证每次写数据后,数据一定落盘。default模式与O_SYNC模式类似,只是写数据后不保证数据一定落盘,数据有可能还在文件系统中,当主机宕机,数据有可能丢失。
此外,写操作不仅需要修改或者增加的数据落盘,而且还需要文件元信息落盘,只有两部分都落盘了,才能保证数据不丢。O_DIRECT模式不保证文件元信息落盘(但大部分文件系统都保证,Bug #45892),因此如果不做其他操作,用O_DIRECT写文件后,也存在丢失的风险。O_SYNC则保证数据和元信息都落盘。default模式两种数据都不保证。
调用函数fsync后,能保证数据和日志都落盘,因此使用O_DIRECT和default模式打开的文件,写完数据,需要调用fsync函数。
同步IO : 我们常用的read/write函数(Linux上)就是这类IO,特点是,在函数执行的时候,调用者会等待函数执行完成,而且没有消息通知机制,因为函数返回了,就表示操作完成了,后续直接检查返回值就可知道操作是否成功。这类IO操作,编程比较简单,在同一个线程中就能完成所有操作,但是需要调用者等待,在数据库系统中,比较适合急需某些数据的时候调用,例如WAL中日志必须在返回客户端前落盘,则进行一次同步IO操作。
异步IO : 在数据库中,后台刷数据块的IO线程,基本都使用了异步IO。数据库前台线程只需要把刷块请求提交到异步IO的队列中即可返回做其他事情,而后台线程IO线程,则定期检查这些提交的请求是否已经完成,如果完成再做一些后续处理工作。同时异步IO由于常常是一批一批的请求提交,如果不同请求访问同一个文件且偏移量连续,则可以合并成一个IO请求。例如,第一个请求读取文件1,偏移量100开始的200字节数据,第二个请求读取文件1,偏移量300开始的100字节数据,则这两个请求可以合并为读取文件1,偏移量100开始的300字节数据。数据预读中的逻辑预读也常常使用异步IO技术。
目前Linux上的异步IO库,需要文件使用O_DIRECT模式打开,且数据块存放的内存地址、文件读写的偏移量和读写的数据量必须是文件系统逻辑块大小的整数倍,文件系统逻辑块大小可以使用类似sudo blockdev --getss /dev/sda5
的语句查询。如果上述三者不是文件系统逻辑块大小的整数倍,则在调用读写函数时候会报错EINVAL,但是如果文件不使用O_DIRECT打开,则程序依然可以运行,只是退化成同步IO,阻塞在io_submit函数调用上。
InnoDB常规IO操作以及同步IO
在InnoDB中,如果系统有pread/pwrite函数(os_file_read_func
和os_file_write_func
),则使用它们进行读写,否则使用lseek+read/write方案。这个就是InnoDB同步IO。查看pread/pwrite文档可知,这两个函数不会改变文件句柄的偏移量且线程安全,所以多线程环境下推荐使用,而lseek+read/write方案则需要自己使用互斥锁保护,在高并发情况下,频繁的陷入内核态,对性能有一定影响。
在InnoDB中,使用open系统调用打开文件(os_file_create_func
),模式方面除了O_RDONLY(只读),O_RDWR(读写),O_CREAT(创建文件)外,还使用了O_EXCL(保证是这个线程创建此文件)和O_TRUNC(清空文件)。默认情况下(数据库不设置为只读模式),所有文件都以O_RDWR模式打开。innodb_flush_method这个参数比较重要,重点介绍一下:
如果innodb_flush_method设置了O_DSYNC,日志文件(ib_logfileXXX)使用O_SYNC打开,因此写完数据不需要调用函数fsync刷盘,数据文件(ibd)使用default模式打开,因此写完数据需要调用fsync刷盘。
如果innodb_flush_method设置了O_DIRECT,日志文件(ib_logfileXXX)使用default模式打开,写完数据需要调用fsync函数刷盘,数据文件(ibd)使用O_DIRECT模式打开,写完数据需要调用fsync函数刷盘。
如果innodb_flush_method设置了fsync或者不设置,数据文件和日志文件都使用default模式打开,写完数据都需要使用fsync来刷盘。
如果innodb_flush_method设置为O_DIRECT_NO_FSYNC,文件打开方式与O_DIRECT模式类似,区别是,数据文件写完后,不调用fsync函数来刷盘,主要针对O_DIRECT能保证文件的元数据也落盘的文件系统。
InnoDB目前还不支持使用O_DIRECT模式打开日志文件,也不支持使用O_SYNC模式打开数据文件。
注意,如果使用linux native aio(详见下一节),innodb_flush_method一定要配置成O_DIRECT,否则会退化成同步IO(错误日志中不会有任务提示)。
InnoDB使用了文件系统的文件锁来保证只有一个进程对某个文件进行读写操作(os_file_lock
),使用了建议锁(Advisory locking),而不是强制锁(Mandatory locking),因为强制锁在不少系统上有bug,包括linux。在非只读模式下,所有文件打开后,都用文件锁锁住。
InnoDB中目录的创建使用递归的方式(os_file_create_subdirs_if_needed
和os_file_create_directory
)。例如,需要创建/a/b/c/这个目录,先创建c,然后b,然后a,创建目录调用mkdir函数。此外,创建目录上层需要调用os_file_create_simple_func
函数,而不是os_file_create_func
,需要注意一下。
InnoDB也需要临时文件,临时文件的创建逻辑比较简单(os_file_create_tmpfile
),就是在tmp目录下成功创建一个文件后直接使用unlink函数释放掉句柄,这样当进程结束后(不管是正常结束还是异常结束),这个文件都会自动释放。InnoDB创建临时文件,首先复用了server层函数mysql_tmpfile的逻辑,后续由于需要调用server层的函数来释放资源,其又调用dup函数拷贝了一份句柄。
如果需要获取某个文件的大小,InnoDB并不是去查文件的元数据(stat
函数),而是使用lseek(file, 0, SEEK_END)
的方式获取文件大小,这样做的原因是防止元信息更新延迟导致获取的文件大小有误。
InnoDB会预分配一个大小给所有新建的文件(包括数据和日志文件),预分配的文件内容全部置为零(os_file_set_size
),当前文件被写满时,再进行扩展。此外,在日志文件创建时,即install_db阶段,会以100MB的间隔在错误日志中输出分配进度。
总体来说,常规IO操作和同步IO相对比较简单,但是在InnoDB中,数据文件的写入基本都用了异步IO。
InnoDB异步IO
由于MySQL诞生在Linux native aio之前,所以在MySQL异步IO的代码中,有两种实现异步IO的方案。
第一种是原始的Simulated aio,InnoDB在Linux native air被import进来之前以及某些不支持air的系统上,自己模拟了一条aio的机制。异步读写请求提交时,仅仅把它放入一个队列中,然后就返回,程序可以去做其他事情。后台有若干异步io处理线程(innobase_read_io_threads和innobase_write_io_threads这两个参数控制)不断从这个队列中取出请求,然后使用同步IO的方式完成读写请求以及读写完成后的工作。
另外一种就是Native aio。目前在linux上使用io_submit,io_getevents等函数完成(不使用glibc aio,这个也是模拟的)。提交请求使用io_submit, 等待请求使用io_getevents。另外,window平台上也有自己对应的aio,这里就不介绍了,如果使用了window的技术栈,数据库应该会选用sqlserver。目前,其他平台(Linux和window之外)都只能使用Simulate aio。
首先介绍一下一些通用的函数和结构,接下来分别详细介绍一下Simulate alo和Linux上的Native aio。
在os0file.cc中定义了全局数组,类型为os_aio_array_t
,这些数组就是Simulate aio用来缓存读写请求的队列,数组的每一个元素是os_aio_slot_t
类型,里面记录了每个IO请求的类型,文件的fd,偏移量,需要读取的数据量,IO请求发起的时间,IO请求是否已经完成等。另