前言

从3月中旬到前几天,我的工作重心一直在符号还原服务的重构上;整个重构从提案、方案设计,到难点攻关、核心功能实现,最后到功能验证,性能优化以及搭建监控和压力测试。全程的体验可以说是历尽艰险,但也成就感满满

符号还原系统的开发告一段落,这里我就遵循空雨伞的思考方式来总结下整个重构工作

需要了解的词

  • symbol demangle

    在swift, C++, Rust等语言中,为了唯一标识变量/类/方法等程序实体,编译器以函数、结构、类或其他数据类型的名称对附加信息进行编码,以便将更多语义信息从编译器传递到链接器(如不同包中的同名方法等)

  • 符号还原

    根据平台的不同,程序的运行时堆栈中可能充满了内存地址或混淆后的函数名;这样的堆栈是无法阅读的。而符号还原则是将它们转换为人类可读的类名/方法名、文件名和行号的过程。本文中的符号还原泛指iOS、Java Native等内存地址形的符号还原以及Android, javascript等混淆形的符号还原

Android 混淆堆栈

Caused by: java.lang.Exception: Exception at the end of the call
at ly.count.android.demo.a.b(SourceFile:29)
at ly.count.android.demo.a.a(SourceFile:21)
at ly.count.android.demo.ActivityExampleCrashReporting.c(SourceFile:98)
at ly.count.android.demo.ActivityExampleCrashReporting.b(SourceFile:94)
at ly.count.android.demo.ActivityExampleCrashReporting.a(SourceFile:90)
at ly.count.android.demo.ActivityExampleCrashReporting.onClickCrashReporting10(SourceFile:82)

Android 符号还原后堆栈

Caused by: java.lang.Exception: Exception at the end of the call
at ly.count.android.demo.Utility.void DeepCall_b()(SourceFile:29)
at ly.count.android.demo.Utility.void DeepCall_a()(SourceFile:21)
at ly.count.android.demo.ActivityExampleCrashReporting.void deepFunctionCall_3()(SourceFile:98)
at ly.count.android.demo.ActivityExampleCrashReporting.void deepFunctionCall_2()(SourceFile:94)
at ly.count.android.demo.ActivityExampleCrashReporting.void deepFunctionCall_1()(SourceFile:90)
at ly.count.android.demo.ActivityExampleCrashReporting.void onClickCrashReporting10(android.view.View)(SourceFile:82)

事实

项目中的老翻译服务的呈现形式为分端实现,各端分别部署微服务提供翻译服务(consumer, web);这造成了很多额外的运维成本和维护成本,实际上大部分的翻译层逻辑是相同的,核心逻辑都是地址 / 混淆后符号到符号的映射,在redis缓存,符号表管理以及符号表监控等方面也都可以使用统一的架构和解决方案。在这些前提下,统一的符号还原模块成为了可能

思路

1

实现符号还原大一统的大前提是统一各端符号表,要实现各端形式各异的符号表到统一格式的能实现高效率翻译的符号表的转换

统一符号表格式后,则要考虑符号表的管理,在历史各端符号还原服务的生命历程中,有不少的问题都是符号表管理不当导致的,排查起来也相当痛苦。在统一的符号还原服务中,符号表的管理粒度依旧为产品 → 版本号 → 符号表,但需要依据MECE原则设计完整的符号表生命周期,保证整个符号表系统的可控性和可维护性

其次是符号表缓存结构设计,通过redis缓存减少实际高并发翻译中与符号表的文件IO,减轻服务器压力的同时增加整个翻译服务的吞吐量

在符号表的相关难点攻克后,我们才能开始真正的符号翻译,整个翻译流程需要保证架构和实现的健壮性,高性能,可维护性,以支持实际翻译服务中的各种需求

最后是监控方案的设计与实现,首先要基设计翻译服务的SLI以及SLO,再基于翻译流程接口以及符号表生命周期进行监控埋点,完成翻译层监控Dashboard的配置

行动

思路清晰,开始行动

技术攻坚

首先要解决我们统一符号还原的大前提——将各端的符号表转换为统一格式的符号表

各端的符号表基本可以分为内存地址形和混淆形符号表,本质都是映射关系(内存地址 -> 符号 / 混淆符号 -> 原始符号)

那么要统一这两类符号表,我们的思路就是将混淆类的混淆符号首先转换为数字,这里就要面对两个问题:

  • 散列哈希算法的选择(满足字符串 → 数字,且散列效果较好)
  • 面对 Android 符号表 / js 符号表这类有多层结构的符号表时,如何在压平结构后保证最终的结果不溢出

经过多次技术评审后最终我们攻克了这里的技术难关,完成了符号表统一;在后续的过程中面临的架构设计、监控埋点等一系列其它难题时,团队内也是不断通过技术评审集思广益,解决关键问题的同时保证信息差最小,一一攻克难关

稳步开发

在主要的技术难点都基本确定了解决方案后,我们开始了正式的统一符号还原模块的开发。

基于已经设计好的满足 golang 标准项目结构的包结构设计,逐个完成各核心 package 的开发(symbolmap, transform, translate等),以及 library 内部分基础组建的更新(kafka, redis分布式锁, resource等),并编写完善的单元测试。在完成符号还原模块的主体部分后迅速开始交叉测试工作,最终在3个星期内完成了符号还原模块本身的交付

进度推进

在符号还原模块开发的过程中受到了整个项目大版本交付进度的催促,而在符号还原模块这个开发阶段,大家都还沉浸在性能优化与监控完善方面的工作中,有些忽略了整体进度;后续也是及时将数据流验证提到最高优先级,在较短的时间内完成了各数据流的验证,交付项目后,再捡起符号还原模块本身的性能优化和监控系统完善的工作,继续完成了大文件符号表转换的性能优化,符号表管理api接口拓展,部分架构调整以及监控指标建设等工作

测试收尾

在最终的符号还原模块交付前,我们继续做了私有云环境的适配工作以及整个符号还原模块的系统测试和压力测试,同时验证监控面板、监控指标的有效性。在不间断的测试过程中,也发现了符号还原服务接入的部分数据流上的适配问题,以及在kafka版本上的适配问题等关键问题,完成了及时的修复,同时也不断迭代优化项目内的符号表生命周期、翻译流程等,完善监控面板,保证一眼就能抓住关键信息。

总结

重新以空雨伞的视角审视这次的符号还原服务重构工作,less & well 如下:

less

后期的进度把控以及测试工作上还有待改进

  • 进度把控上没有把握好优先级,过度沉浸在了项目开发工作本身,而忽略了交付进度
  • 虽然在前期完成了大量单元测试的编写,保证了核心 package 的可用性,但在环境适配、组件测试以及数据流接入测试等方面没有以较高的优先级做好、

well

前期的工作动机、思路整理和核心开发工作都差强人意

  • 整体的重构思路完整清晰,遵循MECE原则,保证重构工作完整可控
  • 技术难点的攻克上坚持了技术评审,保证信息透明,集思广益
  • 在完成准备工作后快速完成了核心代码的开发,为后续的一系列监控和测试工作留足了时间