前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >去哪儿“技术债”偿还实践:如何高效、低风险砍掉50%无用代码?

去哪儿“技术债”偿还实践:如何高效、低风险砍掉50%无用代码?

作者头像
TakinTalks稳定性社区
发布2023-12-04 12:23:26
3540
发布2023-12-04 12:23:26
举报
文章被收录于专栏:TakinTalks稳定性社区

作者介绍

去哪儿旅行基础架构组技术专家——马阳阳

TakinTalks稳定性社区专家团成员,去哪儿旅行基础架构组技术专家。公司云原生 SIG 成员,负责测试环境治理平台、代码精简平台、组件市场等,专注于研发效能领域。2022 年深度参与的“线上代码精简50%”项目获得公司级技术型一等奖,指导多个团队完成系统精简,积累了大量经验。

温馨提醒:本文约7500字,预计花费12分钟阅读。

背景

一个普遍而持久的问题:随着时间的流逝,系统中逐渐堆积起了大量的历史债务,比如被废弃的模块和无用的代码。这些"历史包袱"不只是让系统变得臃肿,也在新功能的开发或者系统维护时对开发工程师造成困扰。

我们可以看看去哪儿旅行的一组数据:每个开发人员平均需要维护6个应用,每个人负责的代码量大约是十几万行。在这些经常使用的应用中,有20%的应用在一年内都没有进行过任何变更。更令人震惊的是,线上方法行的覆盖率仅有40%,这意味着,有60%的代码是没有流量经过的,被视作无用代码。

这些数据揭示了许多问题,比如老旧系统的维护成本高昂,时间估计误差较大,开发进度缓慢,这些问题都可能影响到系统的稳定性。最终,这将导致效率和质量的双重下降。为了解决这些问题,去哪儿旅行在2022年初启动了一个名为“系统瘦身”的项目,在不影响系统运行的前提下,成功地剔除了千万行无用代码。

本文将分享如何运用可观测性技术来识别并清除无用代码。我将从服务和代码两个角度详细分享项目的实施细节,并会探讨在服务和代码精简后,对研发效能、质量和效率等指标的提升效果。期望通过本文的经验分享,能够帮助读者在系统精简方面有更深的理解和更好的实践。

一、如何制定目标和系统瘦身策略?

1.1 系统瘦身目标&挑战

“系统瘦身”项目的目标:削减代码和服务的数量均达到50%。其中,代码精简是主指标,而服务精简则是次要考虑的目标。但要实现这个目标,其难度和挑战性不可小视。

首先,目标的设定其实非常高。减少50%的代码和服务,意味着我们需要在保证系统正常运行的前提下,削去一半的代码。我相信听到这一要求,许多人都会感到难以置信。

其次,精简工作的范围广泛,涉及到各个业务线。这个项目是公司级别的项目,去哪儿旅行的每一条业务线,包括国内和国际机票、门票、酒店、火车票等,都需要完成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方法就有可能被精简掉。

其次,也可以通过线上运行时的状态分析,找出那些没有流量经过的方法。这些方法也是有可能被删除的。

最后,还可以通过代码重构,简化流程处理,减少代码重复,使得代码组织更清晰,这也有助于减少代码量。

在使用这三种方法时,需要评估每种方法的效果。具体来说,需要考虑两个指标:一是能够删掉的代码总量,如果这个量大,说明该方法的效果好;二是该方法的通用性,如果具备通用性,就可以通过编程自动完成代码精简,从而提高效率。

对于未被引用的方法,虽然其数量可能不大,但是具备通用性。对于没有流量的方法,其数量是非常大的,并且也具备通用性。之前提到,在去哪儿旅行,线上有60%的代码都是没有流量的。至于代码重构,其效果大小与系统有关,对于新的系统,重构可能带来的效果较小。虽然理论上,基于规则的重构是通用的,但这部分量往往很少,大部分重构都需要基于对业务的理解,结合业务进行,所以整体而言,重构并不具备完全的通用性。

因此,对于量大的两个方法——未被引用的方法和没有流量的方法,将由公共支持团队提供工具进行处理。而对于代码重构,这需要业务线团队自己去完成。

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中方法计数并保存结果的过程。

在跑数时,需要注意几个关键点:首先,JVM处于STW(Stop-the-World)状态,即业务线不工作;其次,跑数时长一般为1到3分钟;第三,跑数过程中需要查看内存中各个参数的情况,可能会额外消耗一些内存。

3.2.4 三种方案的比较

在选择度量的方案时,需要关注的是稳定性,最好的方案应该是对线上业务无影响。我们将三种方案根据性能损耗、故障风险和实现复杂度进行了评估。最终,选择使用SA工具。通过对SA的深度优化,可以做到对线上业务性能损耗为零,故障风险也为零。

3.3 度量方案的关键实施步骤

前文有提到,使用SA进行数据抽取时会导致STW,增加内存消耗,且需要运行两三分钟。那么,如何才能在这种情况下不影响业务呢?关键在于控制SA跑数的时机。

3.3.1 定时跑数

首先,我们考虑的是定时跑数的方案,例如每日执行一次,针对线上的某个服务进行运行。设想服务就像一堆容器或KVM,包含许多Pod,我们随机选择一个Pod进行下线,但这个下线只是暂停接收线上流量,实际上它的JVM进程仍然在运行。停止服务后,使用SA对其进行探测。此时,即使发生STW或内存增加,也无关紧要,因为它已经不再接收线上流量。探测完成后,再将Pod上线,恢复正常服务。因此,整个过程对业务没有任何影响。

但是,我们发现这个方案在实际应用中存在许多困难。首先,JVM在运行时需要有足够的剩余内存。如果剩余内存很低,例如只有一两百兆,使用SA跑数会很可能导致系统卡住,进而无法正常结束或成功跑数。因此,需要预留充足的内存。通常,内存使用量与JVM的内存使用量正相关。在SA跑数时,内存大约会增加500M到1G。

其次,需要确认服务是否有多个实例。如果只有一个实例,下线操作会导致服务直接中断。如果有两个实例,下线一个,那么它也会变成单点,这也是无法下线的。只有在服务具有两个以上的实例时,才能选择一个进行下线。

最后,还需要考虑多环境问题。在我们的系统中,每个服务可能运行在多个环境中,每个环境可能有不同的流量,但代码只有一份。这就需要针对每个环境都进行一次跑数。

3.3.2 下线时跑数

考虑到以上的困难,我们提出了一种更好的方案。不再定时跑数,而是与系统管理员的操作配合,例如我们会经常进行服务的发布、重启或下线等操作,这些操作都会进入后续的灰度流程。

在下线前,会进行SA跑数,即使此过程导致系统挂掉也无所谓,因为最终都会进行下线操作。同样,如果是发布新版本,也没关系,因为这个容器后面会拉取新的Pod。这个方案能实现对业务性能无影响、故障风险为零的效果。

然而,这个方案也有一个局限性,那就是如果一个服务非常稳定,一年都不需要重启或发布新的版本,那么就只能回到原来的策略,即定时跑数。例如,可以对所有的服务进行检查,找出那些已经运行两三个月而没有发布新版本的服务,然后给这些服务定时跑数。

3.3.3 SA跑数代码实现

在实现SA跑速的代码部分,需要根据解释执行和编译执行的不同,使用不同的API去探测它。

(跑数代码实现)

解释执行方法的代码实现很简单,分为三个步骤:实现特定的Visitor接口、根据Visit方法入参获取到Method结构、根据Method结构状态判断是否有流量。通过这些步骤,可以快速识别出没有流量的方法。

(代码实现-解释执行方法)

(代码实现-编译执行方法)

3.3.4 计算可精简方法集

通过上述代码,可以获取到一份结果,其中包含了函数的唯一标识、调用次数和代码行数等字段。然而,仅仅跑一次是不够的,我们需要进行长期的跑数,例如跑2个月的数据。在这个时间范围内,会发现一些函数始终没有被调用,这时可以确定这些函数是没有流量的。

将每次跑数的结果进行聚合并取并集,就能得到所有有流量的方法集。另外,通过静态代码分析,可以获取到工程方法全集。最后,用方法全集减去有流量的方法集,就得到了没有流量的方法集,也就是最终可以精简的方法集。

3.3.5 业务流程图

3.4 代码清理的多种手段

在代码清理阶段,我们会对那些无流量的代码进行彻底的删除。

3.4.1 全自动化清理策略

最初,我们推出了全自动的删除策略。看似理想的这种策略,具有高度自动化和高效率的特点。通过此方式,程序会自动克隆项目代码,创建分支,然后自动删除无流量的方法。完成后,程序会推送分支,然后通知人工进行代码审查(CR),最后发布。此策略的主要优势在于,前面的所有步骤都是全自动完成的,唯一需要人工进行的是代码审查和发布。

然而,实际部署这种策略后,发现很多业务团队不敢或不愿使用。原因是业务同学会觉得直接创建了一个分支,然后在代码审查平台上看到大量的代码被删除,他们无法判断被删除的代码量,也无法确定哪些代码应该被删除,哪些不应该。因此,他们会认为发布存在风险。

3.4.2 半自动清理策略

因此,后来推出了一种半自动化的策略。开发了一个Idea插件,能够扫描整个工程中所有无流量的方法。

在插件的左侧,提供了一个可以被删除的方法列表。用户可以在这个列表中进行全选,也可以按包进行选择。选择完之后,右侧提供了一些一键删除方法的功能。这时,删除流程就变成了人工通过Idea插件件进行手动删除。因为每一个方法都是他们亲手删除的,所以他们认为在删除过程中的可控力度更强,风险也更低。

3.4.3 代码清理最佳实践

总的来说,我们提供了两种不同的策略,以适应业务的重要程度。对于重要性较低的业务,可以进行全自动化的精简;对于重要性较高的业务,则进行半自动化的精简。

3.5 一些实践经验

四、最终效果如何?

  • 代码量减少50%,服务减少26%;

我们设定的目标是将代码量减少50%,最终,恰好实现了这一目标,同时服务数量也减少了26%。整个项目实施周期超过半年,期间并未发生任何严重的故障。

  • 故障率持续下降,维持在0.3%以下;

从数据中可以看出,故障率基本上呈现出下降的趋势。随着代码瘦身项目和其他稳定性保障项目的相继落地,当前的故障率维持在0.3%以下。

  • 需求处理的平均耗时减少10.9%;发布效率提升9.5%。

我们对比了项目实施前后两个时间段的需求处理平均耗时,发现项目实施完成后,处理需求的平均时间减少了10.9%。此外,由于代码量的减少,从克隆代码到编译,再到JVM运行等每个步骤的时间都有所减少,最终导致发布效率提升了9.5%。

五、总结展望

最后,我想用一张图来做个总结。

“给我一个API,我就能减少一半的代码”。这句话主要是想引导大家进行深思,底层技术虽然可能看起来比较枯燥或者无趣,但其所能带来的价值却无比巨大。回顾整个项目,我们投入了大量的人力资源,涉及了诸多团队,最终成功减少了数千万行的线上代码。从技术角度来看,核心技术仅是使用了一个 JVM 工具的 API。然而从业务角度来看,这直接影响到了公司的服务质量、用户体验,乃至整个公司的效率和运营成本。因此,底层技术能够为上层应用提供无限的可能性,这也鼓励我们持续积极探索新的技术和方向,通过我们的专业技术尽可能地提升公司的整体效能。(全文完)

Q&A:

1、刚提到的服务下线前和下线后,通知到具体负责人?你们是有使用什么工具或者平台吗?

2、原来是通过运行时去分析函数被调用次数。你们有没有遇到一年才运行一次的定时任务吗?这种代码会被误删吧,如果采用你们这种方法。

3、业务团队没有人力的时候公共支撑团队来支援,那公共支撑团队的工作量怎么管理和控制?

4、SA技术进行跑数时,会不会存在可能影响线上服务的风险?

5、精简中有没有出现过故障的问题,都是哪些情况导致的故障?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 TakinTalks稳定性社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.2.1 时间规划
  • 1.2.2 组织架构规划
  • 1.3.1 策略一:两阶段法
  • 1.3.2 策略二:四步筛选模型
  • 二、如何实现自动化且低风险的服务精简?
    • 2.3.1 建立服务精简平台
      • 2.3.2 服务删除流程
        • 3.2.1 方案一:面向切面编程 (AOP)
          • 3.2.2 方案二:基于Agent的字节码插桩
            • 3.2.3 方案三:基于Serviceability Agent (SA)工具
              • 3.2.4 三种方案的比较
                • 3.3.1 定时跑数
                  • 3.3.2 下线时跑数
                    • 3.3.3 SA跑数代码实现
                      • 3.3.4 计算可精简方法集
                        • 3.3.5 业务流程图
                          • 3.4.1 全自动化清理策略
                            • 3.4.2 半自动清理策略
                              • 3.4.3 代码清理最佳实践
                              • 四、最终效果如何?
                              • 五、总结展望
                              • Q&A:
                              相关产品与服务
                              容器服务
                              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档