共计 8524 个字符,预计需要花费 22 分钟才能阅读完成。
](/img/bVc3ZXk)
01 背景
随着业务的倒退,百度 APP 有很多大内存业务场景如直播、短视频、小程序、百度识图等,通过线上页面统计数据得悉超过 150M 页面有 40 个,耗内存最多的页面有 400M。单个页面不会有内存或者稳定性问题,然而当用户浏览了很多页面之后,累加起来内存曾经很高了,再加上咱们为了谋求秒开,常常采纳的思路是以空间换取工夫,从而导致 APP 处于一个内存高水位状态,在这种状况下如果关上一个大内存页面,中低端机极大概率会呈现 OOM 类型的解体。
内存管控计划应运而生,该计划重点解决的问题是在内存水位很高的状况下,保障 APP 稳定性又兼顾用户体验,缩短 APP 应用时长同时防止 OOM。
该技术计划在百度 APP 于 22 年 Q1 顺利上线,随着根底服务层和越来越多的业务线接入,尤其是 OOM 频发的页面接入后,在升高 OOM 率方面施展了重大作用,成果非常明显。
02 技术计划综述
内存管控整体计划架构图如下所示:
- 实时监控 APP 内存:在 APP 运行阶段一直监控内存变动。重点关注两个问题,第一,选取适合的能反映 APP 内存的指标,第二、实时性必须满足要求,同时不能引入额定的性能问题。
- 页面内存预测:依据历史教训和线上数据,咱们能够预测将要关上新页面后 APP 占用内存大小,联合以后内存咱们能够实时计算出利用新开以后页面后本身占用的内存大小,举个例子,以后页面占用内存 400M,通过线上历史教训数据咱们晓得新页面须要占用内存是 300M,那么新开一个页面后,APP 内存 700M。
- 内存水位判断:依据对以后 APP 内存状态的监控,可能判断出用户内存所处的水位状态,如平安水位和危险水位,平安水位是指以后 APP 内存足够,可齐全依照业务需要分配内存,危险水位是指目前 APP 很容易呈现 OOM,必须马上开释内存缓存。
- 频率管制:因为每隔 3S 做一次内存检测,当处于危险水位时,会告诉 APP 各个模块做内存开释,但内存开释也是须要工夫的,并且不肯定会立马升高到平安水位,如果接下来还是每隔 3S 告诉各模块做内存开释,其实是一种资源节约,频繁的内存开释操作会给 APP 性能带来损耗,所以通过频率管制模块既能最大限度地开释内存,又实现 APP 性能最小损失。
- 危险水位报警:当 APP 的内存处于危险水位的状态时,会向根底服务层和业务两个层面发送报警告诉,对于根底服务层来说,百度 APP 次要做了图片内存和 NSURLCache 内存主动回收,全局失效;对于业务层来说,次要针对内存小户且 OOM 率较高的页面做了内存开释操作,如小程序页面,收到内存报警时,会将缓存的处于非沉闷状态的页面做清理操作,对于其余业务同样情理,清理业务本身的数据缓存和其余内存缓存。
- 被动降级:是指业务层在调配较大内存时,先判断以后 APP 所属的内存水位等级,若处于危险水位,业务做降级调配较小内存,若处于平安水位,做全量内存调配。目前百度 APP 的识图和数字人业务已接入此计划,对于百度识图场景,做多模态图片辨认加载算法模型文件较大,处于危险水位时加载兜底模型,以业务能用为规范,其余场景相似。
03 与内存报警的区别
目前 iOS 零碎中存在相似的计划,业余名称为内存报警机制,当设施可用内存降落到到危险状态时,Mach 零碎的 pageout 守护程序会查问过程列表及其驻留页面数,向驻留页面数最高的过程发送 NOTE\_VM\_PRESSURE,被选中的过程会响应这个压力告诉,实质上就是 APP 收到零碎的 didReceiveMemoryWarning 内存正告,APP 开释局部内存达到升高手机内存负载的指标。有人会问 iOS 零碎提供了内存报警告诉,为什么咱们还会做貌似相似的事件,这是因为咱们对系统的内存报警机制做了如下 两点补充:
- 内存报警机制是内存极其危险的时候才收回的,尤其是对于低端机而言十分致命,因为 APP 来不及开释内存到平安水位就曾经 OOM 了。在实际开发过程中,对低端机 (iPhone8 以下) 测试后果发现,当收到内存报警时,APP 理论可应用内存 (可用内存减去已用内存) 没有超过 100M,然而目前手百 APP 大于 150M 页面就有 40 个,当收到内存正告前后,轻易关上上述 40 个大页面中的任何一个页面,APP 基本没有来得及解决警报利用就会解体。相同,百度 APP 内存管控计划在制订危险水位时思考到这种状况,适当预留了较大空间,让 APP 更从容地开释内存。
- 内存报警机制没有提供获取 APP 实时内存状态的性能,在实践中常常会遇到大块内存调配的场景,较为常见场景如在中低端机端智能场景中,加载大模型到内存时,因为不晓得内存以后处于危险状态还是平安状态,调配较大内存会呈现内存峰值刹时上涨到高点,中低端机手机设施间接 OOM,在整个过程中也基本没有收到过内存报警。内存管控计划补救了这一有余,通过实时获取内存状态,不同机型不同设施设置不同危险水位级别,在调配较大内存时,先判断 APP 内存状态,若处于危险水位时,业务线开发能够走降级逻辑,升高对内存耗费,缩小 OOM 危险。
04 实时监控 APP 内存
百度 APP 实时监控内存采纳如下计划:在子线程开启定时器,每隔 3S 去采样一次内存 phys\_footprint 字段数据,以此作为掂量的内存的惟一指标,其余字段值一律不要获取,因为多减少一个变量会多减少 CPU 计算量。实际数据表明,第一、单次获取 phys\_footprint 耗时小于 1us,每隔 3S 获取 phys\_footprint 没有引起 CPU 占比的涨幅,也就是说不会带来性能问题;第二、3S 的采样周期实时性齐全满足咱们工程的要求,失常状况下,开启一个页面到页面可交互须要 1.5S+,采样周期如果太长,会存在页面内存曾经飙升然而还没来得及做管控,采样周期太短会节约过多的 CPU 资源。
为什么咱们选用 phys\_footprint 作为内存掂量指标,而不必其余字段,须要重点解释一下。iOS 端所有的内存相干指标都集中在 task\_vm\_info 构造体中,下载 XNU 最新开源代码(https://opensource.apple.com/…),代码门路:osfmk/mach/task\_info.h,具体字段值如下所示:
struct task_vm_info {mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
integer_t page_size;
mach_vm_size_t resident_size; /* resident memory size (bytes) */
/* 省略 */
mach_vm_size_t phys_footprint;
/* 省略 */
}
iOS 开发演变的这几年历程中,受 Android 端内存指标影响,咱们先后应用过各种内存指标,常见的如 virtual\_size(虚拟内存)、resident\_size(驻留内存)和 phys\_footprint,那到底应用哪个指标是正当的?咱们晓得 iOS 应用的是低内存清理机制叫 Jetsam,这个机制有点相似于 Linux 的“Out-of-Memory”杀手,当内存压力过大时,Jetsam 会把一些优先级不高或者占用内存过大的过程杀掉。就是说内存处于危险状态时 Jetsam 决定 kill 哪个过程,因而 Jetsam 掂量内存水位指标相对是泛滥内存指标中最为正当的一项,接下来咱们看 Jetsam 机制源码。
咱们再次回到 XNU 源码中,查看代码 bsd/kern/kern\_memorystatus.c,重点查看函数 memorystatus\_kill\_hiwat\_proc,这是 jetsam 外围代码,用于 kill 高内存调配过程的要害函数,具体实现如下所示:
static boolean_t
memorystatus_kill_hiwat_proc(uint32_t *errors, boolean_t *purged, uint64_t *memory_reclaimed)
{next_p = memorystatus_get_first_proc_locked(&i, TRUE);
while (next_p) {
/* 省略 */
footprint_in_bytes = get_task_phys_footprint(p->task);
skip = (footprint_in_bytes <= memlimit_in_bytes);
if (skip) {continue;} else {memorystatus_kill_proc(p, kMemorystatusKilledHiwat, jetsam_reason, &killed, &footprint_in_bytes);
/* 省略 */
}
}
首先通过 memorystatus\_get\_first\_proc\_locked 去优先级队列外面取出优先级最低的过程,如果内存超过阈值,将通过 memorystatus\_kill\_proc 杀掉这个过程,否则跳过取下一个过程。咱们看到 Jetsam 是通过 get\_task\_phys\_footprint 办法获取内存水位来决定是不是须要 kill 该过程,因而应用 phys\_footprint 作为 APP 内存指标是最合适的。
对于 phys\_footprint 的定义,咱们回到 XNU 源码中,查看代码 osfmk/kern/task.c,有 phys\_footprint 的正文定义。
* Physical footprint: This is the sum of:
* + (internal - alternate_accounting)
* + (internal_compressed - alternate_accounting_compressed)
* + iokit_mapped
* + purgeable_nonvolatile
* + purgeable_nonvolatile_compressed
* + page_table
phys\_footprint = (internal – alternate\_accounting) + (internal\_compressed – alternate\_accounting\_compressed) + iokit\_mapped + purgeable\_nonvolatile + purgeable\_nonvolatile\_compressed + page\_table。
purgeable 内存是 iOS 零碎为开发者提供的一层 cache 机制,分为 volatile、empty 和 non\_volatile 三种类型,volatile 示意该内存资源是临时不被应用的,零碎将在内存吃紧的时候回收掉它,应用这种类型资源前要查问是否曾经有效了(变成 empty 状态);empty 示意该内存资源明确不必了须要立刻开释;non\_volatile 示意该内存资源始终有用,不能被回收。volatile 和 empty 状态的资源不计入过程本人的 mem footprint,它算零碎的 cache 内存,nonvolatile 会算本人过程的内存,被虚拟内存零碎回收时不会被换出到磁盘,所以 phys\_footprint 在计算内存时,只计算了 nonvolatile 类型,对于 volatile、empty 没做计算。
05 页面内存预测
为了可能更精准的对页面的内存进行剖析和预测,咱们在实时内存监控的根底上,开发了页面内存预测计划。具体来说,在后面通过定时器咱们晓得了每隔 3S 手机 APP 内存状态,本计划通过教训数据间接预测将来一段时间内存的涨幅,让业务线能够更加从容的开释内存。咱们晓得当新关上一个页面时存在内存飙升的情景,这个时候 3S 的采样周期未到,内存曾经上涨很多,内存管控计划还未失效 APP 极有可能曾经 OOM 了。咱们的计划是通过页面内存计算,在关上新页面前一刻,就晓得接下来页面内存可能会涨到多少,如果进入危险水位,实时开释内存以升高 OOM 率,通过这种精细化解决进一步提前升高内存峰值。
页面内存计算计划如下所示,首先,以后页面是 P1 页面,当有页面跳转产生,将要通过 push 操作进入到 P2 页面时,记录以后百度 APP 内存 phys\_footprint 值为 M1,当从 P2 页面同样产生跳转到其余页面时,记录百度 APP 内存 phys\_footprint 值为 M2,那么 M2-M1 为 P2 页面内存。
留神,咱们只通过 push 形式统计了页面内存,没有通过 pop 形式统计,有两个起因,第一、通过线上数据发现,pop 形式时因页面曾经关上,并且会创立单例导致内存统计存在很多 badcase,push 形式时页面从未创立也不会有单例,数据绝对精确;第二、通过 push 形式曾经能够笼罩所有页面了,pop 形式不须要统计。
06 制订内存水位
对于内存水位的制订间接决定了本计划理论收益的大小,水位阈值制订太小会导致频繁的内存管控影响业务成果,水位阈值制订的太大,与理论的 Jetsam 水位线偏离过大,导致内存管控无奈失效,可能会呈现 APP 曾经 OOM 了,管控计划还没失效,水位线的制订十分要害。
对于危险水位线的制订,必须联合 Jetsam 原理,目前苹果官网没有公开 Jetsam 水位的文档,业界有如下办法解决方案。
丨 6.1 通过 Jetsam 日志获取
具体来说从手机 ” 设置 -> 隐衷 -> 剖析与改良 -> 剖析数据 ” 这条操作门路中,能够拿到 JetsamEvent 结尾的日志。这些日志中就能够获取一些对于 App 的内存信息,查找解体起因时须要关注 per-process-limit 局部的 rpages, 其中 rpages 代表过程占用的内存页数量。pageSize 代表以后设施物理内存页的大小,在 JetsamEvent 结尾的系统日志里能够找到 pageSize 的值,那么 pageSize * rpage 的值代表目前该过程 OOM 时应用的内存大小,可作为过程可用内存的下限。
实际操作过程中,发现此办法可操作性不强,因为同一台手机不同的 JetsamEvent 日志 rpages 值变动太大,用 iphone12 的测试结果显示,从 400 到 800 都有,pageSize 是固定值 16384Byte,依照最高值计算以后 App 的内存限度值:pageSize * rpages / 1024 /1024 =16384 * 800 / 1024 / 1024 = 12.5M,按这个后果 iphone12 最大的内存阈值是 12.5M,置信度显著有问题。
丨 6.2 通过 XNU 源码获取内存水位阈值
首先必须越狱手机获取 root 权限,通过 XNU 源码中的数据结构、宏定义和函数获取 OOM 阈值,参考 XNU 最新开源代码(https://opensource.apple.com/…),代码门路:bsd/sys/kern\_memorystatus.h,要害数据结构 memorystatus\_priority\_entry,定义如下,其中 pid 代表过程标识,priority 代表 JetSam 中的优先级,limit 就是咱们要找的水位线上线。同时,在文件 kern\_memorystatus.h 有如下跟过程优先级相干的宏命令,其中通过 MEMORYSTATUS\_CMD\_GET\_PRIORITY\_LIST 宏定义能够获取过程的优先级列表以及每个过程的内存水位线。
typedef struct memorystatus_priority_entry {
pid_t pid;
int32_t priority;
uint64_t user_data;
int32_t limit;
uint32_t state;
} memorystatus_priority_entry_t;
#define MEMORYSTATUS_CMD_GET_PRIORITY_LIST 1
#define MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES 2
#define MEMORYSTATUS_CMD_GET_JETSAM_SNAPSHOT 3
#define MEMORYSTATUS_CMD_GET_PRESSURE_STATUS 4
/* 省略 */
最初通过调用零碎函数 memorystatus\_control 的实现可获取 memorystatus\_priority\_entry 构造体值,其中 limit 字段代表水位线,代码门路:bsd/kern/kern\_memorystatus.c
int memorystatus_control(struct proc *p __unused, struct memorystatus_control_args *args, int *ret)
/* 省略 */
switch (args->command) {
case MEMORYSTATUS_CMD_GET_PRIORITY_LIST:
error = memorystatus_cmd_get_priority_list(args->buffer, args->buffersize, ret);
break;
/* 省略 */
}
实践证明这种办法是可行的,惟一毛病是须要获取 root 权限,咱们要获取不同机型的内存阈值,须要将这些设施全副越狱。
丨 6.3 百度 APP 采纳的技术计划
百度 APP 采纳的计划是综合百度 APP 本身的线上业务数据,采纳被动触发 OOM 获取内存阈值计划,联合多方数据最初确定内存危险水位阈值。
丨 6.3.1 内存数据摸底
通过线上内存采样打点,获取了百度 APP 不同机型在应用过程中的内存值,而后通过服务端数据聚合,咱们明确晓得了百度 APP 在没有产生 OOM 状况下不同机型的内存最大值,这份线上数据很重要,尽管不是内存阈值的,然而内存阈值必定是高于该值的。
丨 6.3.2 页面内存数据统计
技术计划在第五节做过具体介绍,这儿不再赘述,通过服务端对页面内存数据挖掘后,咱们明确晓得了不同机型新开一个页面时最大的内存涨幅。
丨 6.3.3 被动触发 OOM 获取内存值
开启定时器工作每隔 1S 调配 20M 内存,示例代码如下所示:
int size = 20 * 1024 * 1024;
char *info = malloc(size);
memset(info, 1, size);
同时监控内存变动,在控制台输入,随着可用内存越来越少,触发 Jetsam 机制,直到产生 OOM,从而失去 OOM 前内存阈值。
(int64_t)memoryUsage {
int64_t memoryUsageInByte = 0;
struct task_vm_info info;
mach_msg_type_number_t size = TASK_VM_INFO_COUNT;
kern_return_t kerr = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &info, &size);
if (kerr == KERN_SUCCESS) {memoryUsageInByte = info.phys_footprint;}
return memoryUsageInByte;
}
丨 6.3.4 确定内存管控危险水位阈值
通过后面三个步骤,咱们获取了不同机型的三个阈值,别离是内存数据摸底阈值、页面内存阈值、被动触发 OOM 获取的阈值,为了让业务更从容地开释内存, 内存管控阈值为被动触发 OOM 获取的阈值减去页面内存阈值,如果该值小于内存数据摸底阈值,那么内存数据摸底阈值就是该机型内存管控阈值。
百度 APP 采纳的这个技术计划不须要越狱手机,通过被动触发 OOM 获取的阈值体现了 Jetsam 机制,更具备可操作性;同时联合本身线上数据,针对手百场景定制化开掘。
07 总结
最初,总结百度 APP 内存管控计划具备如下特点:
- 针对不同机型制订了相应的内存水位能够更加从容地开释内存。本技术计划联合 Jetsam 机制和百度 APP 线上内存数据,制订了 iPhone 各机型容许应用的内存水位线,给业务和框架更大的空间开释和清理内存。
- 实时内存监控和精细化页面内存预测,在实时内存监控的根底上,开发了页面级的内存度量计划,能够估算出用户在新开一个页面内存涨幅多少,在将来一段时间内存会不会达到危险水位。
- 内存管控计划提供被动和被动告诉两种形式获取内存水位状态,实现了各业务层依据手机内存状况实时降级,时效性更强,跟之前服务端降全量降级计划相比,更加灵便,性能更好。
该计划上线后,随着 Q2 根底服务层和业务线接入,实现 OOM 升高一半的收益,并且业务层接入老本很低,后续会推动更多内存小户和 OOM 频发的页面接入。感激各位浏览至此,如有问题请不吝指正。
———— END————
参考资料:
[1] OOM 探索:XNU 内存状态治理:https://www.jianshu.com/p/445…
[2]XNU 源码:https://opensource.apple.com/…
[3]《深刻解析 Mac OS X & iOS 操作系统》
[4]iOS Out-Of-Memory 原理论述及计划调研:https://juejin.cn/post/684490…
举荐浏览:
Ernie-SimCSE 比照学习在内容反作弊上利用
品质评估模型助力危险决策程度晋升
合约广告平台架构演进实际
AI 技术在基于危险测试模式转型中的利用
Go 语言躲坑经验总结