简单回顾一下之前的结论:

  • 机械磁盘的IO速度主要受寻道速度的限制,所以在访问小文件时IO性能会极差。如果不在乎成本,可以通过使用固态硬盘来解决这个问题。

  • Linux主流文件系统(如ext4等),在文件系统持续比较满,且需要经常删改文件时,会产生大量文件碎片。在我开发的一款代理服务器中,磁盘长期满负荷运转,运行一个月后文件碎片大约会让IO性能降低至只剩20%~30%.

这一期我们来看一下有哪些办法可以减少Linux下的文件碎片。主要是针对磁盘长期满负荷运转的使用场景(例如http代理服务器); 另外有一个小技巧,针对互联网图片服务器,可以将IO性能提升数倍。

如果为服务器订制一个专用文件系统,可以完全解决文件碎片的问题,将磁盘IO的性能发挥至极限。对于我们的代理服务器,相当于把IO性能提升到3-5倍。

1. 在现有文件系统下进行优化

Linux内核和各个文件系统采用了几个优化方案来提升磁盘访问速度。但这些优化方案需要在我们的服务器设计中进行配合才能得到充分发挥。

1) 文件系统缓存

Linux内核会将大部分空闲内存交给虚拟文件系统,来作为文件缓存,叫做page cache。在内存不足时,这部分内存会采用LRU算法进行淘汰。通过free命令查看内存时,显示为cached的部分就是文件缓存了:

# free
              total        used        free      shared  buff/cache   available
Mem:      263857760    82599316    37606704     4281572   143651740   145906980
Swap:       8388604      549852     7838752

我们如何进行针对性优化呢?

LRU并不是以一个优秀的淘汰算法,LRU最大的优势是普适性好,在各种使用场景下都能起到一定的效果。如果能找到当前使用场景下,文件被访问的统计特征,针对性的写一个淘汰算法,可以大幅提升文件缓存的命中率。

对于HTTP正向代理来说,一个好的淘汰算法可以用1GB内存达到LRU算法100GB内存的缓存效果。如果不打算写一个新的淘汰算法,一般不需要在应用层再搭一个文件cache程序来做缓存。

2) 最小分配

当文件扩大,需要分配磁盘空间时,大部分文件系统不会仅仅只分配当前需要的磁盘空间,而是会多分配一些磁盘空间。这样下次文件扩大时就可以使用已经分配好的空间,而不会频繁的去分配新空间。例如ext3下,每次分配磁盘空间时,最小是分配8KB。

最小分配的副作用是会浪费一些磁盘空间(分配了,但是又没有使用)。

如何进行针对性优化呢? 我们在reiserfs下将最小分配空间从8KB改大到128KB后提升了30%的磁盘IO性能。如果当前使用场景下小文件很多,把预分配改大就会浪费很多磁盘空间,所以这个数值要根据当前使用场景来设定。

3) IO访问调度

在同时有多个IO访问时,Linux内核可以对这些IO访问按LBA进行合并和排序,这样磁头在移动时,可以顺便读出移动过程中的数据。Linux 2.6版本内核有四种不同的排序算法,有些侧重于IO性能最大化,也有一些侧重于调度的公平性,大致上的原理都类似于电梯排序。

SATA等磁盘甚至在磁盘中内置了IO排序来进一步提升性能,一般需要在主板中进行配置才能启动磁盘内置IO排序。Linux的IO排序是根据LBA进行的,但是LBA是一个一维线性地址,无法完全反映出二维的圆形磁盘,所以磁盘的内置IO排序能达到更好的效果。

如何进行优化呢? IO访问调度能大幅提升IO性能,前提是应用层同时发起了足够的IO访问供Linux去调度。怎样才能从应用层同时向内核发起多个IO访问呢?

  • 方案1: 用aio_read异步发起多个文件读写请求

  • 方案2: 使用磁盘线程池同时发起多个文件读写请求

对于我们的http正向代理来说,采用16个线程读写磁盘可以将性能提升到2.5倍左右。具体开多少个线程/进程,可以根据具体使用场景来决定。

小提示:

