資源隔離技術之記憶體隔離

帶你聊技術發表於2023-03-08



1.混部的記憶體隔離現狀


在降本增效的大背景下,為提高機器資源利用率,將不同優先順序的線上業務(通常為延遲敏感型高優先順序任務)和離線任務(通常為延時不敏感型低優先順序任務)部署在相同的物理機器上。記憶體作為重要資源,混部任務一旦排程到某個k8s節點後,在記憶體資源使用上可能對線上任務產生競爭,為了避免此種情況對線上任務的干擾,我們可能需要感知線上任務的負載情況並做相應的記憶體隔離管控,儘量做到對線上任務零干擾。


 1.1 mem cgroup記憶體特性說明


開源核心cgroup的memory 子系統也包含了些資源隔離相關特性,例如可限制程式的 memory 使用量等。鑑於目前主流業務場景還是使用的cgroup V1,本文預設只討論V1的情況。

以下圖cgroup v1的memory子系統樹狀目錄結構為例:


資源隔離技術之記憶體隔離


每個cgroup節點含兩個重要的引數介面:

  • memory.usage_in_bytes:只讀,它的數值是當前cgroup裡所有程式實際使用的記憶體總和,主要是 RSS 記憶體和 Page Cache 記憶體的和

  • memory.limit_in_bytes:可配置,當前cgroup裡所有程式可使用記憶體的最大值

由於cgroup v1預設按層級繼承的方式管理各個cgroup,因此每個cgroup計費的記憶體使用量包括其所有子孫cgroup的記憶體使用量,反應到數值上可以表現為:

    HIER_A表示cgroup A的memory.usage_in_bytes, SELF_A表示cgroup A自身使用的記憶體,那麼有:

    HIER_A = HIER_D + HIER_C + SELF_A; HIER_C = HIER_F + HIER_G + SELF_C;

    LIMT_A表示cgroup A的memory.limit_in_bytes,那麼有:

     LIMT_A >= HIER_A =  HIER_D + HIER_C + SELF_A;

     LIMT_C >= HIER_C =  HIER_F + HIER_G + SELF_C;

因此儘管LIMT_C可以超過LIMT_A,但實際HIER_C仍然受到LIMT_A的限制,例如上圖LIMT_C為300M,LIMT_A為200M,當HIER_C到達200M時,HIER_A必然也達到200M,此時打到LIMT_A就會在A點發生mem cgroup級別的直接記憶體回收。

回收之後,若能從C回收一定量的記憶體,則C的記憶體分配可以繼續,反之則A發生mem cgroup級別的OOM,從C子樹中選擇得分最高的程式kill,回收其記憶體滿足C的記憶體使用申請。因此可保證C實際使用的記憶體永遠不會超過其父A節點的limit限制。


資源隔離技術之記憶體隔離


如上圖所示(系統swappiness=0,cgroup.memory=nokmem),當某cgroup節點子樹內任何程式申請分配X記憶體時,若usage + X > limit,則會在記憶體分配上下文中觸發記憶體回收行為,從該子樹中回收記憶體( Page Cache 記憶體)。如果回收到>=X的記憶體,則程式的記憶體分配成功返回;反之失敗,繼續在記憶體分配上下文中觸發OOM,從該子樹中選擇得分最高的(預設為記憶體使用量最大)的程式kill掉並回收其記憶體,迴圈往復直至滿足記憶體分配申請。


 1.2 混部記憶體隔離


在1.1節所描述的基礎上,當線上和離線任務混合部署時,假設C子樹管理線上任務,D子樹管理離線任務,如下圖:


資源隔離技術之記憶體隔離


當離線任務部署到H葉子節點,假設其limit為100M,那麼當其記憶體使用量超過60M後,最壞情況下,C子樹的線上任務在記憶體使用到140M後就會打到A的limit,並導致記憶體分配上下文中出現mem cgroup級別的的直接記憶體回收行為,造成延遲。

可見由於離線任務使用了一定量記憶體,讓線上任務的記憶體分配更早觸碰到了A的limit,更糟糕的是,如果記憶體回收沒有回收到需求的記憶體,則會在A節點觸發mem cgroup級別的OOM,從A子樹下選擇程式kill掉。在不加其它保護的情況下,系統很可能選中C內的線上任務並殺死,這對線上業務來說,會造成很大的影響,甚至無法繼續執行。

因此為儘量避免上述離線任務部署對線上任務的記憶體使用干擾,B站引入龍蜥社群開源核心的memcg OOM 優先順序和memcg後臺非同步回收特性,讓我們來分析驗證一下它們在這個問題場景中發揮的作用。


