Linux 脏数据回刷参数与调优

简介:

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:尽可能不丢数据#
有些产品形态的数据非常重要,例如行车记录仪。在满足性能要求的情况下,要做到尽可能不丢失数据。

Copy
/ 此配置不一定适合您的产品,请根据您的实际情况配置 /
dirty_background_ratio = 5
dirty_ratio = 10
dirty_writeback_centisecs = 50
dirty_expire_centisecs = 100
这样的配置有以下特点:

当脏数据达到可用内存的5%时唤醒回刷进程
当脏数据达到可用内存的10%时,应用每一笔数据都必须同步等待
每隔500ms唤醒一次回刷进程
内存中脏数据存在时间超过1s则在下一次唤醒时回刷
由于发生交通事故时,行车记录仪随时可能断电,事故前1~2s的数据尤为关键。因此在保证性能满足不丢帧的情况下,尽可能回刷数据。

此配置通过减少Cache,更加频繁唤醒回刷进程的方式,尽可能让数据回刷。

此时的性能理论上会比每笔数据都O_SYNC略高,比默认配置性能低,相当于用性能换数据安全。

场景2:追求更高性能#
有些产品形态不太可能会掉电,例如服务器。此时不需要考虑数据安全问题,要做到尽可能高的IO性能。

Copy
/ 此配置不一定适合您的产品,请根据您的实际情况配置 /
dirty_background_ratio = 50
dirty_ratio = 80
dirty_writeback_centisecs = 2000
dirty_expire_centisecs = 12000
这样的配置有以下特点:

当脏数据达到可用内存的50%时唤醒回刷进程
当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
每隔20s唤醒一次回刷进程
内存中脏数据存在时间超过120s则在下一次唤醒时回刷
与场景1相比,场景2的配置通过 增大Cache,延迟回刷唤醒时间来尽可能缓存更多数据,进而实现提高性能

场景3:突然的IO峰值拖慢整体性能#
什么是IO峰值?突然间大量的数据写入,导致瞬间IO压力飙升,导致瞬间IO性能狂跌,对行车记录仪而言,有可能触发视频丢帧。

Copy
/ 此配置不一定适合您的产品,请根据您的实际情况配置 /
dirty_background_ratio = 5
dirty_ratio = 80
dirty_writeback_centisecs = 500
dirty_expire_centisecs = 3000
这样的配置有以下特点:

当脏数据达到可用内存的5%时唤醒回刷进程
当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
每隔5s唤醒一次回刷进程
内存中脏数据存在时间超过30s则在下一次唤醒时回刷
这样的配置,通过 增大Cache总容量,更加频繁唤醒回刷的方式,解决IO峰值的问题,此时能保证脏数据比例保持在一个比较低的水平,当突然出现峰值,也有足够的Cache来缓存数据。

内核代码实现#
知其然,亦要知其所以然。翻看内核代码,寻找配置的实现,细细品味不同配置的细微差别。

基于内核代码版本:5.5.15

sysctl文件#
在 kernel/sysctl.c中列出了所有的配置文件的信息。

Copy
static struct ctl_table vm_table[] = {

...
{
    .procname    = "dirty_background_ratio",
    .data        = &dirty_background_ratio,
    .maxlen        = sizeof(dirty_background_ratio),
    .mode        = 0644,
    .proc_handler    = dirty_background_ratio_handler,
    .extra1        = &zero,
    .extra2        = &one_hundred,
},
{
    .procname    = "dirty_ratio",
    .data        = &vm_dirty_ratio,
    .maxlen        = sizeof(vm_dirty_ratio),
    .mode        = 0644,
    .proc_handler    = dirty_ratio_handler,
    .extra1        = &zero,
    .extra2        = &one_hundred,
},
{
    .procname    = "dirty_writeback_centisecs",
    .data        = &dirty_writeback_interval,
    .maxlen        = sizeof(dirty_writeback_interval),
    .mode        = 0644,
    .proc_handler    = dirty_writeback_centisecs_handler,
},

}
为了避免文章篇幅过大,我只列出了关键的3个配置项且不深入代码如何实现。

