pdflush機制


在做進程安全監控的時候,拍腦袋決定的,如果發現一個進程在D狀態時,即TASK_UNINTERRUPTIBLE(不可中斷的睡眠狀態),時間超過了8min,就將系統panic掉。恰好DB組做日志時,將整個log緩存到內存中,最后刷磁盤,結果系統就D狀態了很長時間,自然panic了,中間涉及到Linux的緩存寫回刷磁盤的一些機制和調優方法,寫一下總結。

目前機制需要將臟頁刷回到磁盤一般是以下情況:

  1. 臟頁緩存占用的內存太多,內存空間不足;
  2. 臟頁已經更改了很長時間,時間上已經到了臨界值,需要及時刷新保持內存和磁盤上數據一致性;
  3. 外界命令強制刷新臟頁到磁盤
  4. write寫磁盤時檢查狀態刷新

內核使用pdflush線程刷新臟頁到磁盤,pdflush線程個數在2和8之間,可以通過/proc/sys/vm/nr_pdflush_threads文件直接查看,具體策略機制參看源碼函數__pdflush。

一、內核其他模塊強制刷新

先說一下第一種和第三種情況:當內存空間不足或外界強制刷新的時候,臟頁的刷新是通過調用wakeup_pdflush函數實現的,調用其函數的有do_sync、free_more_memory、try_to_free_pages。wakeup_pdflush的功能是通過background_writeout的函數實現的:

static void background_writeout(unsigned long _min_pages)
{
     long min_pages = _min_pages;
     struct writeback_control wbc = {
         .bdi = NULL,
         .sync_mode = WB_SYNC_NONE,
         .older_than_this = NULL,
         .nr_to_write = 0,
         .nonblocking = 1,
    };
 
    for ( ; ; ) {
         struct writeback_state wbs;
         long background_thresh;
         long dirty_thresh;
 
         get_dirty_limits(&wbs, &background_thresh, &dirty_thresh, NULL);
         if (wbs.nr_dirty + wbs.nr_unstable < background_thresh
             && min_pages <= 0)
             break;
         wbc.encountered_congestion = 0;
         wbc.nr_to_write = MAX_WRITEBACK_PAGES;
         wbc.pages_skipped = 0;
         writeback_inodes(&wbc);
         min_pages -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
         if (wbc.nr_to_write > 0 || wbc.pages_skipped > 0) {
              /* Wrote less than expected */
              blk_congestion_wait(WRITE, HZ/10);
              if (!wbc.encountered_congestion)
                  break;
         }
   }
}

background_writeout進到一個死循環里面,通過get_dirty_limits獲取臟頁開始刷新的臨界值background_thresh,即為dirty_background_ratio的總內存頁數百分比,可以通過proc接口/proc/sys/vm/dirty_background_ratio調整,一般默認為10。當臟頁超過臨界值時,調用writeback_inodes寫MAX_WRITEBACK_PAGES(1024)個頁,直到臟頁比例低於臨界值。

二、內核定時器啟動刷新

內核在啟動的時候在page_writeback_init初始化wb_timer定時器,超時時間是dirty_writeback_centisecs,單位是0.01秒,可以通過/proc/sys/vm/dirty_writeback_centisecs調節。wb_timer的觸發函數是wb_timer_fn,最終是通過wb_kupdate實現。

static void wb_kupdate(unsigned long arg)
{
    sync_supers();
    get_writeback_state(&wbs);
    oldest_jif = jiffies - (dirty_expire_centisecs * HZ) / 100;
    start_jif = jiffies;
    next_jif = start_jif + (dirty_writeback_centisecs * HZ) / 100;
    nr_to_write = wbs.nr_dirty + wbs.nr_unstable +
    (inodes_stat.nr_inodes - inodes_stat.nr_unused);
    while (nr_to_write > 0) {
        wbc.encountered_congestion = 0;
        wbc.nr_to_write = MAX_WRITEBACK_PAGES;
        writeback_inodes(&wbc);
        if (wbc.nr_to_write > 0) {
            if (wbc.encountered_congestion)
                blk_congestion_wait(WRITE, HZ/10);
            else
                break; /* All the old data is written */
        }
        nr_to_write -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
    }
    if (time_before(next_jif, jiffies + HZ))
    next_jif = jiffies + HZ;
    if (dirty_writeback_centisecs)
    mod_timer(&wb_timer, next_jif);
 }

