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

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進程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章