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

对上述的设置文件,有几点要弥补的:

  1. XXX_ratio 和 XXX_bytes 是同一个设置属性的差异盘算方式,优先级 XXX_bytes > XXX_ratio
  2. 可用内存并不是系统所有内存,而是free pages + reclaimable pages
  3. 脏数据超时示意内存中数据标识脏一准时间后,下次回刷历程事情时就必须回刷
  4. 回刷历程既会准时叫醒,也会在脏数据过多时被动叫醒。
  5. dirty_background_XXX与dirty_XXX的差异在于前者只是叫醒回刷历程,此时应用依然可以异步写数据到Cache,当脏数据比例继续增添,触发dirty_XXX的条件,不再支持应用异步写。

关于同步与异步IO的说明,可以看另一篇博客《Linux IO模子》

更完整的功效先容,可以看内核文档Documentation/sysctl/vm.txt

设置示例

单纯的设置说明究竟太抽象。连系网上的分享,我们看看在差异场景下,该若何设置?

场景1:尽可能不丢数据

有些产物形态的数据非常主要,例如行车记录仪。在知足性能要求的情况下,要做到尽可能不丢失数据。

/* 此设置不一定适合您的产物,请凭据您的实际情况设置 */
dirty_background_ratio = 5
dirty_ratio = 10
dirty_writeback_centisecs = 50
dirty_expire_centisecs = 100

这样的设置有以下特点:

  1. 当脏数据到达可用内存的5%时叫醒回刷历程
  2. 当脏数据到达可用内存的10%时,应用每一笔数据都必须同步守候
  3. 每隔500ms叫醒一次回刷历程
  4. 内存中脏数据存在时间跨越1s则在下一次叫醒时回刷

由于发生交通事故时,行车记录仪随时可能断电,事故前1~2s的数据尤为要害。因此在保证性能知足不丢帧的情况下,尽可能回刷数据。

此设置通过削减Cache加倍频仍叫醒回刷历程的方式,尽可能让数据回刷。

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

场景2:追求更高性能

有些产物形态不太可能会掉电,例如服务器。此时不需要思量数据平安问题,要做到尽可能高的IO性能。

/* 此设置不一定适合您的产物,请凭据您的实际情况设置 */
dirty_background_ratio = 50
dirty_ratio = 80
dirty_writeback_centisecs = 2000
dirty_expire_centisecs = 12000

这样的设置有以下特点:

  1. 当脏数据到达可用内存的50%时叫醒回刷历程
  2. 当脏数据到达可用内存的80%时,应用每一笔数据都必须同步守候
  3. 每隔20s叫醒一次回刷历程
  4. 内存中脏数据存在时间跨越120s则在下一次叫醒时回刷

与场景1相比,场景2的设置通过 增大Cache延迟回刷叫醒时间来尽可能缓存更多数据,进而实现提高性能

场景3:突然的IO峰值拖慢整体性能

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

/* 此设置不一定适合您的产物,请凭据您的实际情况设置 */
dirty_background_ratio = 5
dirty_ratio = 80
dirty_writeback_centisecs = 500
dirty_expire_centisecs = 3000

这样的设置有以下特点:

  1. 当脏数据到达可用内存的5%时叫醒回刷历程
  2. 当脏数据到达可用内存的80%时,应用每一笔数据都必须同步守候
  3. 每隔5s叫醒一次回刷历程
  4. 内存中脏数据存在时间跨越30s则在下一次叫醒时回刷

这样的设置,通过 增大Cache总容量加倍频仍叫醒回刷的方式,解决IO峰值的问题,此时能保证脏数据比例保持在一个比较低的水平,当突然泛起峰值,也有足够的Cache来缓存数据。

内核代码实现

知其然,亦要知其以是然。翻看内核代码,寻找设置的实现,细细品味差异设置的细微差异。

基于内核代码版本:5.5.15

IBN-Net: 提升模型的域自适应性

sysctl文件

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

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设置项的信息,实际上修改了对应的某个全局变量的值。

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

<mm/page-writeback.c>

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的内核历程

$ ps aux | grep "writeback"
root        40  0.0  0.0      0     0 ?        I<   06:44   0:00 [writeback]

这实际上是一个事情行列对应的历程,在default_bdi_init()中建立。

 /* 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()绑定。

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);
	...
}

叫醒回刷历程的操作是这样的

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()的最后,有这样的代码:

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_intervalwb_wakeup_delayed()中起作用,在wb_workfn()的最后凭据dirty_writeback_interval设置下一次叫醒时间。

我们还发现通过msecs_to_jiffies(XXX * 10)来换算单元,示意dirty_writeback_interval乘以10之后的计量单元才是毫秒msecs。怪不得说dirty_writeback_centisecs的单元是1/100秒。

脏数据量

脏数据量通过dirty_background_XXXdirty_XXX示意,他们又是怎么事情的呢?

凭据kernel/sysctl.c的内容,我们知道dirty_background_XXX设置项对应的全局变量是dirty_background_XXXdirty_XXX对于的全局变量是 vm_dirty_XXX

我们把目光聚焦到函数domain_dirty_limits(),通过这个函数换算脏数据阈值。

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;
}

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

  1. dirty_background_bytes/dirty_bytes的优先级高于dirty_background_ratio/dirty_ratio
  2. dirty_background_bytes/ratio和dirty_bytes/ratio最终会统一换算成做计量单元
  3. dirty_background_bytes/dirty_bytes做进一除法,示意若是值为4097Bytes,换算后是2页
  4. dirty_background_ratio/dirty_ratio相乘的基数是available_memory,示意可用内存
  5. 若是dirty_background_XXX大于dirty_XXX,则取dirty_XXX的一半

可用内存是怎么盘算来的呢?

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 */
}

以是,

可用内存 = 空闲页 - 内核预留页 + 流动文件页 + 非流动文件页 ( - 高端内存)

脏数据到达阈值后是怎么触发回刷的呢?我们再看balance_dirty_pages()函数

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);
}

总结下有以下特征:

  1. 可接纳内存 = 文件脏页 + 文件系统不稳定页(NFS)
  2. 可接纳内存到达dirty_background_XXX盘算的阈值,只是叫醒脏数据回刷事情后直接返回,并不会守候接纳完成,最终接纳事情照样看writeback历程

原创文章,作者:2d28新闻网,如若转载,请注明出处:https://www.2d28.com/archives/4510.html