上面的代碼沒有拷貝全。內核首先將超級塊信息刷新到文件系統上,然后獲取oldest_jif作為wbc的參數只刷新已修改時間大於dirty_expire_centisecs的臟頁,dirty_expire_centisecs參數可以通過/proc/sys/vm/dirty_expire_centisecs調整。

三、WRITE寫文件刷新緩存

用戶態使用WRITE函數寫文件時也有可能要刷新臟頁,generic_file_buffered_write函數會在將寫的內存頁標記為臟之后,根據條件刷新磁盤以平衡當前臟頁比率,參看balance_dirty_pages_ratelimited函數:

void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
    static DEFINE_PER_CPU(int, ratelimits) = 0;
    long ratelimit;
 
    ratelimit = ratelimit_pages;
    if (dirty_exceeded)
        ratelimit = 8;
 
    /*
     * Check the rate limiting. Also, we do not want to throttle real-time
     * tasks in balance_dirty_pages(). Period.
     */
    if (get_cpu_var(ratelimits)++ >= ratelimit) {
        __get_cpu_var(ratelimits) = 0;
        put_cpu_var(ratelimits);
        balance_dirty_pages(mapping);
        return;
    }
    put_cpu_var(ratelimits);
}

balance_dirty_pages_ratelimited函數通過ratelimit_pages調節刷新(調用balance_dirty_pages函數)的次數,每ratelimit_pages次調用才會刷新一次,具體刷新過程看balance_dirty_pages函數:

static void balance_dirty_pages(struct address_space *mapping)
{
    struct writeback_state wbs;
    long nr_reclaimable;
    long background_thresh;
    long dirty_thresh;
    unsigned long pages_written = 0;
    unsigned long write_chunk = sync_writeback_pages();
 
    struct backing_dev_info *bdi = mapping->backing_dev_info;
 
    for (;;) {
        struct writeback_control wbc = {
            .bdi        = bdi,
            .sync_mode  = WB_SYNC_NONE,
            .older_than_this = NULL,
            .nr_to_write    = write_chunk,
        };
 
        get_dirty_limits(&wbs, &background_thresh,
                    &dirty_thresh, mapping);
        nr_reclaimable = wbs.nr_dirty + wbs.nr_unstable;
        if (nr_reclaimable + wbs.nr_writeback <= dirty_thresh)
            break;
 
        if (!dirty_exceeded)
            dirty_exceeded = 1;
 
        /* Note: nr_reclaimable denotes nr_dirty + nr_unstable.
         * 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.
         */
        if (nr_reclaimable) {
            writeback_inodes(&wbc);
            get_dirty_limits(&wbs, &background_thresh,
                    &dirty_thresh, mapping);
            nr_reclaimable = wbs.nr_dirty + wbs.nr_unstable;
            if (nr_reclaimable + wbs.nr_writeback <= dirty_thresh)
                break;
            pages_written += write_chunk - wbc.nr_to_write;
            if (pages_written >= write_chunk)
                break;      /* We've done our duty */
        }
        blk_congestion_wait(WRITE, HZ/10);
    }
 
    if (nr_reclaimable + wbs.nr_writeback <= dirty_thresh && dirty_exceeded)
        dirty_exceeded = 0;
 
    if (writeback_in_progress(bdi))
        return;     /* pdflush is already working this queue */
 
    /*
     * In laptop mode, we wait until hitting the higher threshold before
     * starting background writeout, and then write out all the way down
     * to the lower threshold.  So slow writers cause minimal disk activity.
     *
     * In normal mode, we start background writeout at the lower
     * background_thresh, to keep the amount of dirty memory low.
     */
    if ((laptop_mode && pages_written) ||
         (!laptop_mode && (nr_reclaimable > background_thresh)))
        pdflush_operation(background_writeout, 0);
}

函數走進一個死循環,通過get_dirty_limits獲取dirty_background_ratio和dirty_ratio對應的內存頁數值,當24行做判斷,如果臟頁大於dirty_thresh,則調用writeback_inodes開始刷緩存到磁盤,如果一次沒有將臟頁比率刷到dirty_ratio之下,則用blk_congestion_wait阻塞寫,然后反復循環,直到比率降低到dirty_ratio;當比率低於dirty_ratio之后,但臟頁比率大於dirty_background_ratio,則用pdflush_operation啟用background_writeout,pdflush_operation是非阻塞函數,喚醒pdflush后直接返回,background_writeout在有pdflush調用。

