共计 3820 个字符,预计需要花费 10 分钟才能阅读完成。
0. Overview
卡夫卡说:不要害怕文件系统。
它就那么简简单单地用顺序写的普通文件,借力于 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 还是会被清理掉的。这时消费者就需要读盘了。
内核这里又有个动态自适应的预读策略,每次读请求会尝试预读更多的内容(反正都是一次读操作)。内核如果发现一个进程一直使用预读数据,就会增加预读窗口的大小(最小 16K,最大 128K),否则会关掉预读窗口。连续读的文件,明显适合预读。
2. IO 调度层
如果所有读写请求都直接发给硬盘,对传统硬盘来说太残忍了。IO 调度层主要做两个事情,合并和排序。合并是将相同和相邻扇区 (每个 512 字节) 的操作合并成一个,比如我现在要读扇区 1,2,3,那可以合并成一个读扇区 1 - 3 的操作。排序就是将所有操作按扇区方向排成一个队列,让磁盘的磁头可以按顺序移动,有效减少了机械硬盘寻址这个最慢最慢的操作。
排序看上去很美,但可能造成严重的不公平,比如某个应用在相邻扇区狂写盘,其他应用就都干等在那了,pdflush 还好等等没所谓,读请求都是同步的,耗在那会很惨。
所有又有多种算法来解决这个问题,其中内核 2.6 的默认算法是 CFQ(完全公正排队),把总的排序队列拆分成每个发起读写的进程自己有一条排序队列,然后以时间片轮转调度每个队列,轮流从每个进程的队列里拿出若干个请求来执行(默认是 4)。
在 Apache Kafka 里,消息的读写都发生在内存中,真正写盘的就是那条 pdflush 内核线程,因为都是顺序写,即使一台服务器上有多个 Partition 文件,经过合并和排序后都能获得很好的性能,或者说,Partition 文件的个数并不影响性能,不会出现文件多了变成随机读写的情况。
如果是 SSD 硬盘,没有寻址的花销,排序好像就没必要了,但合并的帮助依然良多,所以还有另一种只合并不排序的 NOOP 算法可供选择。
题外话
另外,硬盘上还有一块几十 M 的缓存,硬盘规格上的外部传输速率 (总线到缓存) 与内部传输速率 (缓存到磁盘) 的区别就在此 ……IO 调度层以为已经写盘了,其实可能依然没写成,断电的话靠硬盘上的电池或大电容保命 ……
相关阅读:
分布式发布订阅消息系统 Kafka 架构设计 http://www.linuxidc.com/Linux/2013-11/92751.htm
Apache Kafka 代码实例 http://www.linuxidc.com/Linux/2013-11/92754.htm
Apache Kafka 教程笔记 http://www.linuxidc.com/Linux/2014-01/94682.htm
Apache kafka 原理与特性(0.8V) http://www.linuxidc.com/Linux/2014-09/107388.htm
Kafka 部署与代码实例 http://www.linuxidc.com/Linux/2014-09/107387.htm
Kafka 介绍和集群环境搭建 http://www.linuxidc.com/Linux/2014-09/107382.htm
Kafka 的详细介绍:请点这里
Kafka 的下载地址:请点这里
本文永久更新链接地址:http://www.linuxidc.com/Linux/2015-05/117022.htm