从Apache Kafka 重温文件高效读写

它就那么简简单单地用顺序写的普通文件,借力于Linux内核的Page Cache,不(显式)用内存,胜用内存,完全没有别家那样要同时维护内存中数据、持久化数据的烦恼——只要内存足够,生产者与消费者的速度也没有差上太多,读写便都发生在Page Cache中,完全没有同步的磁盘访问。

整个IO过程,从上到下分成文件系统层(VFS+ ext3)、 Page Cache 层、通用数据块层、 IO调度层、块设备驱动层。 这里借着Apache Kafka的由头,将Page Cache层与IO调度层重温一遍,记一篇针对Linux kernel 2.6的科普文。

1. Page Cache 1.1 读写空中接力

Linux总会把系统中还没被应用使用的内存挪来给Page Cache,在命令行输入free,或者cat /proc/meminfo,"Cached"的部分就是Page Cache。

Page Cache中每个文件是一棵Radix树(基树),节点由4k大小的Page组成,可以通过文件的偏移量快速定位Page。

当写操作发生时,它只是将数据写入Page Cache中,并将该页置上dirty标志。

当读操作发生时,它会首先在Page Cache中查找内容,如果有就直接返回了,没有的话就会从磁盘读取文件再写回Page Cache。

可见,只要生产者与消费者的速度相差不大,消费者会直接读取之前生产者写入Page Cache的数据,大家在内存里完成接力,根本没有磁盘访问。

而比起在内存中维护一份消息数据的传统做法,这既不会重复浪费一倍的内存,Page Cache又不需要GC(可以放心使用60G内存了),而且即使Kafka重启了,Page Cache还依然在。

1.2 后台异步flush的策略

这是大家最需要关心的,因为不能及时flush的话,OS crash(不是应用crash) 可能引起数据丢失,Page Cache瞬间从朋友变魔鬼。

当然,Kafka不怕丢,因为它的持久性是靠replicate保证,重启后会从原来的replicate follower中拉缺失的数据。

内核线程pdflush负责将有dirty标记的页面,发送给IO调度层。内核会为每个磁盘起一条pdflush线程,每5秒(/proc/sys/vm/dirty_writeback_centisecs)唤醒一次,根据下面三个参数来决定行为:

1. 如果page dirty的时间超过了30秒(/proc/sys/vm/dirty_expire_centiseconds,单位是百分之一秒),就会被刷到磁盘,所以crash时最多丢30秒左右的数据。

2. 如果dirty page的总大小已经超过了10%(/proc/sys/vm/dirty_background_ratio)的可用内存(cat /proc/meminfo里 MemFree+ Cached - Mapped),则会在后台启动pdflush 线程写盘,但不影响当前的write(2)操作。增减这个值是最主要的flush策略里调优手段。

3. 如果wrte(2)的速度太快,比pdflush还快,dirty page 迅速涨到 20%(/proc/sys/vm/dirty_ratio)的总内存(cat /proc/meminfo里的MemTotal),则此时所有应用的写操作都会被block,各自在自己的时间片里去执行flush,因为操作系统认为现在已经来不及写盘了,如果crash会丢太多数据,要让大家都冷静点。这个代价有点大,要尽量避免。在Redis2.8以前,Rewrite AOF就经常导致这个大面积阻塞,现在已经改为Redis每32Mb先主动flush()一下了。

详细的文章可以看: The Linux Page Cache and pdflush
 

1.3 主动flush的方式

对于重要数据,应用需要自己触发flush保证写盘。

1. 系统调用fsync() 和 fdatasync()

fsync(fd)将属于该文件描述符的所有dirty page的写入请求发送给IO调度层。

fsync()总是同时flush文件内容与文件元数据, 而fdatasync()只flush文件内容与后续操作必须的文件元数据。元数据含时间戳,大小等,大小可能是后续操作必须,而时间戳就不是必须的。因为文件的元数据保存在另一个地方,所以fsync()总是触发两次IO,性能要差一点。

2. 打开文件时设置O_SYNC,O_DSYNC标志或O_DIRECT标志

O_SYNC、O_DSYNC标志表示每次write后要等到flush完成才返回,效果等同于write()后紧接一个fsync()或fdatasync(),不过按APUE里的测试,因为OS做了优化,性能会比自己调write() + fsync()好一点,但与只是write相比就慢很多了。

O_DIRECT标志表示直接IO,完全跳过Page Cache。不过这也放弃了读文件时的Cache,必须每次读取磁盘文件。而且要求所有IO请求长度,偏移都必须是底层扇区大小的整数倍。所以使用直接IO的时候一定要在应用层做好Cache。
 

1.4 Page Cache的清理策略

当内存满了,就需要清理Page Cache,或把应用占的内存swap到文件去。有一个swappiness的参数(/proc/sys/vm/swappiness)决定是swap还是清理page cache,值在0到100之间,设为0表示尽量不要用swap,这也是很多优化指南让你做的事情,因为默认值居然是60,Linux认为Page Cache更重要。

Page Cache的清理策略是LRU的升级版。如果简单用LRU,一些新读出来的但可能只用一次的数据会占满了LRU的头端。因此将原来一条LRU队列拆成了两条,一条放新的Page,一条放已经访问过好几次的Page。Page刚访问时放在新LRU队列里,访问几轮了才升级到旧LRU队列(想想JVM Heap的新生代老生代)。清理时就从新LRU队列的尾端开始清理,直到清理出足够的内存。

1.5 预读策略

根据清理策略,Apache Kafka里如果消费者太慢,堆积了几十G的内容,Cache还是会被清理掉的。这时消费者就需要读盘了。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/3fab09b7832b56f78d5a9b139449b3b7.html