InfoQ|日活超过 3 亿的快手是怎么进行性能优化的?( 二 )


验收:对于一些重要的优化 , 我们会通过 AB 上线验收效果 , 回收技术数据和产品数据(例如优化 FPS , 我们会看相关页面 CTR 的变化) 。
防劣化:我们会制定一些机制、方案 , 防止已经优化的数据在日常的迭代中劣化 , 包括每个版本灰度期间的监控、线下实验室环境测试、开发阶段提醒等 。
InfoQ:怎么进行内存优化?
杨凯:内存主要是 Java 和 C 两部分 , 对于 Java , 我们研发了一套线上裁剪、分析、上传用户本地镜像的方案 , 可以做到用户内存不足时 , 快速而准确地上报当时的 Java 内存状态 , 初步可以判断出来泄露和内存大户是谁 。 上报后我们会对所有用户的信息做汇总、展示 。 Java 部分的内存监控主要做了以下几点:
内存镜像转储 , 我们研发了一种高效 dump 方案 , 解决了传统方法虚拟机内存转储需要暂停虚拟机的问题 。
内存镜像分析 , 研发了基于 shark 的低内存开销、低 CPU 开销的独立进程解析方案 , 采用了更为节省内存的高性能数据结构以及更为高效的内存索引 , 增加了同类型对象阈值用于 GCRoot 最短路径搜索剪枝 , 可以在手机侧 10 分钟内完成 400M 镜像、200 万 对象的极端 case 解析 。
内存镜像裁剪 , 我们研发了一种 hook 虚拟机内存镜像转储时 IO 的高效裁剪方案 , 解决了传统裁剪效率低、成功率低的问题 , 辅以 zstd 压缩 , 90% 内存镜像可以压缩至 80M 内 。
InfoQ|日活超过 3 亿的快手是怎么进行性能优化的?
本文插图

C 的内存我们主要利用编译器插桩及 malloc hook 记录所有活着的内存块 , 利用 mark-and-sweep 算法在单独的进程中分析测试应用进程 Native Heap 中不可达的内存块 。 将发现的不可达内存上报后台 。 具体操作如下:
利用编译器插桩及 malloc hook 记录所有活着的内存块(包含内存块地址、backtrace 信息) , 对性能影响较小 。
利用 mark-and-sweep 算法在单独的进程中分析测试应用进程 Native Heap 中不可达的内存块(包含内存块地址) 。
对于步骤 2 中收集到的不可达内存块 , 从 1 中获取其对应的 backtrace 信息 , 将泄漏信息上报至 APM 监控平台 。
APM 监控平台解析泄漏信息(backtrace 信息符号化等) , 做友好的展示 , 业务方根据 APM 展示信息可快速定位泄漏问题 。
InfoQ:如何优化卡顿?
杨凯:我们定义的卡顿是:一个消息 / 任务在主线程执行超过 1s 。
优化主要看卡顿的堆栈特征、当前 CPU 占用、其它线程正在执行的任务 。 通常有以下几种情况:CPU 占用过高(一般是主线程或者子线程任务重) , 主线程等锁(需要看其它线程当时的任务) , 系统服务忙(binder 调用耗时长) 。 解决方案一般需要结合场景、页面 , 增加 log 等 , 丰富更多上报信息 , 定位主要问题 , 或者缓解 CPU 占用 。
InfoQ:在启动优化这部分做了哪些动作 , 优化前后对比效果如何?
杨凯:启动优化我们主要是建立启动框架 , 将启动所有的任务 , 全部收敛到框架内 , 统计每个任务的耗时、相互依赖关系 。
启动优化定位问题:
将启动时运行的代码 , 按照功能 , 做成 task;
线上收集每个 task 的耗时;
在线下 , 在 Android 端利用 systrace、在 iOS 端利用自研的火焰图工具 , 来分析耗时 。
优化手段:
优化整体流程;
分场景、分用户特性 , 推迟甚至取消一些 task (根据用户登录状态 , 用户使用习惯 , 后面我们会用机器学习预测 task 是否需要初始化);
特别关注一些锁的等待、主线程 CPU 分配不足等问题;
一些系统 API , 背后会引发一系列的初始化(setcookie , 会引起 WebView 内核初始化);


推荐阅读