转载

从Apache Kafka 重温高效文件操作

0. Overview

-- 初稿修改中,请勿转载。

Apache Kafka说,不要害怕文件。

它就那么简简单单的用顺序写的普通的文件,充分利用Linux内核的Page Cache,不用内存,胜用内存,完全没有别家那样要同时维护内存中数据、持久化数据的麻烦————只要内存足够,生产者与消费者的速度也没有差上太多,那它虽然使用文件,但读写都发生在Page Cache中,完全没有同步的磁盘访问操作。

整个IO系统里,从上到下分成文件系统层(vfs + ext3)、 Page Cache 层、通用数据块层、 IO调度层、块设备驱动层。 这里就借着Apache Kafka的由头,将Page Cache层与IO调度层重温一遍。

1. Page Cache

1.1 读写空中接力

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

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

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

当读操作发生时,它会首先在Page Cache中查找内容是否存在,如果没有,它会锁住该页,从磁盘读取文件,写回Page Cache,再解锁该页。

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

1.2 Page Cache的flush策略

这是大家最关心的,因为flush不当的话,OS crash 可能引起丢数据。当然,Kafka不怕丢,因为它的持久性靠replicate保证,kafka重启后会从原来的replicate follower中拉缺失的数据。

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

1. 如果dirty page的时间超过了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 线程写盘。增减这个值是是flush策略里最主要的调优手段。

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

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

1.3 其他触发flush的方式

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

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

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

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

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

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

O_DIRECT标志表示完全跳过Page Cache,写的时候从ext3层直接发送给IO调度层,不过这样子,读的时候也就不能从Page Cache里读取必须去访问磁盘文件了,而且要求所有IO请求长度,缓冲区对齐及偏移都必须是底层上去大小的。所以开O_DIRECT的时候一定要在应用层做好Cache,

1.4 Page Cache的清理策略

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

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

2. IO调度层

如果所有读写都直接发给硬盘,对传统硬盘来说太残忍了。IO调度层主要做两个事情,合并和排序。 合并是将相同和相邻扇区(每个512字节)的操作合并成一个,比如我现在要读扇区1,2,3,那可以合并成一个读扇区1-3的操作。排序就是将所有操作按扇区排成一个队列,让磁盘的磁头可以按顺序移动,有效减少了机械硬盘寻址这个最慢的操作。

排序看上去很美,但可能造成严重的不公平,比如某个应用在相邻扇区狂写盘,其他应用就都等在那了,pdflush还好等等没所谓,读请求都是同步的,耗在那很惨。

所有又有多种算法解决这个问题,其中内核2.6的默认算法是CFQ(完全公正排队),就是把总的排序队列拆分成每个发起读写的进程自己有一条排序队列,然后以时间片轮转调度每个队列,每次从每个进程的队列里拿出若干个请求来执行(默认是4)。

在Apache Kafka里,应用的读写都发生在内存中,真正写盘的就是那条flusher进程,因为都是顺序写,无论它要写多少个Partition的文件,经过合并和排序后都能获得很好的性能。

如果是SSD硬盘,没有寻址的花销,排序好像就没必要了,但合并的帮助依然很多,所以还有另一种只合并不排序的NOOP的算法可供选择。

正文到此结束
Loading...