如此可知:WRITE寫的時候,緩存超過dirty_ratio,則會阻塞寫操作,回刷臟頁,直到緩存低於dirty_ratio;如果緩存高於background_writeout,則會在寫操作時,喚醒pdflush進程刷臟頁,不阻塞寫操作。

四,問題總結

導致進程D狀態大部分是因為第3種和第4種情況:有大量寫操作,緩存由Linux系統管理,一旦臟頁累計到一定程度,無論是繼續寫還是fsync刷新,都會使進程D住。

 

 

系統緩存相關的幾個內核參數 (還有2個是指定bytes的,含義和ratio差不多):
1.         /proc/sys/vm/dirty_background_ratio
該文件表示臟數據到達系統整體內存的百分比,此時觸發pdflush進程把臟數據寫回磁盤。
缺省設置:10
當用戶調用write時,如果發現系統中的臟數據大於這閾值(或dirty_background_bytes ),會觸發pdflush進程去寫臟數據,但是用戶的write調用會立即返回,無需等待。pdflush刷臟頁的標准是讓臟頁降低到該閾值以下。
即使cgroup限制了用戶進程的IOPS,也無所謂。
2.         /proc/sys/vm/dirty_expire_centisecs
該文件表示如果臟數據在內存中駐留時間超過該值,pdflush進程在下一次將把這些數據寫回磁盤。
缺省設置:3000(1/100秒)
3.         /proc/sys/vm/dirty_ratio
該文件表示如果進程產生的臟數據到達系統整體內存的百分比,此時用戶進程自行把臟數據寫回磁盤。
缺省設置:40
當用戶調用write時,如果發現系統中的臟數據大於這閾值(或dirty_bytes ),需要自己把臟數據刷回磁盤,降低到這個閾值以下才返回。
注意,此時如果cgroup限制了用戶進程的IOPS,那就悲劇了。
4.         /proc/sys/vm/dirty_writeback_centisecs
該文件表示pdflush進程的喚醒間隔,周期性把超過dirty_expire_centisecs時間的臟數據寫回磁盤。
缺省設置:500(1/100秒)
系統一般在下面三種情況下回寫dirty頁:
1.      定時方式: 定時回寫是基於這樣的原則:/proc/sys/vm/dirty_writeback_centisecs的值表示多長時間會啟動回寫線程,由這個定時器啟動的回寫線程只回寫在內存中為dirty時間超過(/proc/sys/vm/dirty_expire_centisecs / 100)秒的頁(這個值默認是3000,也就是30秒),一般情況下dirty_writeback_centisecs的值是500,也就是5秒,所以默認情況下系統會5秒鍾啟動一次回寫線程,把dirty時間超過30秒的頁回寫,要注意的是,這種方式啟動的回寫線程只回寫超時的dirty頁,不會回寫沒超時的dirty頁,可以通過修改/proc中的這兩個值,細節查看內核函數wb_kupdate。
2.      內存不足的時候: 這時並不將所有的dirty頁寫到磁盤,而是每次寫大概1024個頁面,直到空閑頁面滿足需求為止
3.      寫操作時發現臟頁超過一定比例:
當臟頁占系統內存的比例超過/proc/sys/vm/dirty_background_ratio 的時候,write系統調用會喚醒pdflush回寫dirty page,直到臟頁比例低於/proc/sys/vm/dirty_background_ratio,但write系統調用不會被阻塞,立即返回.
當臟頁占系統內存的比例超/proc/sys/vm/dirty_ratio的時候, write系統調用會被被阻塞,主動回寫dirty page,直到臟頁比例低於/proc/sys/vm/dirty_ratio
大數據量項目中的感觸:
1  如果寫入量巨大,不能期待系統緩存的自動回刷機制,最好采用應用層調用fsync或者sync。如果寫入量大,甚至超過了系統緩存自動刷回的速度,就有可能導致系統的臟頁率超過/proc/sys/vm/dirty_ratio, 這個時候,系統就會阻塞后續的寫操作,這個阻塞有可能有5分鍾之久,是我們應用無法承受的。因此,一種建議的方式是在應用層,在合適的時機調用fsync。
2  對於關鍵性能,最好不要依賴於系統cache的作用,如果對性能的要求比較高,最好在應用層自己實現cache,因為系統cache受外界影響太大,說不定什么時候,系統cache就被沖走了。
3  在logic設計中,發現一種需求使用系統cache實現非常合適,對於logic中的高樓貼,在應用層cache實現非常復雜,而其數量又非常少,這部分請求,可以依賴於系統cache發揮作用,但需要和應用層cache相配合,應用層cache可以cache住絕大部分的非高樓貼的請求,做到這一點后,整個程序對系統的io就主要在高樓貼這部分了。這種情況下,系統cache可以做到很好的效果。
 
