找回密码
 会员注册
查看: 27|回复: 0

转转服务瘦身实战

[复制链接]

4

主题

0

回帖

13

积分

新手上路

积分
13
发表于 2024-9-20 07:18:28 | 显示全部楼层 |阅读模式
1背景2第一步-发现并下掉僵尸服务2.1如何发现僵尸服务2.2如何下掉僵尸服务3第二步-发现并下掉僵尸方法3.1如何发现僵尸方法3.2如何删除僵尸方法4第三步-发现并下掉僵尸组件依赖4.1如何发现僵尸组件依赖4.2如何下掉僵尸组件依赖5总结与成果参考1背景2023年转转迎来了他的8周岁生日,祝贺转转8岁生日快乐。8岁的人还只是个小朋友,8岁的转转成熟稳重,而许多8岁的代码已经迟暮。互联网公司的业务有一个特点,那就是快速迭代。许多功能的生命周期非常短暂,这带来3个问题。有些服务已经没有业务流量,却仍然占用服务器资源,称之为僵尸服务。有些代码已经不再调用,却仍然存在于服务项目中,代码变得臃肿,难以维护、优化,称之为僵尸代码。有些组件依赖如MySQL、Redis、RPC服务等,已经不再调用却仍在连接,称之为僵尸组件依赖。针对这3个问题,转转架构部制定了3步走计划,在下文中详细阐述。2第一步-发现并下掉僵尸服务直接下掉一个服务可获取最大回收收益,项目代码可删除,占用的服务器资源可回收。且经过评估,技术难度较低,短期内可获得较大收益。所以把下掉僵尸服务放在了第一步。2.1如何发现僵尸服务僵尸服务是指已经没有业务流量,却仍然占用服务器资源的服务。在转转公司,服务入口流量大致分为以下4种。经nginx转发的http/WebSocket流量。RPC服务流量。MQ消费。定时任务平台调度。私有协议流量/服务内部定时任务。对于前4种流量我们有标准的prometheus监控,可以很容易抓取到。而第5种流量需要RD自定义监控指标,瘦身系统通过自定义的指标抓取监控。瘦身服务每日从监控平台抓取流量监控,每月1日跑出1个月内无流量的服务,并通知服务负责人。2.2如何下掉僵尸服务虽然通过技术手段已经确定服务没有流量,但贸然删除服务节点及其代码仍然是不可取的,对线上服务要始终保持敬畏之心。经过仔细评估,我们制定了如下的服务下线流程。在下掉服务节点后15天内如果发现问题仍然可以随时拉起服务,终止下线流程。3第二步-发现并下掉僵尸方法删除僵尸方法的收益中等,并不能节省服务器资源,更侧重于防止项目代码腐败。技术难度中等。所以放在了第2步。僵尸方法就是指长期没有调用的方法,如果想获取僵尸方法的集合,只需要取项目全量方法和活动方法(有调用的方法)的差集,如下图所示。3.1如何发现僵尸方法3.1.1全量方法的获取首先是采用什么技术获取全量方法,经过调研,我们采用了spoon工具扫描项目源码获取全量方法,示例代码如下。    private static void doScanJavaFile(String javaVersion, File javaFile, List sourceCodeJavaMethodList) {        Launcher launcher = new Launcher();        launcher.addInputResource(new FileSystemFile(javaFile));        launcher.getEnvironment().setNoClasspath(true);        launcher.getEnvironment().setAutoImports(true);        launcher.getEnvironment().setComplianceLevel(Integer.parseInt((javaVersion.contains(".") ? javaVersion.substring(2) : javaVersion)));        Collection> allTypes = launcher.buildModel().getAllTypes();        for (CtType type : allTypes) {            String className = type.getQualifiedName();            for (CtMethod method : type.getMethods()) {                SourcePosition position = method.getPosition();                sourceCodeJavaMethodList.add(new SourceCodeJavaMethod(className, method.getSignature(), position.getEndLine() - position.getLine() + 1));            }        }    }其次是扫描时机。在瘦身服务上线时对公司内所有项目源码进行一次全量扫描。在服务每次上线完成合并代码到master后再发起一次扫描。每周日对公司内所有项目源码进行一次兜底全量扫描。3.1.2活动方法的获取活动方法也就是在jvm运行期间调用过的方法,对活动方法的统计经过调研大致有3种实现方案。SpringAOP此方案要求所有需要监控的方法所在的类都是springbean,对业务代码有侵入性,并且实现复杂度高。javaagent字节码增强通过在jvm启动参数中加入javaagent参数。对源码中的方法进行增强和监控,此方案对业务代码无侵入性,但是实现复杂度高。ServiceAbilityAgent简称SA,是hotspot虚拟机提供的一种调试工具集,我们常用的jvm命令如jmap、jstack也是采用了该技术。在JVM中,Java代码有两种执行方式,即解释执行和编译执行。JVM会首先进行解释执行,并对解释执行的方法进行计数,超过一定的阈值后则使用jit编译器将字节码编译成本地代码。对于解释执行的方法在SA的Api中用sun.jvm.hotspot.oops.InstanceKlass类表示,而编译执行的方法则以sun.jvm.hotspot.code.CodeBlob类表示。只需要将ServiceAbilityAgentattach至进程上,就可以从其api中获取所有的InstanceKlass和CodeBlob。3种方法的对比如下:方案性能损耗代码侵入性实现复杂度SpringAop高高高JavaAgent低无高SA无无低经过对比发现ServiceAblilityAgent展现出无与伦比的优势。SA唯一的问题是当进程被attach后,至采集完成detach期间,整个进程处于stoptheworld状态,该问题在下文中有详细解决方案。3.1.3ServiceAbilityAgent方案详解3.1.3.1ServiceAbilityAgent使用方法SA在各大版本间不兼容。转转线上有jdk8和jdk17,jdk8中SA以独立jar包的形式存在,位于$JAVA_HOME/lib/sa-jdi.jar,需要手动添加至classpath中,而jdk17不需要。以下为示例代码。获取InstanceKclass数据public class KlassVisitor implements SystemDictionary.ClassVisitor {    private List out;    public KlassVisitor(List out) {        this.out = out;    }    @Override    public void visit(Klass klass) {        if (klass instanceof InstanceKlass) {            String className = klass.getName().asString();            MethodArray methods = ((InstanceKlass) klass).getMethods();            for (int i = 0; i > 3;                if (invocationCount > 0) {                    String name = method.getName().asString();                    String signature = method.getSignature().asString();                    this.out.add(new CalledMethod(className, name, signature, invocationCount));                }            }        }    }}获取CodeBlob数据public class CodeBlobVisitor implements CodeCacheVisitor {    private List out;    public CodeBlobVisitor(List out) {        this.out = out;    }    @Override    public void visit(CodeBlob codeBlob) {        if (codeBlob == null) {            return;        }        NMethod nMethodOrNull = codeBlob.asNMethodOrNull();        if (nMethodOrNull == null) {            return;        }        Method method = nMethodOrNull.getMethod();        if (method == null || method.isNative()) {            return;        }        String className = method.getMethodHolder().getName().asString();        String methodName = method.getName().asString();        String signature = method.getSignature().asString();        long invocationCount = method.getInvocationCount() >> 3;        out.add(new CalledMethod(className, methodName, signature, invocationCount));    }}3.1.3.2解决stoptheworld对业务流量的影响在上文中我们总结了转转公司的4种主要流量入口有经nginx转发的http请求、RPC服务请求、MQ消费、定时任务调度。而这4种流量我们都实现了在进程不结束的情况下调用api进行流量下线的能力。在流量下线30秒后对jvm进程进行活动方法采集,在采集后重启进程,流量自然恢复。对于有其他特殊流量的服务,我们提供了手动调用命令进行采集的方案。可由RD自行采用其他方案下掉进程流量,如手动调用接口,通过apollo配置等。在自行下掉流量后可手动调用命令进行活动方法的采集。3.1.3.3采集时机虽然实现了流量下线的能力,并在流量下线30秒后进行采集,但是仍然有某些定时任务的执行时间会超过30秒。为了尽量减少对业务的影响,需要尽量避开长耗时定时任务时间。在最终实现中我们我们允许RD设置每个服务的采集时间,精确至分钟。每分钟运行一次定时任务,对配置该在该分钟内的服务进行采集。3.1.3.4采集节点的选择目前转转每个服务都有1到n个子集群(一组相同启动参数节点的集合),每个子集群的功能略有差异,方法的调用也有所不同,每次采集时从所有子集群中选择1个节点进行采集。进程的启动时间也是采集时需要考虑的因素之一,我们选择的是(启动时间-30天前的时间戳)取绝对值最小的节点。首先,刚刚启动的节点,方法还没有充分调用,不适合采集;其次启动时间过久的节点,比如1年以上的节点,也不适合采集,因为采集到活动方法可能只在1年前调用过,1年之后没再调用过。3.2如何删除僵尸方法有了全量方法和活动方法,从全量方法集合中减去活动方法集合就得到了僵尸方法。怎样删除僵尸方法也是个需要考虑的问题,大致有3种可供选择的方案。3.2.1全自动删除使用程序全自动删除风险太高,而且有一定的不准确性。不准确性来源于事实上活动方法集合是包含于有用方法集合。某些用的方法可能永远也不会调用到,比如出现某种异常时的兜底方法,如果异常几十年不出现,这个兜底方法几十年都不会有调用,但是这种方法不能删除。某些调用到的方法也会采集不到,比如关闭方法,因为活动方法的采集在进程关闭之前,关闭方法暂时还未调用。3.2.2手动删除由RD到服务瘦身平台上手动查询僵尸方法,并结合业务实际情况,再决定是否删除。该方式准确性高,但是不友好。3.2.3半自动删除我们开发了idea插件,由插件自动扫描出僵尸方法,再由RD结合业务实际情况决定是否删除。该方式效率高,操作友好。最终我们选择了这种方式。该插件支持设置僵尸时间天数,自动扫描出僵尸方法,并提供快速删除方法按钮。4第三步-发现并下掉僵尸组件依赖下掉僵尸组件依赖的收益较低,而现有监控条件不足以满足要求,需要进一步开发,复杂度较高,所以放在了最后一步。4.1如何发现僵尸组件依赖僵尸组件依赖的发现仍然依赖promethues监控,对于某些组件如RPC、Codis、RocketMQ等,所用的中间件都是转转自研或者二次开发过的,已经提前在中间件中埋入了监控,直接使用现有监控数据即可。而有些组件使用的是开源中间件,无法修改源码。对于开源中间件我们使用javaagent技术进行字节码增强加入监控,比如在MySQL驱动中加入监控,如下图所示。4.2如何下掉僵尸组件依赖暂时没有很好的可以自动化或者半自动化下掉僵尸组件的方法,目前我们的做法是检测到僵尸组件依赖后,向服务负责人发送邮件,最终由RD修改代码来下掉僵尸组件依赖。5总结与成果本文详细介绍了转转在服务瘦身方面的技术实现方案,尤其是代码瘦身部分,甚为详细。希望能对读者有所帮助,如遇技术问题可联系转转架构部。当前该项目收获的成果如下:发现僵尸服务功能上线较早,从10月1日至12月20日,共下线服务30个,实例数68个,节省内存246GB。发现僵尸代码功能上线不久,仍处于试用期,暂未有丰硕成果。但是我们统计了已接入服务的代码利用率,当前综合方法利用率仅43%,行数利用率仅50%,未来可期。发现僵尸组件依赖功能刚刚上线,目前数据量较小,还不足以得出结论。关于作者王建新,转转架构部服务治理负责人,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~参考狂砍千万行代码,零故障!去哪儿网系统瘦身技术揭秘代码瘦身的设计思想及技术内幕
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 13:04 , Processed in 0.333832 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表