我们只需要知道,我们修改/proc/sys/vm配置项的信息,实际上修改了对应的某个全局变量的值。

每个全局变量都有默认值,追溯这些全局变量的定义

Copy

int dirty_background_ratio = 10;
unsigned long dirty_background_bytes;
int vm_dirty_ratio = 20;
unsigned long vm_dirty_bytes;
unsigned int dirty_writeback_interval = 5 100; / centiseconds */
unsigned int dirty_expire_interval = 30 100; / centiseconds */
总结如下:

配置项名 对应源码变量名 默认值
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的内核进程

Copy
$ ps aux | grep "writeback"
root 40 0.0 0.0 0 0 ? I< 06:44 0:00 [writeback]
这实际上是一个工作队列对应的进程,在default_bdi_init()中创建。

Copy
/ bdi_wq serves all asynchronous writeback tasks /
struct workqueue_struct *bdi_wq;

static int __init default_bdi_init(void)
{

...
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |
        WQ_UNBOUND | WQ_SYSFS, 0);
...

}
回刷进程的核心是函数wb_workfn(),通过函数wb_init()绑定。

Copy
static int wb_init(struct bdi_writeback wb, struct backing_dev_info bdi

    int blkcg_id, gfp_t gfp)

{

...
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
...

}
唤醒回刷进程的操作是这样的

