第九章 文件系统性能调优
9.1 PageCache 知识回顾
在上⼀节中我们了解到当系统中可⽤内存低于 low 的时候,内核会通过异步或者同步的⽅式回收 PageCache。
在上⼀篇《PageCache 到底是如何实现的?》 ⽂章中我们也了解了 PageCache 的实现原理,就是在 node 中的⼏个 LRU 链表。当系统中可⽤内存不⾜时,内核会从 LRU 链表中选择⼀些缓存⻚⾯进⾏回收,然后释放物理⻚给应⽤程序堆内存分配使⽤。
对于 PageCache 中的⼲净⻚⾯(和磁盘⽂件中⼀致,⽆修改)释放的时候很快,但是对于脏⻚(内存中有修改)的情况就⽐较耗时了。原因是脏⻚需要写⼊磁盘后才能释放。所以系统因为物理内存短缺⽽导致应⽤进程进⼊直接内存回收状态时,可能会因为脏⻚太多⽽导致耗时超⻓。
我们今天就通过⼀篇⽂章来看看脏⻚的产⽣过程,内核⼜是通过哪些策略将脏⻚写到磁盘中的。应该如何调优才能避免因为脏⻚⽽导致应⽤过慢的直接内存回收。
图像参考: 第88页, 图片39(脏页生成流程图)
9.1.1 脏页生成回顾
在我们的应⽤程序中,经常有写⽂件的需求,最典型的是写⽇志记录系统的请求处理过程。例如如下就是⼀个简单的写⽂件的例⼦。
在 Linux 中,⽂件是⼀套多层的软件机制。
int main()
{
...
out = open("20250101.log", O_WRONLY | O_CREAT | O_TRUNC);
write(out,&c,1);
...
}在 write 函数执⾏的过程中,会进⼊到系统调⽤层,再会进⼊到 vfs 层,…,然后会经过 PageCache 中,最后直到通过磁盘驱动写⼊到磁盘中。我把 write 写到 ext4 ⽂件系统时的各种调⽤和返回,⼤致理出来了⼀个交互图。当然为了突出重点,我抛弃了不少细节,⽐如 DIRECT IO、ext4 ⽇志记录啥的都没有体现出来,只抽取出来了⼀些我认为关键的调⽤。
在这个流程图中,最值得拿出来说的是函数是 __block_commit_write 中。在这个函数⾥绝⼤部分的情况下只是 make dirty。然后 write 函数调⽤就返回了。此时数据现在还在内存中的 PageCache ⾥,只是相应的⻚⾯缓存被标记成了 dirty 状态并没有真正写到硬盘。
只有在⼀种情况下,⽤户进程必须得等待写⼊完成才可以返回,这种情况发⽣的概率相对较低⼀些。那就是对 balance_dirty_pages_ratelimited 的判断如果超出限制了。该函数判断当前脏⻚是否已经超过脏⻚上限 [[dirty_bytes]]、[[dirty_ratio]],超过了就必须得等待。
这两个参数只有⼀个会⽣效,另外 1 个是 0。拿 dirty_ratio 来说,如果设置的是 30,就说明如果脏⻚⽐例超过内存的 30%,则 write 函数调⽤就必须等待写⼊完成才能返回。可以在你的机器下的 /proc/sys/vm/ ⽬录来查看这两个配置。
# cat /proc/sys/vm/dirty_bytes
0
# cat /proc/sys/vm/dirty_ratio
209.1.2 脏页回收回顾
在 Linux 内核中,刷新脏⻚的⼯作是由⼀组称为 flusher 线程的内核线程负责的。
每个块设备(如硬盘)都会有⾃⼰的 flusher 线程,当需要回写脏⻚时,bdi_default 线程会为该设备创建相应的 flusher 线程。这些线程以 "flush-" + "设备名" 的格式命名。例如在我的虚拟机上的 flusher 内核线程名如下:
# ps -ef | grep flust
root 29427 15:23 ? 00:00:00 [kworker/u65:2-flush-254:16]这些内核线程是周期性执⾏的,它的周期取决于内核参数 [[dirty_writeback_centisecs]] 的设置,根据参数名也能看出来,它的单位是百分之⼀秒。我查看到我的配置是 500,就是说每 5 秒会周期性地来执⾏⼀遍。该线程的回写函数调⽤栈⼤概如下。
# cat /proc/sys/vm/dirty_writeback_centisecs
500[kworker/u66:2]-[70612]-[do_writepages]-call--------------------------------
0xffffffff8dde9860 : do_writepages+0x0/0xd0 [kernel]
0xffffffff8ded8e19 : __writeback_single_inode+0x39/0x2e0 [kernel]
0xffffffff8ded92a6 : writeback_sb_inodes+0x1e6/0x450 [kernel]
0xffffffff8ded956f : __writeback_inodes_wb+0x5f/0xc0 [kernel]
0xffffffff8ded9825 : wb_writeback+0x255/0x2e0 [kernel]
0xffffffff8dedacce : wb_workfn+0x34e/0x4a0 [kernel]
0xffffffff8dca37b2 : process_one_work+0x1a2/0x360 [kernel]
0xffffffff8dca3ea0 : worker_thread+0x30/0x390 [kernel]
0xffffffff8dca9330 : kthread+0x110/0x130 [kernel]
0xffffffff8e4001cf : ret_from_fork+0x1f/0x30 [kernel]在 wb_writeback 会判断是否要开始回写。如果是 for_background 模式,且 wb_over_bg_thresh 判断成功,就会开始回写了。
// file:fs/fs-writeback.c
static long wb_writeback(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
work->older_than_this = &oldest_jif;
...
// 1.判断脏⻚是否超过⼀定⽐例
if (work->for_background && !wb_over_bg_thresh(wb))
break;
...
// 2.通过 dirtied_before 判断哪些需要回写
if (work->for_kupdate) {
dirtied_before = jiffies -
msecs_to_jiffies(dirty_expire_interval * 10);
} else if (work->for_background)
dirtied_before = jiffies;
...
}在 wb_writeback 函数中,有两种需要回写的情况需要说明。
第⼀种是在 wb_over_bg_thresh 函数判断的是当前的脏⻚⽐例是否超售了内核参数 [[dirty_background_bytes]] 或 [[dirty_background_ratio]] 的限制。如果超过了的话,就需要开始回写磁盘。
第⼆种是计算 dirtied_before,后⾯会⽤它判断哪些脏⻚“脏”的时间过⻓了,这些⻚⾯也需要回写。其计算中依赖的 dirty_expire_interval 变量是受 /proc/sys/vm/dirty_expire_centisecs 这个内核参数控制。在我的机器上,它的值是 3000。单位是百分之⼀秒,代表的是脏⻚过了 30 秒就会被内核线程认为需要写回到磁盘了。
# cat /proc/sys/vm/dirty_background_bytes
0
# cat /proc/sys/vm/dirty_background_ratio
10
# cat /proc/sys/vm/dirty_expire_centisecs
30009.2 文件系统性能优化
9.2.1 内核尽早回收
在 Linux 的内存⽔位中,有 min、low、high 三条⽔位线。缺⻚中断申请内存时:
- 如果空闲内存⾼于
high⽔位,是空闲内存⽐较充⾜的状态。 - 如果空闲内存从
high下降⾄low,那么kswapd内核线程会异步进⾏ Page Cache 的回收,直到回收⾄high为⽌。 - 如果空闲内存从
low下降⾄min,表明kswapd已经回收不过来了,⽤户进程⾃⼰会亲⾃回收内存。
根据这个回收规则我们可以看到,我们可以提⾼ min 或者 low ⽔位的值,让 kswapd 早点起来⼲活,这样就可以避免回收不过来⽽导致应⽤直接回收内存。
通过查看 /proc/zoneinfo 可以看到你当前服务器上 min、low、high 这三条⽔位线分别是多少。值得注意的是,每个 zone 都有⾃⼰的三条⽔位线,以⻚⾯(4KB)为单位进⾏输出。
# cat /proc/zoneinfo | grep -E "Node|min|low|high "
......
Node 0, zone DMA
min 5
low 8
high 11
Node 0, zone DMA32
min 1004
low 1736
high 2468
Node 0, zone Normal
min 10236
low 17698
high 25160
...在内存分配时,只有 “low” 与 “min” 之间的这段区域才是 kswapd 的活动空间,低于了 “min” 会触发 direct reclaim,⾼于了 “low” ⼜不会唤醒 kswapd,⽽ Linux 中默认的 “low” 与 “min” 之间的差值确实显得⼩了点。
如果想提⾼ min ⽔位线的值,直接修改 min_free_kbytes 就可以。如果想把 min 提升到 1 GB,可以通过在 sysctl 中或者直接 echo 修改。
# echo 1000000 > /proc/sys/vm/min_free_kbytes设置后内核就会把 1 GB 的 min 值均匀地设置到各个 zone 上。
默认情况下,low 和 high 是根据 min 的值⾃动推算出来的。在 6.1 版本内核中:
low⽔位线 = 1.25 ×min⽔位线high⽔位线 = 1.5 ×min⽔位线
在现代服务器中,⼀般内存都很⼤,动不动就⼏百 GB。假如我们把 min 设置为 1 GB,那么 high 才区区 1.5 GB。不得不说,内核的这个默认值设置的还是抠搜了,min、low、high 三者的差距根本就没有拉开。
还好内核还提供了另外⼀个内核参数 watermark_scale_factor,⽤于控制 min、low 和 high 三条⽔线之间的间隔。watermark_scale_factor 越⼤,三条⽔位线之间的差距也就拉的越⼤。其计算规则如下:
low⽔位线 =min⽔位线 +watermark_scale_factor× total / 10000high⽔线线 =low⽔位线 +watermark_scale_factor× total / 10000
如果你决定了要修改 watermark_scale_factor 并且也计算好了它的值,可以通过在 sysctl 中或者直接 echo 修改。
sudo sh -c 'echo 150 > /proc/sys/vm/watermark_scale_factor'9.2.2 脏页刷盘控制
PageCache 中的⼲净⻚⾯(和磁盘⽂### 9.2.2 脏页刷盘控制
PageCache 中的⼲净⻚⾯(和磁盘⽂件中⼀致,⽆修改)释放的时候很快,但是对于脏⻚(内存中有修改)的情况就⽐较耗时了。因为脏⻚需要写⼊磁盘后才能释放。我们可以考虑减少在应⽤进程直接内存回收阶段时需要回收的脏⻚数量。这样就可以避免应⽤程序性能劣化过于严重。
我们来总结⼀下 PageCache 中脏⻚写回磁盘的时机。
第⼀种情况,如果 write 系统调⽤时,如果发现 PageCache 中脏⻚占⽐太多,超过了 dirty_ratio 或 dirty_bytes,write 需要同步写⼊磁盘。
第⼆种情况,内核线程异步运⾏的时候,判断脏⻚占⽐,如果超过了 dirty_background_ratio 或 dirty_background_bytes,也发起写回请求。
第三种情况,内核线程异步运⾏的时候,虽然系统内脏⻚⼀直没有超过 dirty_background_ratio 或 dirty_background_bytes,但是脏⻚在内存中呆的时间超过 dirty_expire_centisecs 了,也会发起回写。
相关的内核参数有如下⼏个:
dirty_bytes:触发同步写的脏数据量dirty_ratio:触发同步写的脏数据占可⽤内存的⽐例dirty_writeback_centisecs:回刷进程定时唤醒时间(单位是百分之⼀秒)dirty_background_bytes:触发回刷的脏数据量dirty_background_ratio:触发回刷的脏数据占可⽤内存的⽐例dirty_expire_centisecs:脏数据超时回刷时间(单位是百分之⼀秒)
现在我们假设我们的应⽤场景是在线服务,希望尽可能让应⽤程序尽可能快地处理,避免⻓时间的卡顿导致⽤户体验不佳。那么我们可以考虑如下⻆度的性能优化。
第⼀个优化思路:让内核线程多刷⼀些数据,把脏数据⽐例保持在⼀个⽐较低的⽔平。这样对应⽤带来的耗时优化是能避免在线上可⽤内存低于 min 进⾏直接内存回收时脏⻚内存刷盘过慢的问题。对应要调整 dirty_background_ratio,默认值是 10 会导致内核线程异步刷盘不彻底,把它设置的⼩⼀些,例如 5。
第⼆个优化思路:避免应⽤进程同步写盘⽽导致的应⽤耗时过⻓。dirty_ratio 这个内核参数控制的是应⽤进程同步刷盘的⽐例。如果耗时敏感的场景,建议把该值设置的⾼⼀些。该参数默认是 20,可以考虑设置到 30 - 50 之间。这样把脏⻚都交给内核线程去刷新。
第三个优化思路:如果能保证 PageCache 内存的充⾜,不被频繁回收。这样也可以考虑将 dirty_background_ratio、dirty_ratio 设置到较⾼,dirty_expire_centisecs 周期也设置到⽐较⻓。带来的效果是脏⻚在内存⾥待的时间⽐较⻓,会⾃带合并写,可以极⼤减少磁盘 IO 的发⽣。
9.2.3 内存规整
在慢速路径中,应⽤也有概率会执⾏内存规整,这也是⼀个相当耗时的操作。
我们也可以配置内核参数,尽量让内核线程来做内存规整,⽽不是让应⽤程序来做。具体⽅法是调整 /proc/sys/vm/extfrag_threshold 内核参数。这是内核线程在后台异步判断是否要启动内存规整的阈值。当内核中判断碎⽚率超过 extfrag_threshold 这个值时,内核就会启动内存规整。
该值设置的越低,内核做内存规整操作就会越频繁,也就能避免应⽤陷⼊内存规整的耗时中。该值推荐设置为 200 左右,但具体还要结合你⾃⼰的需求来。
注意
内存规整涉及到内存拷⻉,⽽拷⻉本身也是⾮常消耗性能的,所以也不宜设的过低。
9.2.4 闲时处理
还有⼀个优化思路是在业务空闲的期间主动发起定时任务执⾏内存 PageCache 回收,以及内存规整。
这样可以避免服务器在⾼峰期处理这些操作的压⼒。但这么做事先⼀定要评估好才可以实施。
echo 1 > /proc/sys/vm/compact_memory
echo 3 > /proc/sys/vm/drop_caches