西瓜视频稳定性治理体系建设一:Tailor 原理及实践
zhezhongyun 2025-04-08 20:50 46 浏览
摘要
Tailor [1]是西瓜视频 Android 团队开发的一款内存快照裁剪压缩工具,广泛用于字节跳动旗下各大 App 的 OOM 治理及异常排查,收益显著,在西瓜视频上更是取得 OOM 降低95%以上的好成绩。Tailor 工具现已开源,本文将通过原理、方案和实践来剖析 Tailor 的相关细节。
背景
稳定性治理一直是个老生常谈的话题,过去我们调查稳定性问题只能依靠堆栈和源码,但很多时候堆栈是远远不够的,对于严重依赖的数据只能临时增加埋点后再次上线搜集,这期间还会遇到能不能搜集到和怎么搜集的问题,使得我们治理稳定性问题时常常过于局限和被动。探寻通用、高效、便捷的异常数据搜集方案一直是我们在治理实践中努力的方向。
西瓜视频 Android 团队基于Java 堆内存快照,搭建了一套相对完整的通用异常数据搜集系统,能够在异常发生时,尝试 dump 出一个相对完整的内存快照文件,必要的时候借助云控系统实现快照回捞,最终通过内存快照辅助调查那些棘手的稳定性问题,以提升稳定性问题的治理效率。如何高效、安全、便捷的获取内存快照,是整个通用异常数据搜集系统里关键的一环。
内存快照的作用
OOM 治理
我们知道内存快照是治理 OOM 问题及其他类型的内存问题的重要数据源,其重要性可以简单理解为:内存快照是解决常规堆内存 OOM 问题的充分条件。同时,内存快照中保存的对象信息和依赖关系也是静态分析内存泄漏的关键,是所有内存泄漏检测工具的基石。
Crash 治理
内存快照中保存的数据,很多时候也是调查其他类型异常的重要参考,比如 Activity、Fragment、View 状态等、Framework 层及第三方对象的数据等,必要的时候都可以用来分析异常问题。作为通用数据大大减少了定向埋点的烦恼,同时也覆盖了很多无法渗透到的场景。
为什么要做裁剪
为了能在需要的时候为各类异常提供数据支持,必须要保证数据的稳定,这就需要解决快照在 dump、存储、传输等环节可能存在的问题,不仅包括存储空间和流量消耗问题,还包括隐私和安全性问题。
存储
以 LargeHeap 应用为例,其 OOM 时的内存快照大小通常在512M左右。不经过裁剪的话只能存储在App的外部存储空间或者 SDcard 上,这就会遇到空间不足或者 SDcard 的权限问题( Android 11对 App 的外部存储空间也做了权限限制)。没有足够稳定的存储空间,快照dump成功率将会大幅降低。
传输
传输过程对于数据的大小是非常敏感的,首当其冲的就是流量消耗问题,其次更小的快照传输耗时更少,回传的成功率也会大幅提升。
隐私
内存快照是虚拟机堆内存数据的完整 copy,这其中可能包含有账号、Token、联系人、密钥以及其他可能存在隐私的图片/字符串等,隐私数据是必须要裁剪掉的。
内存快照裁剪方案
目前已知的裁剪方案有种:一种是已开源的 Matrix 方案,另一种是本人在 2018 提出的 hprof 流裁剪方案。Matrix 方案分为两步:先通过 Debug.dumpHprofData 直接 dump 出一个完整的 hprof 文件;然后通过分析 hprof 文件分别裁剪掉数据相同的 Bitmap 对象和 String 对象。其裁剪方案存在以下问题:
- 原生接口直接 dump 出的 hprof 文件过大,存储问题不好解决;
- 裁剪过程中涉及到大文件 I/O 和 hprof 分析,对 App 性能的影响不好控制;
- 裁剪不彻底,快照中仍然存在大量无用数据和可能存在的隐私问题。
hprof 流裁剪则是基于 hprof 文件格式,在 hprof 文件写入过程中进行裁剪压缩,存储空间问题大幅改善,也没有大文件 I/O 和 hprof 分析过程带来的性能问题。该方案源于实际的 OOM 治理需求,并参考了hprof 文件的格式定义,相关考虑如下:
治理需要
- 对于 OOM 问题,只有对象大小和引用关系是必须的,其余信息都是次要的;
- OOM 时占比最大的对象通常是 Bitmap/String,这些对象的数据主要消耗在 byte[] 、 char[];
- Java 堆中的明文隐私信息通常以 Bitmap/String 的形式存在。
hprof格式
hprof [2]文件有明确定义,其数据组织形式比较简单,整体可分为 Header和 Record 数组两部分,相关数据组织定义如下:
- Header: "JAVA PROFILE 1.0.2" + indetifiers + timestamp (13byte + 4byte + 8byte)
- Record:tag + time + length + body(1byte + 4byte + 4byte + byte[$length])
Android 上 dump 出的 hprof 文件虽然也遵循 hprof 格式,但也有所不同,典型的是其一级TAG只有:STRING、LOAD_CLASS、HPROF_TAG_STACK_TRACE、HEAP_DUMP_SEGMENT、HEAP_DUMP_END。HEAP_DUMP_SEGMENT 又分了很多二级 TAG ,这些二级 TAG 中既有标准 hprof 定义的,也有 Android 自定义的 TAG。跟裁剪关系比较紧密的二级 TAG 是 PRIMITIVE_ARRAY_DUMP,存放的是诸如 byte[] 、char[] 、int[]等类型的数据,其格式如图所示:
通过 hprof 格式定义可以发现,直接裁剪掉所有的 byte[]和 char[]就可以实现对 Bitmap/String 对象的裁剪。同时其数据格式定义中还存在大量的无用数据,比如 timestamp、class-serial-number、stack-serial-number、reserved 数据等等,4byte 的 length/number 等也可以压缩成 3byte 或者 2byte 等等。
Tailor 裁剪压缩实现
如果只为了治理 OOM,可以进行最大化裁剪(如byte[]、char[]、boolean[]、short[]、float[]、int[]、double[]、long[]、hprof格式裁剪),甚至可以只保留 app-heap。但作为通用异常数据,西瓜视频也会在必要的时候,通过回捞快照来分析非 OOM 类的异常,甚至是 native 异常。随着稳定性治理的深入,快照更多是用来分析非 OOM 异常。对于非 OOM 异常,快照的完整性尤为重要,同时非 OOM 的 crash 堆内存通常较小,最大化裁剪没有必要,综合考虑之后 Tailor 只保留了 byte[]、char[] 和 hprof 格式裁剪。
快照 dump 的过程大致可以分为 5 步,Tailor 只关注 open 和 write 环节。通过 xHook [3](针对 Android 平台 ELF 的 PLT hook库)在 native 层 hook dump 过程必然会调用到的 open/write 函数,以此实现对hprof 文件写入流的代理,进而进行 hprof 流裁剪。为了进一步降低写入后的文件体积,Tailor 会在裁剪之后直接进行 zlib 流压缩。流程大致如下:
- 调用 Tailor.dumpHprofData() 时,会依次调用 nOpen()、Debug.dumpHprofData()、nClose();
- nOpen 在 native 层开启对 int open(const char* __path, int __flags, ...)和 ssize_t write_proxy(int fd, const char*buffer, size_t count) 的 hook 代理;
- Debug.dumpHprofData 执行中会先调到 open 函数,hook 代理逻辑会过滤出目标文件的 fd;调到 write 函数时 hook 代理逻辑通过 fd 过滤出目标文件的写入数据进行裁剪压缩;
- nClose 逻辑清除之前的 hook 代理。
// isGzip 是否在裁剪之后进行zip压缩
public static synchronized void dumpHprofData(String fileName, boolean isGzip) throws IOException {
nOpen(fileName, isGzip);
Debug.dumpHprofData(fileName);
nClose();
}
Tailor 裁剪压缩效果
实际的裁剪效果取决于具体现场,OOM 现场的快照通常比较大(LargeHeap/非 LargeHeap 的差异也很大),非 OOM 的则要小很多,根据西瓜视频(LargeHeap)的实践经验可以得出以下数据:
- 体积
- OOM:约 50%可以裁剪压缩到 10M 以内。
- 非 OOM:约 60%可以裁剪压缩到 5M 以内,约 90%可以裁剪压缩到 10M 以内。
- 耗时
- 同原生 dump 耗时相差很小:dump 过程的耗时主要集中在两次 ProcessHeap 调用和文件写入上。
- 稳定性
- 基本没有稳定性问题:此开源版本已运行半年以上,未发现有 Tailor 相关的 crash。
西瓜视频治理实践
西瓜视频汇集了短、中、长各类视频资源,人均使用时长超过 100 分钟,同时启动次数又相对较少,导致内存问题会被放大,进而导致治理难度加大。以西瓜视频 Android v4.0.0 为例,这期间 Java crash 约为 6.5 左右(影响用户的 DAU 占比),而其中 OOM 就高达 3.4,占比过半 。
OOM 问题常见的治理思路,基本都是通过内存泄漏检测工具实现的,这类工具的局限性在于其输出的是孤立的内存泄漏 case,缺少对整体堆内存影响的评估,无法从泄漏中看出 OOM 的直接原因,还存在比较严重的误报行为。虽然后续很多新的工具在性能上有所提升,但本质仍属于 LeakCanary 这一体系,并未从根本上解决工具在治理 OOM 时所面临的问题。
针对这种情况,西瓜视频 Android 完全抛弃了线上内存泄漏检测机制,开发完善了 Tailor 内存快照裁剪压缩工具,并以此为核心制定了线上线下同步治理的长效策略:
- 线下开发、回归、Monkey、压测等环节自动集成 LeakCanary 检测内存泄漏;
- 线上 OOM 时通过 Tailor 主动 dump 内存快照,通过回捞快照精准分析 OOM 问题。
该策略将治理防范的重点放到了线下,在建设完善内存问题前置发现能力的同时,也避免线上分析带来的性能影响和问题规模爆炸。同时,通过 Tailor 内存快照裁剪压缩工具和回捞机制,使得整个内存优化治理形成闭环,以线下防范为主,线上精准治理为辅,线上反哺线下,既可以精准高效地治理线上所有的堆内存 OOM 问题,又补充完善了线下监控体系。
经过一段时间的治理,西瓜视频 Android 新版本的 Java 堆内存 OOM 问题从 3.5 降低到了 0.03,直接降低了两个数量级,并能长期以极低的人力投入保持下去。与此同时,我们也通过内存快照解决了大量迭代过程中遇到的其他类型的棘手的异常,不仅拓展了稳定性治理的思路,也沉淀出了新的稳定性治理的方法论。
在实际治理过程中,很多时候对于堆栈无法直接定位的问题,我们只能通过分析业务代码、分析增量代码、AB 实验等方法来定位。当第二次遇到时,即便知道原因,仍然需要重复之前繁琐的调查,治理经验太过主观,很难传承。而通过内存快照则不会有此类问题,快照的分析过程是客观可重复的,每解决一类问题,后续再遇到是完全可以复制之前的分析过程的。
堆内存 OOM 治理
事实上由于泄漏直接导致的 OOM 问题相对较少,能直接导致 OOM 或者内存水位比较高的,更多的是业务逻辑、缓存逻辑等,这些很多是现有检测工具覆盖不到的。事实上对于大多数 App 而言,实际能够导致 OOM 的原因十分有限,通过快照可以很直观的发现问题。
上图所示的是一个 OOM 现场,通过内存泄漏检测工具,的确可以找出多处泄漏,但都不是导致 OOM 的根本原因。即便修复了这些泄漏,显然也无法解决此类 OOM 问题(Android 硬件加速逻辑的漏洞,导致大量 byte[] 被 JNI Global 持有而泄漏)。
其他Crash治理
内存快照也是及其重要的数据现场,对于调查数据状态相关稳定性问题,是极为重要的数据补充。如果我们在非 OOM 类的 crash 时,也能获取内存快照,那么就获取了crash 时完整的内存状态。对于堆栈无法定位的问题,可以结合源码和快照数据来辅助调查问题,以下是三个典型的案例:
案例1
上图是一个比较常见的 Java crash 堆栈,堆栈中没有业务相关的信息,对于业务比较复杂的 App,传统手段很难快速定位。通过快照来调查此问题,就变得非常简单了:MAT 里先筛选出 mRecycled 为 true 的 Bitmap 对象,再通过「Path to GC Roots」即可定位。
案例2
上图同样是没有任何业务信息的 crash 堆栈,通过源码判断是在
mListener.onSurfaceTextureAvailable 回调里间接将 mLayer 置空导致的。由于置空代码位于 Framework 层,无法通过打点拿到相关 trace。
最后我们通过快照过滤出 crash时的 TextureLayer 实例,发现其 mAttachInfo 为 null,断定是在回调里执行 removeView 而最终导致 mLayer 被置空的,再通过这个 TextureLayer 实例逐层向上找到 mParent 为 null 的节点,最终找定位到被 remove 的上层节点,进而定位到了问题场景。
案例3
西瓜视频里经常遇到 OutOfMemoryError: pthread_create (1040KB stack) failed 类型的 native OOM,有一类明确因为播放器实例过多,导致 native 层缓存占用过大而 OOM 的。究竟是播放器自身的问题,还是业务层的问题很难判断。如果通过针对性的埋点来搜集数据太被动,而通过快照里 Java 层 player 对象的状态、引用关系来判断则非常简单,此类问题前后有三类:业务层未正确释放 player、player 的异步 release 被 block、standard 的 Activity 过多导致 player 实例过多等。
根据西瓜视频团队的实践,大量无法通过堆栈来定位的问题,通过快照则可以很轻松的定位到原因。那些即便不能直接定位到问题原因的 case,内存快照也可以提供必要的数据支持。以下是西瓜视频团队实践中总结出的典型的可以通过内存快照来辅助调查的问题分类:
- Framework:完整的 Activity、Fragment、View 状态,完整的 Framework 层数据&状态。
- 插件类问题:有完整的插件&状态信息、Class、Classloader 及 dex 信息等等。
- 业务层问题
- 第三方问题
内存快照裁剪后续
作为一个立足于提升稳定性治理效率的基础工具,能在必要的时候为任何可能的异常提供完整通用的数据现场,是其当仁不让的职责。能否提供完整的数据现场,核心集中在 dump、存储、传输三个环节,因而 dump 速度、体积、完整性也就成为了核心优化方向。基于目前的成果,对比 Android 原生的快照 dump 逻辑,内存快照裁剪压缩工具在以下方面还有进一步的优化提升空间:
裁剪压缩比
在保证快照数据尽可能完整的前提下,怎样进一步裁剪体积是个矛盾的问题,基于 hprof 格式裁剪仍有很大空间。同时,也可以探索其他高效的裁剪方案,以裁剪掉最终分析时用不到的数据。
裁剪压缩速度
目前 Tailor 的裁剪压缩耗时跟原生 dump 耗时比较接近,这是因为裁剪压缩的过程耗时有限,主要时间消耗在两次调用 ProcessHeap 和文件写入上,直接干掉第一次调用将会大幅减少整个 dump 耗时。
Dump内存消耗
Android 快照 dump 是在 native 层完成的,dump 过程中每个 Record 都是通过 std::vector
通过分析相关源码可以发现,实际只需要 hook 下列接口,就可以代理 Record 的缓存过程,直接对拦截到的数据进行裁剪压缩,就不会有 Record 缓存空间的问题,也可以提升快照 dump 的速度。
总结
Android 稳定性治理发展至今,相关的监控工具和方法论并不完善。基于内存快照的治理思路和分析方法,将会是传统稳定性治理体系的重要补充,其分析过程更客观、直接、高效,有效减少数据埋点的同时也净化了代码逻辑,将内存快照作为通用异常数据进行搜集可以一劳永逸。
内存快照裁剪压缩是通用异常数据搜集系统里至关重要的一环,是关系到整个技术路线是否通用的核心和关键。Tailor 只是迈开了其中的一小步,方案还有很大的优化空间。开源不是终点,我们希望集思广益、共同探索完善,在 Android 稳定性治理上走的更快更远。
接下来我们会逐步开源并详细介绍西瓜 Android 性能稳定性团队的其他核心监控体系建设,这其中主要有:Raphael(Native 内存泄漏监控工具)和 Sliver(高性能 Trace 工具)等,覆盖 Native 内存泄漏检测、ANR 治理、卡顿治理、基础性能优化等方向,敬请关注!
相关资料
- Tailor 开源地址:https://github.com/bytedance/tailor
- HPROF 协议:http://hg.openjdk.java.net/jdk6/jdk6/jdk/raw-file/tip/src/share/demo/jvmti/hprof/manual.html#mozTocId848088
- xHook 链接:https://github.com/iqiyi/xHook
- Android Camera内存问题剖析 (基于 Tailor 和内存快照的实战案例)
更多分享
欢迎关注「 字节跳动技术团队 」
简历投递联系邮箱「 tech@bytedance.com 」
相关推荐
- 激光手术矫正视力对眼睛到底有没有伤害?
-
因为大家询问到很多关于“基质不能完全愈合”的问题,有必要在这里再详细解释一下。谢谢@珍惜年少时光提出的疑问:因为手头刚好在看组织学,其中提到:”角膜基质约占角膜的全厚度的90%,主要成分是胶原板层,...
- OneCode核心概念解析——View(视图)
-
什么是视图?在前面的章节中介绍过,Page相关的概念,Page是用户交互的入口,具有Url唯一性。但Page还只是一个抽象的容器,而View则是一个具备了具体业务能力的特殊的Page,它可以是一个...
- 精品博文图文详解Xilinx ISE14.7 安装教程
-
在软件安装之前,得准备好软件安装包,可从Xilinx官网上下载:http://china.xilinx.com/support/download/index.html/content/xilinx/z...
- 卡片项目管理(Web)(卡片设计的流程)
-
简洁的HTML文档卡片管理,简单框架个人本地离线使用。将个人工具类的文档整理使用。优化方向:添加图片、瀑布式布局、颜色修改、毛玻璃效果等。<!DOCTYPEhtml><html...
- GolangWeb框架Iris项目实战-JWT和中间件(Middleware)的使用EP07
-
前文再续,上一回我们完成了用户的登录逻辑,将之前用户管理模块中添加的用户账号进行账号和密码的校验,过程中使用图形验证码强制进行人机交互,防止账号的密码被暴力破解。本回我们需要为登录成功的用户生成Tok...
- sitemap 网站地图是什么格式?有什么好处?
-
sitemap网站地图方便搜索引擎发现和爬取网页站点地图是一种xml文件,或者是txt,是将网站的所有网址列在这个文件中,为了方便搜索引擎发现并收录的。sitemap网站地图分两种:用于用户导...
- 如何在HarmonyOS NEXT中处理页面间的数据传递?
-
大家好,前两天的Mate70的发布,让人热血沸腾啊,不想错过,自学的小伙伴一起啊,今天分享的学习笔记是关于页面间数据伟递的问题,在HarmonyOSNEXT5.0中,页面间的数据传递可以有很多种...
- 从 Element UI 源码的构建流程来看前端 UI 库设计
-
作者:前端森林转发链接:https://mp.weixin.qq.com/s/ziDMLDJcvx07aM6xoEyWHQ引言由于业务需要,近期团队要搞一套自己的UI组件库,框架方面还是Vue。而业界...
- jq+ajax+bootstrap改了一个动态分页的表格
-
最近在维护一个很古老的项目,里面是用jq的dataTable方法实现一个分页的表格,不过这些表格的分页是本地分页。现在想要的是点击分页去请求数据。经过多次的修改,以失败告终。分页的不准确,还会有这个错...
- 学习ES6- 入门Vue(大量源代码及笔记,带你起飞)
-
ES6学习网站:https://es6.ruanyifeng.com/箭头函数普通函数//普通函数this指向调用时所在的对象(可变)letfn=functionfn(a,b){...
- 青锋微服务架构之-Ant Design Pro 基本配置
-
青锋(msxy)-Gitee.com1、更换AntDesignPro的logo和名称需要修改文件所在位置:/config/defaultSetting.jsconstproSett...
- 大数据调度服务监控平台(大数据调度服务监控平台官网)
-
简介SmartKettle是针对上述企业的痛点,对kettle的使用做了一些包装、优化,使其在web端也能具备基础的kettle作业、转换的配置、调度、监控,能在很大一定程度上协助企业完成不同...
- Flask博客实战 - 实现博客首页视图及样式
-
本套教程是一个Flask实战类教程,html/css/javascript等相关技术栈不会过多的去详细解释,那么就需要各位初学者尽可能的先去掌握这些基础知识,当然本套教程不需要你对其非常精通,但最起码...
- Web自动化测试:模拟鼠标操作(ActionChains)
-
在日常的测试中,经常会遇到需要鼠标去操作的一些事情,比如说悬浮菜单、拖动验证码等,这一节我们来学习如何使用webdriver模拟鼠标的操作首页模拟鼠标的操作要首先引入ActionChains的包fro...
- DCS F-16C 中文指南 16.9ILS仪表降落系统教程
-
10–ILS教程我们的ILS(仪表着陆进近)将到达Batumi巴统机场。ILS频率:110.30跑道航向:120磁航向/126真航向无线电塔频率:131.0001.设置雷达高度表开关打开(前)并...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)