数据库内核月报

数据库内核月报 - 2017 / 03

MySQL · 引擎特性 · InnoDB IO子系统

Author: 韩逸

前言

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_funcos_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使用了文件系统的文件锁来保证只有一个进程对某个文件进行读写操作(os_file_lock),使用了建议锁(Advisory locking),而不是强制锁(Mandatory locking),因为强制锁在不少系统上有bug,包括linux。在非只读模式下,所有文件打开后,都用文件锁锁住。

InnoDB中目录的创建使用递归的方式(os_file_create_subdirs_if_neededos_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 aio被import进来之前以及某些不支持aio的系统上,自己模拟了一条aio的机制。异步读写请求提交时,仅仅把它放入一个队列中,然后就返回,程序可以去做其他事情。后台有若干异步io处理线程(innobase_read_io_threads和innobase_write_io_threads这两个参数控制)不断从这个队列中取出请求,然后使用同步IO的方式完成读写请求以及读写完成后的工作。 另外一种就是Native aio。目前在linux上使用io_submit,io_getevents等函数完成(不使用glibc aio,这个也是模拟的)。提交请求使用io_submit, 等待请求使用io_getevents。另外,Windows平台上也有自己对应的aio,这里就不介绍了,如果使用了Windows的技术栈,数据库应该会选用sqlserver。目前,其他平台(Linux和Windows之外)都只能使用Simulate aio。

首先介绍一下一些通用的函数和结构,接下来分别详细介绍一下Simulate alo和Linux上的Native aio。 在os0file.cc中定义了全局数组,类型为os_aio_array_t,这些数组就是Simulate aio用来缓存读写请求的队列,数组的每一个元素是os_aio_slot_t类型,里面记录了每个IO请求的类型,文件的fd,偏移量,需要读取的数据量,IO请求发起的时间,IO请求是否已经完成等。另外,Linux native io中的struct iocb也在os_aio_slot_t中。数组结构os_aio_slot_t中,记录了一些统计信息,例如有多少数据元素(os_aio_slot_t)已经被使用了,是否为空,是否为满等。这样的全局数组一共有5个,分别用来保存数据文件读异步请求(os_aio_read_array),数据文件写异步请求(os_aio_write_array),日志文件写异步请求(os_aio_log_array),insert buffer写异步请求(os_aio_ibuf_array),数据文件同步读写请求(os_aio_sync_array)。日志文件的数据块写入是同步IO,但是这里为什么还要给日志写分配一个异步请求队列(os_aio_log_array)呢?原因是,InnoDB日志文件的日志头中,需要记录checkpoint的信息,目前checkpoint信息的读写还是用异步IO来实现的,因为不是很紧急。在Windows平台中,如果对特定文件使用了异步IO,就这个文件就不能使用同步IO了,所以引入了数据文件同步读写请求队列(os_aio_sync_array)。日志文件不需要读异步请求队列,因为只有在做奔溃恢复的时候日志才需要被读取,而做崩溃恢复的时候,数据库还不可用,因此完全没必要搞成异步读取模式。这里有一点需要注意,不管变量innobase_read_io_threads和innobase_write_io_threads两个参数是多少,os_aio_read_arrayos_aio_write_array都只有一个,只不过数据中的os_aio_slot_t元素会相应增加,在linux中,变量加1,元素数量增加256。例如,innobase_read_io_threads=4,则os_aio_read_array数组被分成了四部分,每一个部分256个元素,每个部分都有自己独立的锁、信号量以及统计变量,用来模拟4个线程,innobase_write_io_threads类似。从这里我们也可以看出,每个异步read/write线程能缓存的读写请求是有上限的,即为256,如果超过这个数,后续的异步请求需要等待。256可以理解为InnoDB层对异步IO并发数的控制,而在文件系统层和磁盘层面也有长度限制,分别使用cat /sys/block/sda/queue/nr_requestscat /sys/block/sdb/queue/nr_requests查询。 os_aio_init在InnoDB启动的时候调用,用来初始化各种结构,包括上述的全局数组,还有Simulate aio中用的锁和互斥量。os_aio_free则释放相应的结构。os_aio_print_XXX系列的函数用来输出aio子系统的状态,主要用在show engine innodb status语句中。

Simulate aio

Simulate aio相对Native aio来说,由于InnoDB自己实现了一套模拟机制,相对比较复杂。

综上所述,可以看出IO请求是一个一个的push的对立面,每push进一个后台线程就拿去处理,如果后台线程优先级比较高的话,IO合并效果可能比较差,为了解决这个问题,Simulate aio提供类似组提交的功能,即一组IO请求提交后,才唤醒后台线程,让其统一进行处理,这样IO合并的效果会比较好。但这个依然有点小问题,如果后台线程比较繁忙的话,其就不会进入等待状态,也就是说只要请求进入了队列,就会被处理。这个问题在下面的Native aio中可以解决。 总体来说,InnoDB实现的这一套模拟机制还是比较安全可靠的,如果平台不支持Native aio则使用这套机制来读写数据文件。

Linux native aio

如果系统安装了libaio库且在配置文件里面设置了innodb_use_native_aio=on则启动时候会使用Native aio。

在分发IO线程时,尽量把相邻的IO放在一个线程内,这个与Simulate aio类似,但是后续的IO合并操作,Simulate aio是自己实现,Native aio则交给内核完成了,因此代码比较简单。 还要一个区别是,当没有IO请求的时候,Simulate aio会进入等待状态,而Native aio则会每0.5秒醒来一次,做一些检查工作,然后继续等待。因此,当有新的请求来时,Simulated aio需要用户线程唤醒,而Native aio不需要。此外,在服务器关闭时,Simulate aio也需要唤醒,Native aio则不需要。

可以发现,Native aio与Simulate aio类似,请求也是一个一个提交,然后一个一个处理,这样会导致IO合并效果比较差。Facebook团队提交了一个Native aio的组提交优化:把IO请求首先缓存,等IO请求都到了之后,再调用io_submit函数,一口气提交先前的所有请求(io_submit可以一次提交多个请求),这样内核就比较方便做IO优化。Simulate aio在IO线程压力大的情况下,组提交优化会失效,而Native aio则不会。注意,组提交优化,不能一口气提交太多,如果超过了aio等待队列长度,会强制发起一次io_submit。

总结

本文详细介绍了InnoDB中IO子系统的实现以及使用需要注意的点。InnoDB日志使用同步IO,数据使用异步IO,异步IO的写盘顺序也不是先进先出的模式,这些点都需要注意。Simulate aio虽然有比较大的学习价值,但是在现代操作系统中,推荐使用Native aio。