天天看點

一個疑似slab洩漏問題排查

本文代碼基于linux核心4.19.195.

這周同僚回報一個疑似slab記憶體洩漏的問題,問題是這樣的。

同僚在做業務更新測試,不斷的更新24小時後發現,通過/proc/meminfo看到的slab記憶體占用比不跑更新測試的機器多了3.5G(整個系統隻有8G記憶體),懷疑是slab記憶體洩漏了。

找他要了一台不跑更新的機器和一台跑更新測試的機器對比着看,有如下發現:

  1. 通過free指令檢視,available确實少了2.6G
  2. 通過/proc/meminfo對比檢視,确實是slab相關的幾行有着較大的變化,其他條目變化不大
  3. 使用slabtop -s -c收集一下資料,好家夥,第一名和第二名都是和檔案相關的radix_tree_node和filp,分别占用了0.45G和0.27G,接下來3-7位分别是kmalloc-1024、ext4_inode_cache、buffer_head、vm_area_struct、task_struct,這幾個都是占用了0.2+G記憶體;另外,注意到一個細節,這幾個object的USE項都是0%,what?

先和同僚确認業務更新在做的操作,會有fork動作以及使用share memory的動作。

先不管那些細節,順着同僚的思路,如果radix_tree_node或者filp有洩漏,那麼我猜測有如下幾種可能:

  1. fork之後又重複打開了父程序已經打開了的檔案,導緻fd越開越多(進而filp越來越大)
  2. 某些檔案約寫約大,導緻radix_tree_node占用記憶體越來越多
  3. 某些檔案已經被删除了,但是因為引用計數的原因導緻删除動作遲遲沒有進行?

首先,調用

lsof -n | grep deleted
           

了一把,發現隻有11個檔案,每個隻有5M大小,55M應該不會導緻什麼問題吧;

然後,通過

for dir in `ls -d /proc/[0-9]*`;do num=`ls $dir/fd/ | wc -l`;comm=`cat $dir/comm`;echo "$comm -> $num" ;done
           

看看是不是有哪個程序打開了過多的檔案,發現最多的一個程序打開了820個fd,其餘都是100以下的。進入業務程序檢視,發現隻打開了42個fd。

再者,進入業務程序的/proc/pid/fd目錄下,執行

for file in `ls -l | awk '{print $NF}'`;do du -h $file;done
           

發現業務程序打開的最大的檔案是84M。

擷取了這麼多資料,好像沒什麼異常的,那麼是如何引發的slab記憶體洩漏呢?

回想起以前看過的slab記憶體洩漏的問題,現象與這個連結中是非常相似的,也就是某個slab對象占用的記憶體非常大(連結中占用的記憶體80+G),大到和其他對象有着明顯的差距。

重新看了slabtop的輸出,再次注意到頭幾個object的USE項都是0%這個細節,回想了一下slab的原理,突然有了一種想法:

slab記憶體不足時,會從buddy中申請記憶體,但是這些記憶體并不是把所有slab object歸還給slab時,就會歸還的buddy的,slub會保留一部分記憶體作為緩存,供下次申請時使用。如果業務更新流程将相關slub的緩存給打滿,那麼就會出現同僚回報的meminfo中顯示slab占用記憶體增加的現象,而且,slabtop中USE是0%(當然這個數值感覺不怎麼準)也能得到很好的解釋

剛好借此機會,複習一下slab中和這個緩存相關的代碼:

struct kmem_cache {
    ****
    unsigned long min_partial; //代表的是理論上kmem_cache在每個node節點上最多擁有的slab個數,若超過這個數量會嘗試釋放相關page給buddy
    #ifdef CONFIG_SLUB_CPU_PARTIAL
    /* Number of per cpu partial objects to keep around */
    unsigned int cpu_partial; //每個CPU節點上最多擁有的slab個數,若超過這個數量會嘗試将相關page釋放到node節點上
    #endif
    ****
}
           

在mm/slub.c的代碼中,slab的object釋放時會對進行kmem_cache中的門檻值進行判斷,進而确定slab是轉移到node節點上還是還給buddy,下面羅列與歸還到buddy相關的一小部分代碼

static void deactivate_slab(struct kmem_cache *s, struct page *page,
                void *freelist)
{   
    ****
    if (!new.inuse && n->nr_partial >= s->min_partial)
        m = M_FREE;
    ***
    }else if (m == M_FREE) {
        stat(s, DEACTIVATE_EMPTY);
        discard_slab(s, page);
        stat(s, FREE_SLAB);
    }
}

static void __unfreeze_partials(struct kmem_cache *s, struct page *partial_page)
{
    ****
    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
        page->next = discard_page;
        discard_page = page;
    }
    ******
}
           

從代碼中可以看到,必須是slab的數量超過了門檻值,才會把記憶體歸還給buddy,否則相關記憶體還是處在slab的管轄中。

理論上記憶體被slab緩存在自己的object中這個猜想是說的通了,那麼如何驗證呢?想了下有幾種方法:

  1. 繼續跑更新測試,看看再過一段時間這個slab占用是否持續增加或者比較平穩了,如果比較平穩了那麼猜想正确,否則核心代碼可能存在slab記憶體洩漏問題
  2. 既然同僚覺得slab占用記憶體太大,那麼找幾個重點使用者,shrink一下,看看slab記憶體是不是能夠降回原來的水準,如果可以,也能怎麼猜想正确
  3. 把slab緩存的門檻值調小,也就是不讓slab緩存太多記憶體,這樣slab如果增加的值變小,也是能夠怎麼猜想正确的

說到這,那麼怎麼去shrink slab,又怎麼去調slab緩存數量的門檻值呢?這個問題就交給核心文檔去解釋吧

What:       /sys/kernel/slab/cache/shrink
Date:       May 2007
KernelVersion:  2.6.22
Contact:    Pekka Enberg <[email protected].helsinki.fi>,
        Christoph Lameter <[email protected]-foundation.org>
Description:
        The shrink file is used to reclaim unused slab cache
        memory from a cache.  Empty per-cpu or partial slabs
        are freed and the partial list is sorted so the slabs
        with the fewest available objects are used first.
        It only accepts a value of "1" on write for shrinking
        the cache. Other input values are considered invalid.
        Shrinking slab caches might be expensive and can
        adversely impact other running applications.  So it
        should be used with care.
 
What:       /sys/kernel/slab/cache/min_partial
Date:       February 2009
KernelVersion:  2.6.30
Contact:    Pekka Enberg <[email protected].helsinki.fi>,
        David Rientjes <[email protected].com>
Description:
        The min_partial file specifies how many empty slabs shall
        remain on a node's partial list to avoid the overhead of
        allocating new slabs.  Such slabs may be reclaimed by utilizing
        the shrink file.
           

還有一個/sys/kernel/slab/cache/cpu_partial,在文檔裡找不到,不知道為啥。

順帶提一下,shrink的代碼挺有意思,執行shrink的函數是__kmem_cache_shrink()。另外,可以通過觀察/proc/PID/status中VmRSS的值來判斷是否存在記憶體洩漏

除此之外,筆者還發現,在4.19版本的核心中,在使能了kmem的memory cgroup中,會為每個memory cgroup配置設定一個kmem_cache結構體執行個體,用于該cgroup對應slub的記憶體配置設定。也就是說,每個cgroup會擁有屬于自己的一個slub管理結構體,各個cgroup之間的記憶體配置設定并不是在一個kmem_cache結構體執行個體中處理的,這樣可能帶來的後果是,cgroup1配置設定了1個8byte的記憶體,cgroup2也配置設定了1個8byte的記憶體,在系統總體來看,系統中實際的記憶體支出是2個page(假設這個kmem_cache每次從buddy系統中擷取的記憶體是一個page),與同在一個cgroup裡面申請兩個8byte記憶體,系統中實際的記憶體支出1個page是不同的。這樣帶來的後果是,若系統中存在的memory cgroup數量比較多的話,會帶來較大的記憶體消耗,筆者曾經遇到過,因為使用者态沒有删除memory cgroup,導緻的系統slab占用是正常占用的3-4倍的問題。這個問題在5.4核心上得到了解決,也就是所有cgroup中的slub記憶體配置設定都使用全局的kmem_cache。

繼續閱讀