1. 程式人生 > >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 | 對上述的配置檔案,有幾點要補充的: 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模型》](https://www.cnblogs.com/gmpy/p/12652578.html) 更完整的功能介紹,可以看核心文件```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```配置項的資訊,實際上修改了對應的某個全域性變數的值。 每個全域性變數都有預設值,追溯這些全域性變數的定義 ```
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_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; } ``` 上面的程式碼體現瞭如下的特徵 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```程序