天天看點

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性能。

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性能狂跌,對行車記錄儀而言,有可能觸發視訊丢幀。

dirty_writeback_centisecs = 500

dirty_expire_centisecs = 3000

每隔5s喚醒一次回刷程序

記憶體中髒資料存在時間超過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配置項的資訊,實際上修改了對應的某個全局變量的值。

每個全局變量都有預設值,追溯這些全局變量的定義

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);           
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(),通過這個函數換算髒資料門檻值。

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的一半

可用記憶體是怎麼計算來的呢?

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

總結下有以下特征:

可回收記憶體 = 檔案髒頁 + 檔案系統不穩定頁(NFS)

可回收記憶體達到dirty_background_XXX計算的門檻值,隻是喚醒髒資料回刷工作後直接傳回,并不會等待回收完成,最終回收工作還是看writeback程序

作者: 廣漠飄羽

出處:

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

繼續閱讀