磁盤預讀:
關於預讀摘錄如下兩段:
預讀算法概要
1.順序性檢測
為了保證預讀命中率,Linux只對順序讀(sequential read)進行預讀。內核通過驗證如下兩個條件來判定一個read()是否順序讀:
◆這是文件被打開后的第一次讀,並且讀的是文件首部;
◆當前的讀請求與前一(記錄的)讀請求在文件內的位置是連續的。
如果不滿足上述順序性條件,就判定為隨機讀。任何一個隨機讀都將終止當前的順序序列,從而終止預讀行為(而不是縮減預讀大小)。注意這里的空間順序性說的是文件內的偏移量,而不是指物理磁盤扇區的連續性。在這里Linux作了一種簡化,它行之有效的基本前提是文件在磁盤上是基本連續存儲的,沒有嚴重的碎片化。
2.流水線預讀
當程序在處理一批數據時,我們希望內核能在后台把下一批數據事先准備好,以便CPU和硬盤能流水線作業。Linux用兩個預讀窗口來跟蹤當前順序流的預讀狀態:current窗口和ahead窗口。其中的ahead窗口便是為流水線准備的:當應用程序工作在current窗口時,內核可能正在ahead窗口進行異步預讀;一旦程序進入當前的ahead窗口,內核就會立即往前推進兩個窗口,並在新的ahead窗口中啟動預讀I/O。
3.預讀的大小
當確定了要進行順序預讀(sequential readahead)時,就需要決定合適的預讀大小。預讀粒度太小的話,達不到應有的性能提升效果;預讀太多,又有可能載入太多程序不需要的頁面,造成資源浪費。為此,Linux采用了一個快速的窗口擴張過程:
◆首次預讀: readahead_size = read_size * 2; // or *4
預讀窗口的初始值是讀大小的二到四倍。這意味着在您的程序中使用較大的讀粒度(比如32KB)可以稍稍提升I/O效率。
◆后續預讀: readahead_size *= 2;
后續的預讀窗口將逐次倍增,直到達到系統設定的最大預讀大小,其缺省值是128KB。這個缺省值已經沿用至少五年了,在當前更快的硬盤和大容量內存面前,顯得太過保守。
# blockdev –setra 2048 /dev/sda
當然預讀大小不是越大越好,在很多情況下,也需要同時考慮I/O延遲問題。
其他細節:
1.      pread 和pwrite
在多線程io操作中,對io的操作盡量使用pread和pwrite,否則,如果使用seek+write/read的方式的話,就需要在操作時加鎖。這種加鎖會直接造成多線程對同一個文件的操作在應用層就串行了。從而,多線程帶來的好處就被消除了。
使用pread方式,多線程也比單線程要快很多,可見pread系統調用並沒有因為同一個文件描述符而相互阻塞。pread和pwrite系統調用在底層實現中是如何做到相同的文件描述符而彼此之間不影響的?多線程比單線程的IOPS增高的主要因素在於調度算法。多線程做pread時相互未嚴重競爭是次要因素。
內核在執行pread的系統調用時並沒有使用inode的信號量,避免了一個線程讀文件時阻塞了其他線程;但是pwrite的系統調用會使用inode的信號量,多個線程會在inode信號量處產生競爭。pwrite僅將數據寫入cache就返回,時間非常短,所以競爭不會很強烈。
2.       文件描述符需要多套嗎?
在使用pread/pwrite的前提下,如果各個讀寫線程使用各自的一套文件描述符,是否還能進一步提升io性能?
每個文件描述符對應內核中一個叫file的對象,而每個文件對應一個叫inode的對象。假設某個進程兩次打開同一個文件,得到了兩個文件描述符,那么在內核中對應的是兩個file對象,但只有一個inode對象。文件的讀寫操作最終由inode對象完成。所以,如果讀寫線程打開同一個文件的話,即使采用各自獨占的文件描述符,但最終都會作用到同一個inode對象上。因此不會提升IO性能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM