去哪儿“技术债”偿还实践:如何高效、低风险砍掉50%无用代码?
# 一分钟精华速览 #
底层技术是系统稳定运行的基石,往往牵一发而动全身。通过底层技术的优化,有效地管理和减少代码量,能极大提升系统的运行效率。 去哪儿网作为业内较早落地“代码瘦身”的企业,该项目让其系统成功地减少了50%的代码量,26%的服务数量,提高了9.5%的发布效率。
本文旨在分享其如何运用可观测性技术识别并清除无用代码,并尝试通过还原实施细节、总结方法论,并为读者在系统精简方面提供一种新的思考和实践方式。
温馨提醒:本文约7500字,预计花费12分钟阅读。
背景 一个普遍而持久的问题:随着时间的流逝,系统中逐渐堆积起了大量的历史债务,比如被废弃的模块和无用的代码。这些"历史包袱"不只是让系统变得臃肿,也在新功能的开发或者系统维护时对开发工程师造成困扰。1.1 系统瘦身目标&挑战 “系统瘦身”项目的目标: 削减代码和服务的数量均达到50% 。其中,代码精简是主指标,而服务精简则是次要考虑的目标。但要实现这个目标,其难度和挑战性不可小视。
1.2 系统瘦身整体规划
1.2.1 时间规划
在明确目标后,我们首先进行了时间规划,将整个项目时间分为两个阶段: 前两个月用于进行服务精简,随后则进行代码精简 。我们选择先进行服务精简,是因为服务精简的性价比较高,一旦关闭一个服务,可能会立即减少数万至数十万行的代码。1.2.2 组织架构规划
我们设立了一个瘦身公共支持团队。这是一个虚拟组织,主要提供通用工具和技术支持,以帮助各个业务线的团队进行高效的系统精简。在使用这些工具进行精简过程中,各业务线团队若有需求或遇到技术问题,都可以向公共支援团队寻求帮助。1.3 选择策略
1.3.1 策略一:两阶段法
无论是服务精简还是代码精简,我们都将其分为两个阶段,即“找得到”和“删得好”。 “找得到”阶段,即寻找那些可以被删除或可以被优化的服务和代码;“删的好”阶段,即实际进行删除和优化操作。1.3.2 策略二:四步筛选模型
我们为查找阶段抽象出了一个通用的筛选模型,包括四个步骤:挖掘特征、度量特征、收集数据和匹配特征。
二、如何实现自动化且低风险的服务精简?
2.1 识别可精简的服务 根据我们的“两阶段”策略,寻找可精简的服务是第一阶段的任务。在这个过程中,首先需要确定具有精简潜力的服务。为了寻找这些特征,我们采用了两种策略:删除服务和合并服务,以达到服务精简的目标。
2.2 度量特征
前面提到可删除的服务一种是不迭代,一种是没流量。
2.3 删除服务
2.3.1 建立服务精简平台
在识别出无流量且不再迭代的服务,即识别出低价值服务后,便可以进入服务精简的第二阶段,也就是删除阶段。在此阶段,关键在于如何“删得好”,即如何将删除服务的操作流程标准化。一旦流程标准化,我们就可以建立一个服务精简平台,将标准化的流程实现在平台中,从而轻松实现自动化的服务删除。2.3.2 服务删除流程
在去哪儿旅行,我们定义了一个服务删除的标准流程,主要分为四个阶段:确认期、预收回期、观察期和回收期。2.4 一些实践经验 以下是关于服务删除的一些实践经验。
3.1 可精简代码特征分析
我们的策略依然是“两个阶段”——确定可以被精简的代码,然后进行代码的精简。 关于可能被精简的代码,我这里列举了三种情况 —— 首先,可以通过静态代码扫描找出一些未被引用的方法。例如,如果A方法依赖B方法,但A方法没有在任何地方被引用,那么A方法就有可能被精简掉。 其次,也可以通过线上运行时的状态分析,找出那些没有流量经过的方法。这些方法也是有可能被删除的。 最后,还可以通过代码重构,简化流程处理,减少代码重复,使得代码组织更清晰,这也有助于减少代码量。
3.2 度量选型 接下来,将主要关注如何处理没有流量的方法这一问题。本节 主要讨论的是Java技术栈 ,因为在去哪儿,90%以上的系统都是基于Java体系。
3.2.1 方案一:面向切面编程 (AOP)
在Java中,可以采用面向切面编程(AOP)的技术。可以在每个方法前添加切面,使得每次方法执行都会记录日志。然后,在线上运行一段时间后,通过扫描日志来识别哪些方法没有流量。这是一种直观的策略。3.2.2 方案二:基于Agent的字节码插桩
另一种更底层的技术是基于Agent的字节码插桩。可以在JVM启动时添加Agent参数,进行字节码插桩。其逻辑和基于AOP的方法相似,都是为每个方法添加日志记录,然后通过分析日志来找出没有流量的方法。3.2.3 方案三:基于Serviceability Agent (SA)工具
第三种方法是使用Serviceability Agent(SA)工具。大家都知道Java代码有两种执行方式:初始阶段代码会被解释执行,当代码热度达到一定次数或一定时间内的执行次数后,会进行编译执行。 在JVM中,每个方法的执行次数都被记录在一个字段中。通过SA工具,可以使用Java语言读取JVM中的这些数据。SA工具其实并不复杂,它可以暴露Java中的一些对象,并可以探测运行的JVM中每个方法的执行次数。其基本原理是探测JVM中的各种状态值进行分析。 那么,如何基于SA找到没有流量的方法?首先,需要对SA进行包装,可以附加到JVM进程上。然后,通过探测JVM中每个方法的调用次数,将数据保存下来。 整个过程可以称为"跑数",即探测JVM中方法计数并保存结果的过程。3.2.4 三种方案的比较
在选择度量的方案时,需要关注的是稳定性,最好的方案应该是对线上业务无影响。我们将三种方案 根据性能损耗、故障风险和实现复杂度进行了评估 。最终,选择使用SA工具。通过对SA的深度优化,可以做到对线上业务性能损耗为零,故障风险也为零。3.3 度量方案的关键实施步骤 前文有提到,使用SA进行数据抽取时会导致STW,增加内存消耗,且需要运行两三分钟。那么,如何才能在这种情况下不影响业务呢?关键在于控制SA跑数的时机。
3.3.1 定时跑数
首先,我们考虑的是定时跑数的方案,例如每日执行一次,针对线上的某个服务进行运行。 设想服务就像一堆容器或KVM,包含许多Pod,我们随机选择一个Pod进行下线,但这个下线只是暂停接收线上流量,实际上它的JVM进程仍然在运行。 停止服务后,使用SA对其进行探测。 此时,即使发生STW或内存增加,也无关紧要,因为它已经不再接收线上流量。 探测完成后,再将Pod上线,恢复正常服务。 因此,整个过程对业务没有任何影响。3.3.2 下线时跑数
考虑到以上的困难,我们 提出了一种更好的方案 。不再定时跑数,而是与系统管理员的操作配合,例如我们会经常进行服务的发布、重启或下线等操作,这些操作都会进入后续的灰度流程。 在下线前,会进行SA跑数,即使此过程导致系统挂掉也无所谓,因为最终都会进行下线操作。同样,如果是发布新版本,也没关系,因为这个容器后面会拉取新的Pod。这个方案能实现对业务性能无影响、故障风险为零的效果。 然而, 这个方案也有一个局限性 ,那就是如果一个服务非常稳定,一年都不需要重启或发布新的版本,那么就只能回到原来的策略,即定时跑数。例如,可以对所有的服务进行检查,找出那些已经运行两三个月而没有发布新版本的服务,然后给这些服务定时跑数。3.3.3 SA跑数代码实现
在实现SA跑速的代码部分,需要根据解释执行和编译执行的不同,使用不同的API去探测它。3.3.4 计算可精简方法集
通过上述代码,可以获取到一份结果,其中包含了函数的唯一标识、调用次数和代码行数等字段。然而,仅仅跑一次是不够的,我们需要进行长期的跑数,例如跑2个月的数据。在这个时间范围内,会发现一些函数始终没有被调用,这时可以确定这些函数是没有流量的。 将每次跑数的结果进行聚合并取并集,就能得到所有有流量的方法集。另外,通过静态代码分析,可以获取到工程方法全集。最后,用方法全集减去有流量的方法集,就得到了没有流量的方法集,也就是最终可以精简的方法集。3.3.5 业务流程图
3.4 代码清理的多种手段 在代码清理阶段,我们会对那些无流量的代码进行彻底的删除。
3.4.1 全自动化清理策略
最初,我们推出了全自动的删除策略。 看似理想 的这种策略,具有高度自动化和高效率的特点。通过此方式,程序会自动克隆项目代码,创建分支,然后自动删除无流量的方法。完成后,程序会推送分支,然后通知人工进行代码审查(CR),最后发布。此策略的主要优势在于,前面的所有步骤都是全自动完成的,唯一需要人工进行的是代码审查和发布。3.4.2 半自动清理策略
因此,后来推出了一种半自动化的策略。 开发了一个Idea插件 ,能够扫描整个工程中所有无流量的方法。3.4.3 代码清理最佳实践
总的来说,我们提供了两种不同的策略,以适应业务的重要程度。对于重要性较低的业务,可以进行全自动化的精简;对于重要性较高的业务,则进行半自动化的精简。3.5 一些实践经验
四、最终效果如何?
- 代码量减少50%,服务减少26%;
- 故障率持续下降,维持在0.3%以下;
- 需求处理的平均耗时减少10.9%;发布效率提升9.5%。
五、总结展望
最后,我想用一张图来做个总结。