本文转自广漠飘羽 - Linux 脏数据回刷参数与调优
简介
我们知道,Linux用cache/buffer缓存数据,且有个回刷任务在适当时候把脏数据回刷到存储介质中。什么是适当的时候?换句话说,什么时候触发回刷?是脏数据达到多少阈值还是定时触发,或者两者都有?
不同场景对触发回刷的时机的需求也不一样,对IO回刷触发时机的选择,是IO性能优化的一个重要方法。
Linux内核在/proc/sys/vm
中有透出数个配置文件,可以对触发回刷的时机进行调整。内核的回刷进程是怎么运作的呢?这数个配置文件有什么作用呢?
配置概述
在/proc/sys/vm
中有以下文件与回刷脏数据密切相关:
配置文件 | 功能 | 默认值 |
---|---|---|
dirty_background_ratio | 触发回刷的脏数据占可用内存的百分比 | 0 |
dirty_background_bytes | 触发回刷的脏数据量 | 10 |
dirty_bytes | 触发同步写的脏数据量 | 0 |
dirty_ratio | 触发同步写的脏数据占可用内存的百分比 | 20 |
dirty_expire_centisecs | 脏数据超时回刷时间(单位:1/100s) | 3000 |
dirty_writeback_centisecs | 回刷进程定时唤醒时间(单位:1/100s) | 500 |
对上述的配置文件,有几点要补充的:
- XXX_ratio 和 XXX_bytes 是同一个配置属性的不同计算方法,优先级 XXX_bytes > XXX_ratio
- 可用内存并不是系统所有内存,而是free pages + reclaimable pages
- 脏数据超时表示内存中数据标识脏一定时间后,下次回刷进程工作时就必须回刷
- 回刷进程既会定时唤醒,也会在脏数据过多时被动唤醒。
- dirty_background_XXX与dirty_XXX的差别在于前者只是唤醒回刷进程,此时应用依然可以异步写数据到Cache,当脏数据比例继续增加,触发dirty_XXX的条件,不再支持应用异步写。
关于同步与异步IO的说明,可以看另一篇博客《Linux IO模型》
更完整的功能介绍,可以看内核文档Documentation/sysctl/vm.txt
。
配置示例
单纯的配置说明毕竟太抽象。结合网上的分享,我们看看在不同场景下,该如何配置?
场景1:尽可能不丢数据
有些产品形态的数据非常重要,例如行车记录仪。在满足性能要求的情况下,要做到尽可能不丢失数据。
1 | /* 此配置不一定适合您的产品,请根据您的实际情况配置 */ |
这样的配置有以下特点:
- 当脏数据达到可用内存的5%时唤醒回刷进程
- 当脏数据达到可用内存的10%时,应用每一笔数据都必须同步等待
- 每隔500ms唤醒一次回刷进程
- 内存中脏数据存在时间超过1s则在下一次唤醒时回刷
由于发生交通事故时,行车记录仪随时可能断电,事故前1~2s的数据尤为关键。因此在保证性能满足不丢帧的情况下,尽可能回刷数据。
此配置通过减少Cache,更加频繁唤醒回刷进程的方式,尽可能让数据回刷。
此时的性能理论上会比每笔数据都O_SYNC
略高,比默认配置性能低,相当于用性能换数据安全。
场景2:追求更高性能
有些产品形态不太可能会掉电,例如服务器。此时不需要考虑数据安全问题,要做到尽可能高的IO性能。
1 | /* 此配置不一定适合您的产品,请根据您的实际情况配置 */ |
这样的配置有以下特点:
- 当脏数据达到可用内存的50%时唤醒回刷进程
- 当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
- 每隔20s唤醒一次回刷进程
- 内存中脏数据存在时间超过120s则在下一次唤醒时回刷
与场景1相比,场景2的配置通过 增大Cache,延迟回刷唤醒时间来尽可能缓存更多数据,进而实现提高性能
场景3:突然的IO峰值拖慢整体性能
什么是IO峰值?突然间大量的数据写入,导致瞬间IO压力飙升,导致瞬间IO性能狂跌,对行车记录仪而言,有可能触发视频丢帧。
1 | /* 此配置不一定适合您的产品,请根据您的实际情况配置 */ |
这样的配置有以下特点:
- 当脏数据达到可用内存的5%时唤醒回刷进程
- 当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
- 每隔5s唤醒一次回刷进程
- 内存中脏数据存在时间超过30s则在下一次唤醒时回刷
这样的配置,通过 增大Cache总容量,更加频繁唤醒回刷的方式,解决IO峰值的问题,此时能保证脏数据比例保持在一个比较低的水平,当突然出现峰值,也有足够的Cache来缓存数据。
内核代码实现
知其然,亦要知其所以然。翻看内核代码,寻找配置的实现,细细品味不同配置的细微差别。
基于内核代码版本:5.5.15
sysctl文件
在 kernel/sysctl.c中列出了所有的配置文件的信息。
1 | static struct ctl_table vm_table[] = { |
为了避免文章篇幅过大,我只列出了关键的3个配置项且不深入代码如何实现。
我们只需要知道,我们修改/proc/sys/vm
配置项的信息,实际上修改了对应的某个全局变量的值。
每个全局变量都有默认值,追溯这些全局变量的定义
1 | <mm/page-writeback.c> |
总结如下:
配置项名 | 对应源码变量名 | 默认值 |
---|---|---|
dirty_background_bytes | dirty_background_bytes | 0 |
dirty_background_ratio | dirty_background_ratio | 10 |
dirty_bytes | vm_dirty_bytes | 0 |
dirty_ratio | vm_dirty_ratio | 20 |
dirty_writeback_centisecs | dirty_writeback_interval | 500 |
dirty_expire_centisecs | dirty_expire_interval | 3000 |
回刷进程
通过ps aux
,我们总能看到writeback的内核进程
1 | ps aux | grep "writeback" |
这实际上是一个工作队列对应的进程,在default_bdi_init()
中创建。
1 | /* bdi_wq serves all asynchronous writeback tasks */ |
回刷进程的核心是函数wb_workfn()
,通过函数wb_init()
绑定。
1 | static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi |
唤醒回刷进程的操作是这样的
1 | static void wb_wakeup(struct bdi_writeback *wb) |
表示唤醒的回刷任务在工作队列writeback
中执行,这样,就把工作队列和回刷工作绑定了。
我们暂时不探讨每次会回收了什么,关注点在于相关配置项怎么起作用。在wb_workfn()
的最后,有这样的代码:
1 | void wb_workfn(struct work_struct *work) |
根据kernel/sysctl.c
的内容,我们知道dirty_writeback_centisecs
配置项对应的全局变量是dirty_writeback_interval
可以看到,dirty_writeback_interval
在wb_wakeup_delayed()
中起作用,在wb_workfn()
的最后根据dirty_writeback_interval
设置下一次唤醒时间。
我们还发现通过msecs_to_jiffies(XXX * 10)
来换算单位,表示dirty_writeback_interval
乘以10之后的计量单位才是毫秒msecs。怪不得说dirty_writeback_centisecs
的单位是1/100秒。
脏数据量
脏数据量通过dirty_background_XXX
和dirty_XXX
表示,他们又是怎么工作的呢?
根据kernel/sysctl.c
的内容,我们知道dirty_background_XXX
配置项对应的全局变量是dirty_background_XXX
,dirty_XXX
对于的全局变量是vm_dirty_XXX
。
我们把目光聚焦到函数domain_dirty_limits()
,通过这个函数换算脏数据阈值。
1 | static void domain_dirty_limits(struct dirty_throttle_control *dtc) |
上面的代码体现了如下的特征
- dirty_background_bytes/dirty_bytes的优先级高于dirty_background_ratio/dirty_ratio
- dirty_background_bytes/ratio和dirty_bytes/ratio最终会统一换算成页做计量单位
- dirty_background_bytes/dirty_bytes做进一除法,表示如果值为4097Bytes,换算后是2页
- dirty_background_ratio/dirty_ratio相乘的基数是available_memory,表示可用内存
- 如果dirty_background_XXX大于dirty_XXX,则取dirty_XXX的一半
可用内存是怎么计算来的呢?
1 | static unsigned long global_dirtyable_memory(void) |
所以,
1 | 可用内存 = 空闲页 - 内核预留页 + 活动文件页 + 非活动文件页 ( - 高端内存) |
脏数据达到阈值后是怎么触发回刷的呢?我们再看balance_dirty_pages()
函数
1 | static void balance_dirty_pages(struct bdi_writeback *wb, |
总结下有以下特征:
- 可回收内存 = 文件脏页 + 文件系统不稳定页(NFS)
- 可回收内存达到
dirty_background_XXX
计算的阈值,只是唤醒脏数据回刷工作后直接返回,并不会等待回收完成,最终回收工作还是看writeback
进程