2.memcg OOM優先順序


如1.2所述,當某memory cgroup(以下簡稱memcg)子樹記憶體緊缺時,核心會在記憶體分配路徑遍歷該memcg子樹進行記憶體回收,甚至在沒有回收到需求記憶體的情況下直接透過OOM選擇該memcg子樹下耗用記憶體最多的任務kill掉。這對於線上memcg的應用來說,會造成很大的影響,甚至導致其無法繼續執行。

為此我們希望OOM能儘可能選取不重要的離線任務,避免kill線上業務,這就是memcg OOM優先順序配置功能要解決的問題,透過在進行OOM操作時,首先判定memcg的優先順序,選擇低優先順序的memcg進行OOM操作。


 2.1 介面設計


此功能新增2個可配置介面(引自社群文件):


介面
說明
memory.use_priority_oom該介面用於設定是否啟用memcg OOM優先順序策略功能,取值為0或者1。該介面不會繼承,預設值為0。
  • 取值為0時,表示禁用memcg OOM優先順序策略功能。

  • 取值為1時,表示開啟memcgOOM優先順序策略功能。

memory.priority該介面提供13個級別的memcg優先順序以支援不同重要程度的業務。取值範圍為0~12,數值越大表示優先順序越高。該介面不會繼承,預設值為0。
  • 實現一定程度的記憶體QoS,此處需要說明的優先順序值非全域性變數,只能在同父cgroup下的兄弟節點進行比較。

  • 對於優先順序相等的兄弟節點來說,會按照組的記憶體使用量來排序選擇記憶體使用最大的進行OOM操作。


 2.2 選擇策略


仍然以1.2節中的問題場景為例,當我們加上優先順序配置之後,如下圖:


資源隔離技術之記憶體隔離


此時當C節點記憶體使用量達到140M,再次打到A的limit,假設最壞情況下記憶體回收失敗導致OOM時,如果A節點開啟OOM 優先順序功能,則從A開始遍歷選擇優先順序最小的子memcg節點,此處為D,然後再遍歷D的子節點選擇優先順序最小的節點,層層遍歷後找到了優先順序為2的H葉子節點;最後按正常的OOM打分機制,從H中選擇記憶體使用量最大的程式kill掉,若殺光H的任務也無法滿足需求,則會到上層選擇最低優先順序的D繼續OOM流程,這樣就確保了高優先順序的C、F、G最後才會被選中,降低了其任務被殺死的機率。

即使單論優先順序大小,H節點的memory.priority(priority為2)大於F節點的memory.priority值(priority為1),但由於層級關係,先從C、D中選擇了優先順序更低的D,只能從D下選擇葉子節點H。

為什麼不選擇優先順序更低的B、E,原因也是層級結構。作為A的兄弟節點,B所計費的記憶體與A基本沒有任何重疊,其只包含B、E中所有程式申請引入的記憶體,因此當A節點發生memcg OOM時,如果去回收B子樹的記憶體,是無法緩解A的記憶體緊缺狀況的,其usage_in_bytes不會有任何改變,因而此時只能去回收A子樹的記憶體。

關鍵選擇邏輯程式碼如下:


memcg oom priority








































while (parent) {    struct cgroup_subsys_state *pos;    struct mem_cgroup *parent_mem;     parent_mem = mem_cgroup_from_css(parent);               //第一輪迴圈時為A     if (parent->nr_procs <= parent_mem->num_oom_skip)        break;                                              //若沒有合適的可殺任務則跳過    victim = parent;    chosen = NULL;    chosen_priority = DEF_PRIORITY + 1;    list_for_each_entry_rcu(pos, &parent->children, sibling) {//第一輪迴圈時會遍歷到B和C        struct mem_cgroup *tmp, *chosen_mem;         tmp = mem_cgroup_from_css(pos);         if (pos->nr_procs <= tmp->num_oom_skip)            continue;        if (tmp->priority > chosen_priority)            continue;        if (tmp->priority < chosen_priority) {                //進行優先順序比較選擇,最終chosen低優先順序的D            chosen_priority = tmp->priority;            chosen = pos;            continue;        }         chosen_mem = mem_cgroup_from_css(chosen);         if (do_memsw_account()) {                           //若優先順序一致時則選擇記憶體使用更多的memcg            if (page_counter_read(&tmp->memsw) >                page_counter_read(&chosen_mem->memsw))                chosen = pos;        } else if (page_counter_read(&tmp->memory) >            page_counter_read(&chosen_mem->memory)) {            chosen = pos;        }    }    parent = chosen;                                        //第一輪迴圈時,從D開始下一輪迴圈遍歷}


 2.3 測試驗證


為測試memcg OOM priority方案的效能表現,透過docker部署8個sysbench例項(每次寫32MB記憶體)作為線上業務,用另8個相同的sysbench例項作為離線業務,分別綁核執行於不同核,來量化混部隔離效果。讓跑線上任務的memcg節點和跑離線任務的memcg節點處於同一層級,設定其共同的父節點memory.use_priority_oom 使能或關閉memcg OOM priority特性,模擬1.2節中的問題場景。

 測試機器資訊:cpu:Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz,2 Sockets, 16 Cores per Socket, 2 Threads per Core;OS:Debian 9.13;配置:swappiness=0;kernel版本:upstream Linux5.10.103+memcg OOM priority特性


2.3.1 測試驗證


離線和線上的共同父節點不觸發memcg oom


混部型別
線上:95th percentile
線上:max latency
離線:95th percentile
普通混部41.82ms74.68ms46.87ms
OOM 優先順序混部43.74ms75.7ms44.02ms


離線和線上的共同父節點觸發memcg oom


混部型別
線上:95th percentile
線上:max latency
離線:95th percentile
普通混部killed(50%機率)killed(50%概率) killed(50%概率)
OOM 優先順序混部41.48ms
1955.2mskilled


由上述測試結果可知:

  • OOM 優先順序對於那些memcg包含線上任務不能被kill的場景是至關重要的,且沒有OOM發生時,OOM 優先順序混部效能基本等同普通混部場景

  • 發生memcg OOM時,普通混部場景下,線上、離線都有一定機率被kill,預設情況下取決於哪個任務使用記憶體多,誰多誰先被kill

  • 觸發OOM時,線上效能會被影響,導致線上max延時相比不觸發OOM時差別較大


2.3.2 max latency分析


由上述2.3.1測試結果可知,離現和線上共同父節點memcg觸發OOM場景下,OOM優先順序保證了線上核心業務相比離線業務不被優先kill,可是觸發OOM時會有較大秒級延時,可能引發線上業務效能抖動。

為視覺化記憶體分配過程中發生的direct reclaim等延時資訊,B站在cgroup v1下新增memory.bili_stat介面來監控memcg級別記憶體分配等延時。由memory.bili_stat介面抓到的線上任務memcg 直接記憶體回收延時資料可知,線上任務的延時主要原因:memcg charge過程中由於memcg記憶體使用超過 memory.limit_in_bytes 介面設定記憶體大小,在任務記憶體分配上下文中進行memcg級別的直接記憶體回收造成延時


資源隔離技術之記憶體隔離


由上圖可看出線上、離線同時執行並導致先祖觸發OOM時間段內,memcg直接記憶體回收造成的總延時有286秒(total_ms列),最大延時為128~256ms;

每次memcg計費一個page觸發記憶體回收時如果無記憶體可回收那麼最多會嘗試進入16(MAX_RECLAIM_RETRIES)次記憶體回收路徑,之後進入OOM路徑,採取memcg OOM策略,如1.1節的第二張圖所述。當臨近發生memcg OOM時,按每次記憶體回收路徑平均延時為30ms,每次page fault平均耗時480ms用於記憶體回收,而sysbench每次操作記憶體時會發生多次page fault,因此上述測試的max latency 達到1955.2ms是可以解釋的。

透過測試結果我們知道OOM優先順序可以解決1.2節中描述的問題之一,但仍然無法解決OOM本身帶來的對於線上任務的效能影響,這部分還有待解決。


3.memcg後臺非同步回收


如1.2節所述,除了在memcg級別的OOM發生時透過優先順序保障線上業務不被殺死以外,還可以透過提前回收任務快取,降低usage記憶體打到limit機率,進而減少記憶體分配上下文中的memcg級別直接記憶體回收,memcg後臺非同步回收功能就可以幫助我們做到這一點。

該功能在cgroup v1的場景下,透過當相應memcg中的統計記憶體達非同步回收水位線時,觸發memcg級的非同步回收,來降低直接記憶體回收發生機率,為還沒有引入memcg QoS功能的cgroup v1提供了一種非同步回收方案。


 3.1 介面設計


該功能的實現不同於全域性kswapd核心執行緒的實現,並沒有建立對應的memcg kswapd核心執行緒,而是採用了workqueue機制來實現,並在cgroup v1和cgroup v2兩個介面中,均新增了4個memcg控制介面(引自社群文件)。


介面

說明

memory.wmark_ratio該介面用於設定是否啟用memcg後臺非同步回收功能,以及設定非同步回收功能開始工作的memcg記憶體水位線。單位是相對於memcg limit的百分之幾。會繼承父組的值,取值範圍:0~100
  • 預設值為0,該值也表示禁用memcg後臺非同步回收功能。

  • 取值為非0時,表示開啟memcg後臺非同步回收功能並設定對應的水位線。

memory.wmark_high只讀介面,說明如下:
  • 當memcg記憶體使用超過該介面的值時,後臺非同步回收功能啟動。

  • 該介面的值由(memory.limit_in_bytes * memory.wmark_ratio / 100)計算獲得。

  • memcg後臺非同步回收功能被禁用時,memory.wmark_high預設為一個極大值,從而達到永不觸發後臺非同步回收功能的目的。

  • memcg根組目錄下不存在該介面檔案。

memory.wmark_low只讀介面,說明如下:
  • 當memcg記憶體使用低於該介面的值時,後臺非同步回收結束。

  • 該介面的值由memory.wmark_high - memory.limit_in_bytes * memory.wmark_scale_factor / 10000計算得出。

  • memcg根組目錄下不存在該介面檔案。

memory.wmark_scale_factor該介面用於控制memory.wmark_high和memory.wmark_low之間的間隔。單位是相對於memcg limit的萬分之幾。取值範圍:1~1000
  • 該介面在建立時,會繼承父組的值(該值為50),該值也是預設值,即memcg limit的千分之五。

  • memcg根組目錄不存在該介面檔案。


 3.2 回收策略


內部程式碼實現主要是透過對memory.wmark_high介面進行處理:charge過程中當memcg的使用記憶體達到給定水位時,利用工作佇列進行memcg非同步記憶體回收。而且此功能增加了memory.wmark_low引數用於跟蹤控制非同步回收過程回收到指定記憶體量就停止回收工作,並新增1專用工作佇列用於memcg後臺非同步回收。

仍然以1.2節中的問題場景為例,當我們在A節點加上非同步回收配置之後,如下圖:


資源隔離技術之記憶體隔離


設定memcgA:memory.limit_in_bytes = 200MB; memory.wmark_ratio = 80; memory.wmark_scale_factor = 50(預設);在A節點使能memcg非同步回收功能,此配置下觸發非同步回收的水位線為wmark_high:160MB

D使用60MB記憶體,當C使用記憶體到達100M,A的usage必然達到160M,而A觸發非同步回收的水線就是160M,則此刻A子樹的記憶體分配上下文中會利用工作佇列排程一個work執行記憶體回收工作,此記憶體回收過程不包含於程式的記憶體分配上下文,屬於非同步記憶體回收,不會增加程式的page fault延時。

直到從A子樹回收到記憶體,並使usage低於wmark_low介面值,則回收停止。也就是說上圖A節點usage到達160M時觸發memcg後臺非同步回收,直到從該memcg子樹回收到1M記憶體,滿足usage<wmark_low後,則此次memcg非同步記憶體回收工作結束。< code="">

在memcgA usage未打到limit之前,提前非同步回收掉不活躍的Page Cache,避免之後usage達到limit時,在程式的記憶體請求過程中觸發memcg級別的直接記憶體回收可能影響業務效能。


 3.3 測試驗證


為測試memcg 後臺非同步回收方案的效能表現,透過docker部署8任務A作為線上業務和另8個任務B作為離線業務,分別綁核執行於不同核,來量化混部隔離效果。模擬1.2節中的問題場景,讓跑線上任務的memcg節點和跑離線任務的memcg節點作為兄弟節點,設定其共同的父節點的limit接近線上任務申請的總記憶體量來創造記憶體壓力場景,並設定其共同的父節點memory.wmark_ratio等於80或0,來使能或關閉memcg後臺非同步回收特性。

  • 模擬線上任務A: 自研測試程式,透過引數持續申請分配一定size匿名記憶體,計算記憶體申請使用速率及延時;

  • 模擬離線任務B: 申請分配一定size的Page Cache

 測試機器資訊:cpu:Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz,2 Sockets, 16 Cores per Socket, 2 Threads per Core;OS:Debian 9.13;配置:swappiness=0;kernel版本:upstream Linux5.10.103+memcg後臺非同步回收特性


3.3.1 測試結果


線上持續10次每次申請10MB記憶體,記錄10次記憶體分配平均和最大使用時間


混部型別
線上:avg latency
線上:max latency
普通混部7.74ms14.44ms
非同步回收混部5.03ms
5.15ms


申請分配使用不同size大小記憶體,不同混部型別max latency


混部型別
10MB
50MB
100MB
200MB
500MB
普通混部
14.44ms
68.32ms102.19ms198.33ms361.81ms
開啟非同步回收
5.15ms25.43ms51.84ms107.19ms297.88ms


折線圖:


資源隔離技術之記憶體隔離


3.3.2 結果分析


由上面表格資料可知未開啟memcg後臺非同步回收訪問記憶體會有更高max延時,主要延時原因為:memcg charge計費統計過程中由於memcg使用記憶體超過 memory.limit_in_bytes 介面設定記憶體大小,進行memcg級別直接記憶體回收造成延時

同樣也用bili_stat介面檢視到線上任務所在memcg的直接記憶體回收延時:

  • 關閉memcg後臺非同步回收


資源隔離技術之記憶體隔離


  • 開啟memcg後臺非同步回收 


資源隔離技術之記憶體隔離


相比普通混部,開啟memcg後臺非同步回收後,可避免部分記憶體分配上下文中的直接記憶體回收延時。但如1.1節的第二張圖所述,如當該memcg子樹內無Page Cache可回收,隨著RSS記憶體使用量增加,usage達到limit時仍然會進入直接記憶體回收路徑多次嘗試記憶體回收,之後觸發OOM kill離線任務來緩解記憶體緊缺情況。

不過由於非同步回收發生,Page Cache被回收,一些效能敏感的線上業務可能會介意因此而導致的檔案讀寫效率降低。

如下表格,線上任務在其父節點發生非同步回收前後分別使用命令dd if=test_file of=/dev/null bs=80M count=1讀同一80MB大小的檔案:


cache數量(bytes)8380416037576704
dd 速率2.2 GB/s821 MB/s


當線上和離線共同父節點發生memcg級別的非同步回收時,會同時回收線上和離線任務的Page Cache,如上面表格資料所示:發生非同步回收時,線上任務的Page Cache也會被回收,導致讀寫檔案速率降低。 

透過測試結果我們知道開啟memcg後臺非同步回收能提高業務記憶體分配訪問速率、降低max延時、減少延時敏感型線上業務效能抖動,可以解決1.2節中描述的過早觸碰到limit點,導致線上任務發生直接記憶體回收產生延時問題。但是一些效能敏感任務可能不希望自身Page Cache被回收,而是在非同步回收觸發時能優先回收離線任務的Page Cache,這部分還有待解決。


4.結論與展望


本文由混部技術的記憶體隔離問題引入對龍蜥社群開源核心memcg OOM 優先順序和memcg後臺非同步回收特性的分析,並基於相關特性進行了簡單的模擬混部測試,對結果進行了分析。從測試結果來看我們不難得出以下結論:

針對memcg OOM優先順序

  • 可以降低線上任務被殺死的機率,並優先殺死離線任務

  • 相比於oom_score_adj,能以memcg為單位進行管控

  • 先祖memcg發生OOM時,對線上任務造成的效能影響仍然無法消除

針對memcg後臺非同步回收

  • 可以降低線上任務觸發直接記憶體回收的機率

  • 相比現有的memcg回收機制,可以利用空閒CPU提前回收掉不活躍的Page Cache

  • 線上任務Page Cache被回收,可能會對其效能造成一定影響

針對OOM本身對線上任務的效能影響,我們正在嘗試透過更精細化的記憶體水位控制功能,提前殺死低優先順序的任務,緩解記憶體緊缺情況,避免線上任務由於先祖memcg發生OOM而遭受效能損失。

而針對非同步回收對線上任務的效能影響,我們也在嘗試透過優先順序機制(類似memcg QoS),來優先回收離線Page Cache,降低線上任務被回收Page Cache機率。

未來透過不斷地實踐記憶體隔離能力在混部場景中的應用,我們會持續發現和解決其中存在的問題,打磨出最適合B站業務混部的記憶體隔離方案,助力降本增效。


5.參考


[1] 龍蜥社群開源核心程式碼倉庫:

[2] Memcg OOM優先順序策略功能介紹:https://help.aliyun.com/document_detail/435534.htmlMemcg

[3] 後臺非同步回收介紹:https://help.aliyun.com/document_detail/169535.html

[4] B站雲原生混部技術實踐


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2938573/,如需轉載,請註明出處,否則將追究法律責任。

相關文章