资源隔离技术之内存隔离

admin 2023年3月8日09:25:26评论28 views字数 9905阅读33分1秒阅读模式

本期作者

    SYS-OS    

B站系统部操作系统(SYS-OS)团队

负责公司OS层面系统软件支持

覆盖内核优化、系统工具、操作系统镜像、软硬件结合等方向的工作


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.82ms 74.68ms 46.87ms
OOM 优先级混部 43.74ms 75.7ms 44.02ms


离线和在线的共同父节点触发memcg oom


混部类型
在线:95th percentile
在线:max latency
离线:95th percentile
普通混部 killed(50%概率) killed(50%概率)  killed(50%概率)
OOM 优先级混部 41.48ms
1955.2ms killed


由上述测试结果可知:

  • 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.74ms 14.44ms
异步回收混部 5.03ms
5.15ms


申请分配使用不同size大小内存,不同混部类型max latency


混部类型
10MB
50MB
100MB
200MB
500MB
普通混部
14.44ms
68.32ms 102.19ms 198.33ms 361.81ms
开启异步回收
5.15ms 25.43ms 51.84ms 107.19ms 297.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) 83804160 37576704
dd 速率 2.2 GB/s 821 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] 龙蜥社区开源内核代码仓库:https://gitee.com/anolis/cloud-kernel

[2] Memcg OOM优先级策略功能介绍:https://help.aliyun.com/document_detail/435534.htmlMemcg

[3] 后台异步回收介绍:https://help.aliyun.com/document_detail/169535.html

[4] B站云原生混部技术实践


以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!


往期精彩指路

原文始发于微信公众号(哔哩哔哩技术):资源隔离技术之内存隔离

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月8日09:25:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   资源隔离技术之内存隔离https://cn-sec.com/archives/1591749.html

发表评论

匿名网友 填写信息