Copy
static void wb_wakeup(struct bdi_writeback *wb)
{

spin_lock_bh(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
    mod_delayed_work(bdi_wq, &wb->dwork, 0);
spin_unlock_bh(&wb->work_lock);

}
表示唤醒的回刷任务在工作队列writeback中执行,这样,就把工作队列和回刷工作绑定了。

我们暂时不探讨每次会回收了什么,关注点在于相关配置项怎么起作用。在wb_workfn()的最后,有这样的代码:

Copy
void wb_workfn(struct work_struct *work)
{

...
/* 如果还有需要回收的内存,再次唤醒 */
if (!list_empty(&wb->work_list))
    wb_wakeup(wb);
/* 如果还有脏数据,延迟唤醒 */
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
    wb_wakeup_delayed(wb);

}

static void wb_wakeup(struct bdi_writeback *wb)
{

spin_lock_bh(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
    mod_delayed_work(bdi_wq, &wb->dwork, 0);
spin_unlock_bh(&wb->work_lock);

}

void wb_wakeup_delayed(struct bdi_writeback *wb)
{

unsigned long timeout;

/* 在这里使用dirty_writeback_interval,设置下次唤醒时间 */
timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
spin_lock_bh(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
    queue_delayed_work(bdi_wq, &wb->dwork, timeout);
spin_unlock_bh(&wb->work_lock);

}
根据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(),通过这个函数换算脏数据阈值。

Copy
static void domain_dirty_limits(struct dirty_throttle_control *dtc)
{

...
unsigned long bytes = vm_dirty_bytes;
unsigned long bg_bytes = dirty_background_bytes;
/* convert ratios to per-PAGE_SIZE for higher precision */
unsigned long ratio = (vm_dirty_ratio * PAGE_SIZE) / 100;
unsigned long bg_ratio = (dirty_background_ratio * PAGE_SIZE) / 100;
...
if (bytes)
    thresh = DIV_ROUND_UP(bytes, PAGE_SIZE);
else
    thresh = (ratio * available_memory) / PAGE_SIZE;

if (bg_bytes)
    bg_thresh = DIV_ROUND_UP(bg_bytes, PAGE_SIZE);
else
    bg_thresh = (bg_ratio * available_memory) / PAGE_SIZE;

if (bg_thresh >= thresh)
    bg_thresh = thresh / 2;

dtc->thresh = thresh;
dtc->bg_thresh = bg_thresh;

}
上面的代码体现了如下的特征

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的一半
可用内存是怎么计算来的呢?

Copy
static unsigned long global_dirtyable_memory(void)
{

unsigned long x;

x = global_zone_page_state(NR_FREE_PAGES);
/*
 * Pages reserved for the kernel should not be considered
 * dirtyable, to prevent a situation where reclaim has to
 * clean pages in order to balance the zones.
 */
 
 x += global_node_page_state(NR_INACTIVE_FILE);
 x += global_node_page_state(NR_ACTIVE_FILE); 
 
 if (!vm_highmem_is_dirtyable)
     x -= highmem_dirtyable_memory(x);
 
 return x + 1; /* Ensure that we never return 0 */

}
所以,

Copy
可用内存 = 空闲页 - 内核预留页 + 活动文件页 + 非活动文件页 ( - 高端内存)
脏数据达到阈值后是怎么触发回刷的呢?我们再看balance_dirty_pages()函数

Copy
static void balance_dirty_pages(struct bdi_writeback *wb,

            unsigned long pages_dirtied)

{

unsigned long nr_reclaimable;   /* = file_dirty + unstable_nfs */
...
/*
 * Unstable writes are a feature of certain networked
 * filesystems (i.e. NFS) in which data may have been
 * written to the server's write cache, but has not yet
 * been flushed to permanent storage.
 */
nr_reclaimable = global_node_page_state(NR_FILE_DIRTY) +
                global_node_page_state(NR_UNSTABLE_NFS);
...
if (nr_reclaimable > gdtc->bg_thresh)
    wb_start_background_writeback(wb);

}

void wb_start_background_writeback(struct bdi_writeback *wb)
{

wb_wakeup(wb);

}
总结下有以下特征:

可回收内存 = 文件脏页 + 文件系统不稳定页(NFS)
可回收内存达到dirty_background_XXX计算的阈值,只是唤醒脏数据回刷工作后直接返回,并不会等待回收完成,最终回收工作还是看writeback进程
作者: 广漠飘羽

出处:https://www.cnblogs.com/gmpy/p/12657801.html

相关文章
|
15天前
|
监控 Unix Linux
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
29 0
|
2月前
|
缓存 运维 Linux
Linux系统调优详解(十二)——IO调优之磁盘测速
Linux系统调优详解(十二)——IO调优之磁盘测速
58 1
|
2月前
|
运维 监控 数据可视化
Linux系统调优详解(六)——网络状态查看命令nload
Linux系统调优详解(六)——网络状态查看命令nload
81 5
|
2月前
|
缓存 运维 Linux
Linux系统调优详解(三)——CPU状态查看相关命令
Linux系统调优详解(三)——CPU状态查看相关命令
41 1
|
2月前
|
运维 Linux
Linux系统调优详解(二)——CPU负载查看相关命令
Linux系统调优详解(二)——CPU负载查看相关命令
51 10
|
2月前
|
缓存 运维 Linux
Linux系统调优详解(一)——系统调优概述与Top命令详解
Linux系统调优详解(一)——系统调优概述与Top命令详解
52 4
|
1月前
|
存储 缓存 Linux
【Shell 命令集合 磁盘维护 】Linux 设置和查看硬盘驱动器参数 hdparm命令使用教程
【Shell 命令集合 磁盘维护 】Linux 设置和查看硬盘驱动器参数 hdparm命令使用教程
35 0
|
15天前
|
Linux
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
21 0
|
1月前
|
网络协议 Linux API
Linux网络编程:shutdown() 与 close() 函数详解:剖析 shutdown()、close() 函数的实现原理、参数说明和使用技巧
Linux网络编程:shutdown() 与 close() 函数详解:剖析 shutdown()、close() 函数的实现原理、参数说明和使用技巧
77 0
|
2月前
|
运维 Linux
Linux系统调优详解(五)——磁盘IO状态查看命令
Linux系统调优详解(五)——磁盘IO状态查看命令
51 5