将文件句柄设置为非阻塞时,进程还是会睡眠等待磁盘IO,非阻塞对于文件读写是不生效的。正常情况下,读文件只会引入十几
毫秒睡眠,所以不太明显; 而在磁盘IO极大时,读文件会引起十秒以上的进程睡眠。

详见内核源代码,do_generic_file_read会调用lock_page_killable进入睡眠,但是不会判断句柄的非阻塞标志

4) 调整Linux文件读写请求队列长度

在Linux系统中,如果有大量读请求,默认的请求队列或许应付不过来,我们可以动态调整请求队列数来提高效率。默认的请求队列数存放在/sys/block/xvda/queue/nr_request文件中。例如,我们要查看磁盘sda的nr_request,则:

# cat /sys/block/sda/queue/nr_requests 
128

我们可以适当调大一点nr_requests的值。但需要注意的是,虽然适当的调整nr_requests参数可以大幅提升磁盘的吞吐量,缺点就是你要牺牲一定的内存

5) 预读取

Linux内核可以预测我们将来的读请求并提前将数据读取出来。通过预读取可以减少IO的次数,并且减小读请求的延时。

如何进行针对性的优化呢? 预读取的准确率是有限的,与其依赖预读取,不如我们直接开一个较大的缓冲区,一次性将文件读出来再慢慢处理;尽量不要开一个较小的缓冲区,循环读文件/处理文件。

究竟开多大的缓冲区合适,要根据具体使用场景下的内存/磁盘IO压力来决定。

虽然说预读取延迟分配能起到类似的作用,但是我们自己扩大读写缓冲区效果往往会更好。

6) 延迟分配

当文件扩大,需要分配磁盘空间时,可以不立即进行分配,而是暂存在内存中,将多次分配磁盘空间的请求聚合在一起后,再进行一次性分配。延迟分配的目的也是减少分配次数,从而减少文件不连续性。

延迟分配的副作用有如下几个:

  • 如果应用程序每次写数据后都通过fsync等接口进行强制刷新,延迟分配将不起作用;

  • 延迟分配有可能间歇性引入一个较大的磁盘IO延时(因为要一次性向磁盘写入较多数据)

注: 只有少数新文件系统支持这个特性

如何进行针对性的优化呢? 如果不是对安全性(是否允许丢失)要求极高的数据,可以直接在应用程序里缓存起来,积累到一定大小再写入,效果比文件系统的延迟分配更好; 如果对安全性要求极高,建议经常用fsync强制刷新。

7) 在线磁盘碎片整理

2. 实现自己的文件系统

我们曾经写过一款专用文件系统,针对代理服务器,将磁盘IO性能提升到3-5倍。在大部分服务器上,不需要支持修改文件这个功能。一旦文件创建好,就不能再做修改操作,只支持读取和删除。在这个前提下,我们可以消灭所有文件碎片,把磁盘IO效率提升到理论极限。

在我们的服务器中,每个文件的缓冲区最大值设定为16MB。

  • 对于小于16MB的文件,在服务器准备好整个文件内容后,再创建文件。创建文件时服务器给出文件大小,文件系统保证为文件分配连续的空间。读写文件时,服务器一次性读写整个文件。

  • 对于大于16MB的文件,服务器创建文件时告诉文件系统分配16MB磁盘空间。后续每次扩大文件大小时,要么是16MB,要么就是文件终结。不允许在文件未终结的情况下分配非16MB的空间。读写文件时,每次读写16MB或者直到文件末尾。

在我们的文件系统中,小文件完全无碎片,一次寻道就能搞定一个文件,达到了理论上的最佳性能。大文件每次磁头定位读写16MB,性能没有达到100%,但已经相当好了。

有一个公式可以用来衡量磁盘IO的效率:

磁盘利用率 = 传输时间/(平均寻道时间 + 传输时间)

对我们当时采用的磁盘(1T 7200转sata)来说,16MB连续读写已经可以达到98%以上的磁盘利用率。



[参看]:

  1. 如何让linux服务器磁盘io性能翻倍

  2. linux磁盘IO读写性能优化

  3. Ceph性能优化总结

  4. IDF15:Ceph性能测试及优化浅析