作者介绍
马阳阳:去哪儿网基础架构组资深开发工程师、公司云原生 SIG 成员,专注于云原生、效能领域。负责组件市场、测试环境治理平台 Noah、代码瘦身平台。优化过 Noah,使环境构建成功率提升 35%、耗时降低 30%。22 年深度参与的“线上服务瘦身50%”项目获得公司级技术型一等奖,指导多个团队完成系统精简,积累了大量经验。毕业后曾就职于 BES,从事 PaaS 云、DevOps 平台开发工作。
一、背景
去哪儿早在 05 年就开始做机票相关业务,在这十几年间业务快速发展,后端系统也在不断迭代,目前公司内拥有大量五年以上的系统。为适应快速变化,公司的组织架构调整和人员流动较多,导致目前开发者们维护的系统,大部分都是交接过来的,对系统全貌掌控力有限。
另一方面,公司的很多业务具备短周期特征,比如临近五一假期了,我们会在去哪儿旅行 App 上增加五一相关的活动业务。一旦五一假期结束后,这些业务会立刻被下掉,不再产生实际价值。而对应的后端代码并不会清理,这就产生了一个有趣的现象,我们的代码只增不减、系统复杂度单调递增。
随着时间的推移,维护和优化现有代码的耗时会越来越多,导致开发新业务的时间减少,业务开发越来越慢,跟不上业务发展的速度,急需解决!
从数据上看,公司有数千个常用应用,数千万行代码,平分到每个开发者头上,人均要维护多个应用、十几万行代码,可以感受到这个维护负担是比较重的。
如何解决维护成本高、业务迭代慢的痛点呢?我们在去年全年开展了系统瘦身项目,制定了两个目标指标,对全司代码、全司应用均精简 50%,也就是都砍掉一半。要完成这个目标,有三个很大的挑战:
- 目标定的非常高,需要在一年内删除掉千万余行代码;
- 这是全司级别的项目,牵扯几十个业务团队、几千个线上应用,如何在这么广的影响范围下,高效、低风险地达成目标,将是个挑战;
- 网上找不到可以参考的案例,这就要求我们要有很强的创造力和技术实力。
要实现这个目标,我们做了两个规划:
- 时间上,分两阶段,从 5 月份到 6 月底进行服务精简,从 7 月初到 11 月底进行代码精简。之所以服务放在前面,是因为服务精简的同时也会带来代码的精简;
- 架构上,将整个目标分配到各个业务线,每个业务线完成 50% 精简的目标,分而治之。另外,创建了一个虚拟组织“瘦身技术支撑团队”,提供通用瘦身工具和技术支持,加速业务线的瘦身效率。本人也是该虚拟组织中的一员。
接下来依次对服务精简、代码精简做详细介绍,会先分析可以被精简的资源有什么通用特征,然后利用这些通用特征批量、自动化地找到可能能被精简的资源全集,这个过程称之为 “找得到”;有了目标全集之后,进行正真的精简,也就是要 “删得好”。
二、服务精简实战
1、可精简服务特征分析
在进行服务精简实战之前,需要先分析出可精简服务所具备的特征,有了一些通用的特征后,就能根据特征批量找到全部的目标服务。
服务精简的手段有两种,合并服务和删除服务,这两种手段所处理的服务特征不一样,因此分开进行分析。
合并服务
哪些服务能进行合并?想直接回答这一问题并不容易,可以反过来思考,哪些服务要进行拆分?开发同学对微服务拆分都很熟悉,常见拆分原则有单一职责、界限上下文、复用性、自治性等等,常见服务拆分维度有 “业务” 和 “质量”,根据业务拆分的常见手段包括:
- 按业务域:卖票和售后属于不同业务,在微服务架构中考虑拆分成两个微服务
- 按业务流程:对于订单业务,按流程可以拆分成 生单、计费、支付、物流、通知 等多个子服务
- 按业务重要性:订单业务一般是核心业务;评价业务则没那么重要,所以订单和评价通常是两个微服务
根据质量拆分的常见手段包括:性能、稳定性、可用性、安全边界、异构等。
理论上,规避了上述服务拆分特征的服务就允许进行合并。
判断当前服务是否符合服务合并的特征,需要对服务所提供的业务有深刻理解才能进行判断,难以通过已有的数据资产分析得出,只能是系统开发者进行判断,因此服务合并主要由各业务线自行推进。
删除服务
哪些服务能被直接删除?从业务价值角度很容易得出结论,无价值甚至低价值服务可以进行删除,更为具体的表现有三种,满足其中一种就可能是低价值服务,注意是 “可能而非一定”:
- 没流量:服务虽然在线上,但没有业务流量,业务价值自然是低的
- 不迭代:这个特征不好理解,可以通过价值模型加深理解。价值分为 “存量价值” 和 “增量价值”,服务一旦没有迭代,就没有了增量价值。先利用这个特征将服务扫出来,再人工判断一下其存量价值,如果存量价值较高那么就不做删除,反之则反
- 已下线:服务已经下线了,但没有被删除
判断当前服务是否可被删除,是可以通过已有数据分析得出的,因此作为瘦身基础支撑团队,提供了两个通用工具,第一个工具的作用是 “找得到”,即找出符合特征的服务全集;第二个工具的作用是“删得好”,自动化将服务进行删除。最终达到快速删除服务的作用,下面对 “找得到”、“删得好” 进行介绍。
2、找得到
现在需要按照之前分析得出的特征,通过已有数据资产,批量找到符合特征的服务,这些服务就是最终要删除的备选目标。
没流量
将流量全集分为三类:
- 南北方向流量:南北方向流量通常是指从客户端到服务器端的流量,也就是来自于外部用户或者应用的请求流量,它通过负载均衡器或API网关被路由到不同的微服务实例上
- 东西方向流量:东西方向流量通常是指微服务之间的内部通信流量,也就是微服务之间的请求和响应流量,这种流量通常发生在内部微服务的调用或者微服务之间的数据交换过程中。这里我将东西间流量进一步细分,拆成两类:
- 东西方向 – 服务之间流量
- 东西方向 – 单服务内流量
三类都没有流量则可以认为服务是完全没有流量的,尽可能保证判断的全面性。
针对每种类型,可以通过分析现有数据资产,来得出是否有特定类型的流量。
- 根据网关的历史访问日志,可以找出一段时间内,没有南北向流量的服务集
- 根据历史 Trace 的拓扑信息,可以分析出一段时间内,没有东西向服务间流量的服务集
- 最后,根据服务是否有生效的定时任务,可以找出没有服务内流量的服务集
最终对这三个集合取交集,即为全部 “没流量” 的服务。
不迭代
从业务上看,不进行服务迭代有两种可能,业务非常稳定了不需要迭代、业务不值得再花时间迭代。无论哪种情况,不迭代的具体表现是没有变更,同时满足下面两种情况我们就认定服务是无变更的:
- 代码无变更:根据发布系统中的发布记录,找到一段时间内没有代码发布、或发布次数很少的服务
- 配置无变更:根据分布式配置中心的变更记录,找到一段时间内没有配置发布、或发布次数很少的服务
已下线
服务是否已下线 很好判断,只需要取一下当前全部服务的状态,筛选出下线的服务。为了更精准,可以取两个时间点的状态,这两个时间点要有一定的跨度,当两个时间点服务都是下线状态,则判定服务 “已下线”。
3、删得好
找到了全部可能可以被删除的服务后,接下来就是删服务了,删服务要保证三个点:
- 删得准:禁止误删
- 删得全:删除服务时,要把服务相关资源都清理掉,比如服务的域名、机器等
- 删得快:删除服务的效率要高
为此,我们制定了服务删除标准流程,搭建了 “应用瘦身平台”。
在流程中增加人工确认步骤,保证 “删得准”;整理服务删除所牵扯的相关资源清理过程,沉淀到瘦身平台中,通过设计服务删除全流程 与 自动化删除能力,保证 “删得全” 和 “删得快”。
平台的关键设计点是服务删除的流程标准化,我们将服务删除分为了四个周期、十个步骤,全流程如下图:
三、代码精简实战
1、可精简代码特征分析
精简代码的思路和精简服务一样,从已知的、具体的操作入手,分析出可精简代码的通用特征,然后基于特征、利用工具批量找到可精简代码全集,最后执行精简代码的操作。
常见的可精简代码方法有三个:
- 静态代码分析,删掉未被调用的方法。在 IDEA 里对这类方法是直接能提示出来的
- 重构代码,包括简化代码写法、精简逻辑、减少重复代码等
- 想办法找出 长期没有线上流量的代码,这些代码大概率是能被删除的
这三个手段都能精简掉代码,但效果并不一样,因此我抽象了两个指标,从 ”量大“ 和 ”通用性“ 上来评估每个手段的优劣。为什么是这两个指标,意义何在?“量大” 决定了能删除的代码数量,“通用性” 影响的是能否自动化进行,决定了删除代码的效率,最终会先选择同时具备这俩指标的手段,这样能最快的删除最多的代码,具体的评估数据如下图:
通过对已有代码的全量度量,我们发现 “静态未被调用的方法” 占比是很少的,因此量不大
重构是件复杂的事情,有时涉及到架构重构,牵扯多个微服务,需要对业务很熟悉,所以只能由业务线同学自行推进,难以找到通用的手段 自动完成重构;重构能减少的代码行数是很多的,因此这也是达到最终目标的抓手之一
经过实际的代码分析,发现没有流量经过的代码是很多的,尤其对于历史悠久的系统,以及活动类的系统,业务没了但代码通常不会删掉,这就产生了大量无流量的废弃代码。对于如何找到这些无流量的代码,并进行删除,都具有通用性,因此作为工具平台组,我们在这个方面做了大量工作,也是分为 “找得到” 和 “删得好”,下面依次进行介绍
2、“找得到” 方案选型
常规方案
需要通过技术手段找到没有线上流量的代码,比较容易想到的方案有两个:
1、AOP:
利用 Java 中的 AOP 技术,动态增强每个方法,在其中增加访问计数逻辑,将源码中的方法全集减去有计数的方法,得到没有线上流量的方法,示例代码如下:
2、Agent 字节码插桩:
通过自实现 Agent,对源码进行字节码插桩,增加记录访问日志的逻辑,并在 JVM 启动时设置 -javaagent 参数来加载 Agent,剩下 “计算没有线上流量的方法” 和 AOP 方案类似,不再赘述。流程图如下:
上面两个方案比较容易想到,除此之外,我们找到了第 3 个方案,基于 SA 工具
3、SA 方案
先不着急介绍 SA 是什么,而从我们熟悉的入手,一步步推理、引出 SA。
在 JVM 中,Java 代码可以通过解释执行(Interpreted Mode)和编译执行(Compiled Mode)两种方式来运行,默认情况下使用混合模式(Mixed Mode)来运行应用程序,即 将解释执行和编译执行相结合。JVM 在应用程序启动时会先进行解释执行,同时使用即时编译器来编译热点代码。当热点代码被编译执行后,就可以使用本地代码来代替解释执行,从而提高应用程序的性能。如果有新的热点代码出现,即时编译器会对其进行编译执行。
那么 JVM 如何判断出热点代码?猜测一下,有一个方法粒度的计数器,统计了方法执行次数,当这个次数超过阈值时触发编译。
通过查看 hotspot 源码完成了验证,在 Method 类中确实有方法计数相关的字段。
有办法通过 java 代码读取到 JVM 中方法计数相关的字段吗?我们找到了一个工具 Serviceability Agent,这是 JVM 开发者为了调试 JVM 而制作的工具 Agent,主要功能是暴露运行时 JVM 中的 Java 对象和数据结构,因此它能把 JVM 中 Method 对象的全部字段都暴露出来,使用 Java 代码进行读取。
SA 官方介绍地址:https://openjdk.org/groups/hotspot/docs/Serviceability.html
SA 并不高深,常见的 JVM 工具 如 jstack、jmap,底层也是用了 SA
方案选型
对于上面三个方案的选型,从三个维度进行评估:
对于方法执行的计数,SA 方案利用 JVM 已有机制就记下来了,因此该方案可以达到 对业务服务没有任何性能损耗的效果,零风险。并且拿到的直接是方法执行次数,不需要额外的计算和聚合,实现复杂度低,因此最终选用了 SA 方案。
从结果上看,SA 方案确实非常好,最大优点就是零风险,整个项目推进过程中,没有产生过任何故障。
3、SA 方案详细设计
本节将对 SA 方案中的几个关键设计点进行详细介绍。
性能无损跑数
使用 SA 对运行中的 JVM 进行方法计数探测,这个过程称之为 “跑数”。直接使用 SA 观测线上的 JVM 是会对性能有影响的,因为在探测期间,用户线程处于 STW 状态,内存占用也会适当增加,想要做到完全的性能无损跑数,需要利用好服务上下线,具体过程如下图:
首先随机挑选一个线上的实例,进行实例下线,下线只会让线上流量不再打到该实例上,JVM 还是跑着的。然后对下线后实例,使用 SA 进行跑数,完成跑数后等待几分钟,对实例进行上线,这样就不会因为跑数,而对线上流量有性能影响了。
跑数有三个避坑点需要注意:
SA 跑数时会消耗一定内存,因此在跑数前 JVM 必须留有足够的剩余内存(通常 大于 500M),这样才能成功跑出数,否则 JVM 会发生 OOM 甚至长时间卡住的问题
服务必须是多实例的,对于单实例服务直接跑数会导致服务全部下线,影响业务。这种情况建议扩容到多个实例,或者放弃跑数
有时 prod 类型的环境会有多个,它们使用的是同一份代码,此时需要对每个环境都挑实例进行跑数
跑数代码实现
SA 就是一个 jar 包,提供了观测 JVM 的 API,本节主要介绍跑数的代码实现。
JVM 对于解释执行和编译执行,方法的结构是不一样的,因此需要区分处理,代码如下:
代码中的 API 运用了观察者模式,其中 InvocationCounterVisitor 和 CompiledMethodVisitor 是我们自定义的观察者,两者的逻辑类似:
- 实现观察者的接口,实现核心的观察(visit)方法
- 通过方法入参,获取到 Method 对象
- 从 Method 对象中取出方法计数器值,保存到结果集中
核心代码如下图:
计算可精简方法集
跑一次数,能探测到 “从 JVM 启动到跑数时刻” 这段时间内,每个方法的执行次数。跑完数之后,得到了一个方法集合,包含方法标识、执行次数等信息:
但是仅跑一次数是不够的,可能在 “从 JVM 启动到跑数时刻” 这段时间之后,有方法被第一次执行了,此时就会产生遗漏。因此要多次、有时间跨度地跑数,比如每天跑一次,持续跑一个月,这样才能获取到更准确的数据,减少遗漏概率。
有了多次跑数结果后,现在开始计算 “可精简方法集”。首先,要对多次的跑数结果取并集,获得全部 “有流量的方法集”;然后,分析工程源码,利用开源工具(如 Spoon)获取 “工程方法全集”;最后,在 “工程方法全集” 中排除掉 “有流量的方法集”,得到最终的 “可精简方法集”,过程如下图:
校准可精简方法集
如之前所述,采用了随机抽取实例进行跑数的方式,这就可能产生遗漏问题,例如:某服务线上实例个数为 600 个,有一个定时任务定向的分发到指定的一台机器上,此时理想情况下,随机跑数 600 次后就会命中一次定时任务所在的实例,抓取到定时任务方法的执行记录。但很有可能,跑了 600 次也没跑到定时任务所在的机器,这就产生了有流量方法集的遗漏,最终导致计算出来的可精简方法集有误判。
要解决这个问题,第一反应是能不能对所有实例都跑数?这样一定能避免遗漏,但根据实际经验,这种方式风险过高,试想一下,每天跑一次数,每次都需要把服务的全部实例轮着下线、上线一遍,操作之繁琐、风险之大可想而知。
我们最终使用的方案是,对于 SA 跑出来的可精简方法集,在源码层面、通过 IDEA 批量加上打点(具体如何增加,下文会介绍),发布到线上后再跑一段时间,根据打出的点校准可精简方法集,这样就能确保百分百的准确性了,在代码中增加的打点如下图:
源码中加打点,不会带来性能损耗吗?确实存在损耗,但基本可以忽略不计,因为这些要加打点的方法,是 SA 长期跑出来的可精简方法集,代表方法执行频率极低甚至没有,因此对这些方法增加打点,能产生的性能影响极少。
4、整体方案
最后,将上面的点串联起来看一下整体的方案,业务流程图如下:
- 定时任务平台,定期触发瘦身服务的跑数逻辑
- 瘦身服务通过发布平台,随机下线一台实例
- 瘦身服务通过 salt 平台,下发跑数的脚本任务
- 在已下线的实例上完成跑数,将结果上报到瘦身服务
- 瘦身服务将跑数的原始结果(每个方法的执行次数等信息)进行存库
- 瘦身服务通过发布平台,将下线的实例进行上线
- 瘦身服务克隆源码,获取 “工程中方法全集”
- 从打点监控平台(watcher)上取到全部瘦身相关打点,转换成有打点的方法集合
- 将 SA 跑出的结果方法集 和 有打点的方法集,从 工程方法全集 中排除,得到最终的 “可精简方法集”,存库
5、删得好
代码想要删得好,自动化就少不了。我们设计并提供了两种代码删除方式,分为 全自动 和 半自动,以适配不同重要性的业务团队。
全自动
整个代码删除过程完全自动化,包含 克隆代码、创建瘦身分支、删除可精简代码、推送分支 4 个步骤。最后需要开发同学进行 CR,没问题后走后续的验证、发布过程。
删除代码时有个注意点,需要保证删除后的代码可正常编译。如果直接删除方法签名和方法体,就可能导致无法分支无法正确编译,这是不能被接受的。
半自动
在全自动方案中,开发者只有在最终 CR 时才能看到被删除的代码,此时如果有大量需要恢复的代码,操作起来会比较麻烦,需要手动回滚。同时,为了让开发对删除的代码有更强的掌控力,因此设计了半自动方案。
半自动方案中,我们开发了一个 idea 插件,该插件能扫出可精简的方法集合,提供批量删除方法、置空方法体、增加打点等功能。开发者利用该插件,手动、高效地进行代码删除工作,并且对每个方法都有精确掌控,风险更低。
验证
代码删完、人工 CR 确认后,仍需要做好测试、验证工作 才能最终发布到线上,具体包含以下六个步骤:
- beta 发布:先在 beta 环境进行发布
- 自动化测试:自动化测试平台对精简后的服务,在 beta 环境完成自动化测试
- 故障演练:故障演练是为了保障强弱依赖没有问题
- 灰度发布:保险起见,对线上环境先进行灰度发布,并进行观察
- 全量发布:灰度发布、观察没问题后,进行全量发布
- 观察指标:全量发布完成后,需要对各种指标观察一段时间,发现问题及时回滚
四、最终效果
最终我们成功精简了 49.87% 的代码,代码总量减少超千万,取得了超预期的效果,具体如下:
- 开发估时平均降低约 10.9%:在删除冗余代码、精简系统后,每个需求的估时和开发耗时明显降低,预计每年可以节省近万 PD 成本;
- 发布效率提升约 9.5%:代码大量减少后带来了更快的发布速度,从克隆到编译到运行,各阶段耗时均有缩短。单次提升看似有限,但实际每月有几万次发布,综合收益是很可观的。
综上所述,系统瘦身是一项有价值的工作,且实际价值超出预期。
五、未来展望
前面分别从服务精简和代码精简角度,分别介绍了特征分析、找得到、删得好、实施经验,希望能够帮助大家将代码量降下来,降低维护成本,提升开发效率。在未来,还有以下五个方面的工作可以继续探索和实施:
- 代码行粒度精简:目前的方案是方法粒度的,更进一步可以细分为行粒度,有利于圈复杂度的降低
- 长周期代码精简:一段代码如果很长时间才执行一次(如 一年一次),执行的时候不在 SA 跑数期间,就会产生误判,该方法应该保留
- 依赖精简:对服务依赖的 jar 包进行精简,减少依赖数量,避免依赖冗余、减少不兼容的情况
- 配置精简:清理服务所依赖的配置项
- 服务合并:探索服务合并的通用路径,完成自动化服务合并功能
作者:马阳阳
来源:微信公众号:Qunar技术沙龙
出处:https://mp.weixin.qq.com/s/H-QedPzfsT88w0n33g6ZhA
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。