性能监控实现原理,去哪儿系统高可用之法

原标题:去何方系统高可用之法:搭建故障演习平台

关系知识点:APM, Java
Agent,
plugin, bytecode, asm, InvocationHandler, smail

作者介绍

风流倜傥. 背景介绍

APM : 应用程序品质管理。 二零一三年时外国的APM行业 NewRelic 和 APPDynamics
已经在该领域拔得头筹,国内近些年来也出现存的APM商家,如: 听云,
OneAPM, 博睿(bonree卡塔尔(قطر‎ 云智慧,阿里百川码力。
(据深入分析,本国android端方案都以抄袭NewRelic集团的,由于该商店的sdk未混淆,产业界良心卡塔尔

能做什么:
crash监察和控制,卡顿监察和控制,内部存款和储蓄器监察和控制,扩展trace,网络质量监察和控制,app页面自动埋点,等。

王鹏,二〇一七年参与去何方机票职业部,首要从事后端研究开发专门的学业,最近在机票工作部负担路程单和故障演习平台以致国有服务ES、数据同步中间件等连锁的研发专业。

二. 方案介绍

新匍京娱乐场最全网站,品质监察和控制其实正是hook
代码到项目代码中,进而造成各类监督。常规手段都以在档次中加进代码,但什么做到非侵入式的,即二个sdk就可以。

去何方网二〇〇六年树立现今,随着系统规模的日渐扩大,已经有超多少个应用类别,那些系统里面包车型大巴耦合度和链路的复杂度不断抓牢,对于大家创设布满式高可用的类别构造具备宏大挑衅。大家供给叁个阳台在运营期自动注入故障,核实故障预案是还是不是起效——故障演习平台。

1. 如何hook

断面编制程序-- AOP。

笔者们的方案是AOP的豆蔻梢头种,通过校订app
class字节码的款式将大家项目标class文件进行改变,进而成就放权大家的监督检查代码。

新匍京娱乐场最全网站 1

通过查看Adnroid编写翻译流程图,能够领略编译器会将有所class文件打包称dex文件,最后打包成apk。那么大家就供给在class编写翻译成dex文件的时候实行代码注入。比方笔者想计算有些方法的实行时间,那自身只必要在每一种调用了这么些法子的代码前后都加二个光阴总结就可以了。关键点就在于编译dex文件时候注入代码,那个编写翻译进程是由dx推行,具体类和办法为com.android.dx.command.dexer.Main#processClass。此措施的第二个参数正是class的byte数组,于是大家只要求在步入processClass方法的时候用ASM工具对class进行更换并替换掉第三个参数,最平生成的apk就是大家改变之后的了。

类:com.android.dx.command.dexer.Main

新的困难:
要让jvm在实施processClass以前先举办大家的代码,一定要对com.android.dx.command.dexer.Main(以下简单称谓为dexer.Main)实行改建。怎样技能落得那一个目标?那个时候Instrumentation和VirtualMachine就上台了,参照他事他说加以考察第一节。

一、背景

2. hook 到哪里

黄金时代期最首如果互连网品质监察和控制。怎么样能收获到网络数据

由此调研发现脚下有下边聚焦方案:

  • root手机,通过adb 命令举办收缴。
  • 建设布局vpn,将享有网络须求实行收缴。
  • 参照他事他说加以考察听云,newrelic等制品,针对特定库开展代理截获。或许还会有任何的不二等秘书籍,须要继续调研。近些日子大家仿照效法newrelic等百货店出品,针对一定互联网诉求库开展代理的的法子进行网络数据截获。比方okhtt3,
    httpclient, 等网络库。

那是某职业部的种类拓扑图:

三. Java Agent

In general, a javaagent is a JVM “plugin”, a specially crafted .jar
file, that utilizes the Instrumentation API that the JVM provides.

出于大家要修正Dexer 的Main类, 而该类是在编写翻译时期由java虚构机运转的,
所以大家须求通过agent来改良dexer Main类。

javaagent的要害职能如下:

  • 能够在加载class文书以前作拦截,对字节码做校勘
  • 可以在运维期对已加载类的字节码做更换

JVMTI:JVM Tool
Interface,是JVM暴揭示来的一些供客户扩张的接口集合。JVMTI是基于事件驱动的,JVM每推行到早晚的逻辑就能够调用一些事变的回调接口(假使有的话),那个接口能够供开垦者扩大本身的逻辑。

instrument agent: javaagent功用就是它来兑现的,此外instrument
agent还有个别名为JPLISAgent(Java Programming Language Instrumentation
Services
Agent卡塔尔(قطر‎,这些名字也截然体现了其最本质的功用:便是特意为Java语言编写的插桩服务提供扶持的。

三种加载agent的格局:

  • 在运营时加载,
    运转JVM时钦赐agent类。这种办法,Instrumentation的实例通过agent
    class的premain方法被传出。
  • 在运维时加载,JVM提供生机勃勃种当JVM运转实现后开启agent机制。这种情况下,Instrumention实例通过agent代码中的的agentmain传入。

参照例子instrumentation
功用介绍(javaagent卡塔尔国

有了javaagent, 大家就足以在编写翻译app时再也校正dex
的Main类,对应改善processClass方法。

新匍京娱乐场最全网站 2

4. Java Bytecode

哪些改善class文件?
大家要求精晓java字节码,然后须求明白ASM开拓。通过ASM编制程序来改良字节码,进而改革class文件。(也得以应用javaassist来张开更换)

在介绍字节代码指令以前,有必不可缺先来介绍 Java 虚构机奉行模型。大家领悟,Java
代码是
在线程内部执行的。各个线程都有自个儿的实践栈,栈由帧组成。每一个帧表示二个方法调用:每趟调用三个办法时,会将七个新帧压入当前线程的施行栈。当方法重回时,也许是正规再次来到,大概是因为特别再次回到,会将那一个帧从实行栈中弹出,施行进程在发出调用的措施中持续开展(那些方
法的帧以往坐落于栈的下边卡塔尔国。

每蓬蓬勃勃帧包蕴两部分:三个有的变量部分和二个操作数栈部分。局部变量部分含有可依照索引
以自由顺序访谈的变量。由名字能够见到,操作数栈部分是一个栈,此中积攒了供字节代码指令
用作操作数的值。

系统之间的注重性特别复杂、调用链路很深、服务中间从未分支。在这里种复杂的依靠下,系统爆发了几起故障:

字节代码指令

字节代码指令由三个标记该指令的操作码和一向数指标参数组成:

  • 操作码是二个无符号字节值——即字节代码名
  • 参数是静态值,分明了纯正的吩咐行为。它们紧跟在操作码之后给出.举例GOTO标志指令(其操作码的值为
    167卡塔尔(قطر‎以二个指明下一条待试行命令的标识作为参数标志。不要
    将指令参数与指令操作数相混淆:参数值是静态已知的,存款和储蓄在编写翻译后的代码中,而
    操作数值来自操作数栈,独有到运营时工夫驾驭。

参考:

科学普及指令:

  • const 将什么数据类型压入操作数栈。
  • push 表示将单字节或短整型的常量压入操作数栈。
  • ldc 代表将何以品种的数目从常量池中压入操作数栈。
  • load 将某项目标有的变量数据压入操作数栈顶。
  • store 将操作数栈顶的数量存入钦命的生龙活虎对变量中。
  • pop 从操作数栈顶弹出多少
  • dup 复制栈顶的数据并将复制的值也压入栈顶。
  • swap 调换栈顶的数目
  • invokeVirtual 调用实例方法
  • invokeSepcial 调用超类布局方法,实例最早化,私有方法等。
  • invokeStatic 调用静态方法
  • invokeInterface 调用接口
  • getStatic
  • getField
  • putStatic
  • putField
  • New

Java源代码

public static void print(String param) {
    System.out.println("hello " + param);
    new TestMain().sayHello();
}

public void sayHello() {
    System.out.println("hello agent");
}

字节码

// access flags 0x9
  public static print(Ljava/lang/String;)V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "hello "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

    NEW com/paic/agent/test/TestMain
    DUP
    INVOKESPECIAL com/paic/agent/test/TestMain.<init> ()V
    INVOKEVIRTUAL com/paic/agent/test/TestMain.sayHello ()V
    RETURN

public sayHello()V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "hello agent"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN
  • 弱重视挂掉,主流程挂掉,改善报废凭据的支出景况,下单主流程战败;
  • 主干服务调用量陡增,某服务超时引起相关联的富有服务“雪崩”;
  • 机房网络可能有个别机器挂掉,不可能提供基本服务。

5. ASM 开发

出于程序剖判、生成和改动本事的用处众多,所以大家针对广大语言完结了过多用以解析、
生成和转移程序的工具,这个语言中就包涵 Java 在内。ASM 正是为 Java
语言设计的工具之生龙活虎,
用于实行运作时(也是脱机的卡塔尔国类生成与转移。于是,大家设计了
ASM1库,用于拍卖经过编写翻译 的 Java 类。

ASM 并不是惟黄金时代可生成和转变已编写翻译 Java
类的工具,但它是流行、最高效的工具之风华正茂,可 从
下载。其利害攸关优点如下:

  • 有叁个简易的模块API,设计宏观、使用方便。
  • 文书档案齐全,拥有一个连锁的Eclipse插件。
  • 支撑最新的 Java 版本——Java 7。
  • 小而快、非常可信。
  • 具有宏大的客户社区,可感到新客商提供支撑。
  • 源许可开放,大概允许私自使用。

新匍京娱乐场最全网站 3

核心类: ClassReader, ClassWriter, ClassVisitor

参考demo:

{   
    // print 方法的ASM代码
    mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "print", "(Ljava/lang/String;)V", null, null);
    mv.visitCode();

    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
    mv.visitLdcInsn("hello ");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

    mv.visitTypeInsn(NEW, "com/paic/agent/test/TestMain");
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKESPECIAL, "com/paic/agent/test/TestMain", "<init>", "()V", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "com/paic/agent/test/TestMain", "sayHello", "()V", false);

    mv.visitInsn(RETURN);
    mv.visitEnd();
}

{
   //sayHello 的ASM代码
    mv = cw.visitMethod(ACC_PUBLIC, "sayHello", "()V", null, null);
    mv.visitCode();
    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitLdcInsn("hello agent");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

    mv.visitInsn(RETURN);
    mv.visitEnd();
}

多少个故障原因:

6. 落到实处原理

  • 系统强弱信任混乱、弱信任无降级;
  • 系统流量猛增,系统容积不足,未有限流熔断机制;
  • 硬件能源互联网现身难题影响系统运营,未有高可用的网络结构。

1. Instrumentation和VirtualMachine

VirtualMachine有个loadAgent方法,它钦定的agent会在main方法前运维,并调用agent的agentMain方法,agentMain的首个参数是Instrumentation,那样我们就能够给Instrumentation设置ClassFileTransformer来达成对dexer.Main的退换,相符也得以用ASM来贯彻。经常的话,APM工具包罗八个部分,plugin、agent和求实的业务jar包。那几个agent正是大家说的由VirtualMachine运转的代理。而plugin要做的事情就是调用loadAgent方法。对于Android
Studio来讲,plugin就是二个Gradle插件。
完毕gradle插件能够用intellij创立叁个gradle工程并贯彻Plugin< Project
>接口,然后把tools.jar(在jdk的lib目录下)和agent.jar出席到Libraries中。在META-INF/gradle-plugins目录下创办多个properties文件,并在文件中参加生机勃勃行内容“implementation-class=插件类的全约束名“。artifacs配置把源码和META-INF加上,但不可能加tools.jar和agent.jar。(tools.jar
在 jdk中, 但是日常须要团结拷贝到工程目录中的,
agent.jar开垦实现前寄放plugin工程中用来获取jar包路线卡塔尔。

三种各类的主题材料,在这里种复杂的依靠构造下被加大,二个依赖二15个SOA服务的系列,每一种服务99.99%可用。99.99%的三10次方≈99.7%。0.3%表暗暗表示气风发亿次呼吁会有3,000,00次停业,换算成时间大概每月有2个时辰服务不安定。随着服务信任数量的变多,服务不安静的可能率会呈指数性升高,那一个标题最后都会转接为故障表现出来。

2. ClassFileTransformer

agent的兑现相对plugin则复杂超多,首先需求提供agentmain(String args,
Instrumentation
inst卡塔尔(قطر‎方法,并给Instrumentation设置ClassFileTransformer,然后在transformer里更动dexer.Main。当jvm成功施行到大家设置的transformer时,就能够意识传进来的class根本就一向不dexer.Main。坑爹呢这是。。。前边提到了,实施dexer.Main的是dx.bat,也正是说,它和plugin根本不在三个进度里。

二、系统高可用的方法论

3. ProcessBuilder

dx.bat其实是由ProcessBuilder的start方法运维的,ProcessBuilder有二个command成员,保存的是开发银行指标经过引导的参数,只要大家给dx.bat带上-javaagent参数就能够给dx.bat所在进度钦定我们的agent了。于是大家得以在实施start方法前,调用command方法获得command,并往当中插入-javaagent参数。参数的值是agent.jar所在的门路,能够采取agent.jar当中三个class类实例的getProtectionDomain(卡塔尔.getCodeSource(卡塔尔.getLocation(卡塔尔国.toU3 WheelerI(卡塔尔.get帕特h(卡塔尔国获得。不过到了此地大家的次序只怕依旧无法准确退换class。如若大家把更正类的代码单独置于二个类中,然后用ASM生成字节码调用那个类的艺术来对command参数进行改换,就能够开掘抛出了ClassDefNotFoundError错误。这里涉及到了ClassLoader的文化。

如何营造叁个高可用的系统吧?首先要深入分析一下不可用的要素都有何:

4. ClassLoader和InvocationHandler

有关ClassLoader的牵线超级多,这里不再赘述。ProcessBuilder类是由Bootstrap
ClassLoader加载的,而大家自定义的类则是由AppClassLoader加载的。Bootstrap
ClassLoader处于AppClassLoader的上层,我们知道,上层类加载器所加载的类是敬谢不敏间接引用下层类加载器所加载的类的。但假诺下层类加载器加载的类实现或继续了上层类加载器加载的类或接口,上层类加载器加载的类获取到下层类加载的类的实例就可以将其强迫转型为父类,并调用父类的措施。这几个上层类加载器加载的接口,部分APM使用InvocationHandler。还也会有叁个主题材料,ProcessBuilder怎么技艺博拿到InvocationHandler子类的实例呢?有二个相比较神奇的做法,在agent运转的时候,创立InvocationHandler实例,并把它赋值给Logger的treeLock成员。treeLock是二个Object对象,並且只是用来加锁的,没有别的用项。但treeLock是三个final成员,所以记得要改良其修饰,去掉final。Logger同样也是由Bootstrap
ClassLoader加载,那样ProcessBuilder就能够经过反射的主意来获得InvocationHandler实例了。(详见:大旨代码例子卡塔尔国

上层类加载器所加载的类是无法间接引用下层类加载器所加载的类的

层次 加载器
上层 BootStrapClassLoader ProcessBuilder
下层 AppClassLoader ProcessBuilderMethodVisitor操作的自定义类

这一句话的驾驭:
我们的指标是经过ProcessBuilderMethodVisitor将我们的代码(自定义改正类卡塔尔写入ProcessBuilder.class中去让BootStrapClassLoader类加载器进行加载,而此刻,
BootStrapClassLoader是回天乏术援引到大家自定义的类的,因为我们自定义的类是AppClassLoader加载的。

但假若下层类加载器加载的类完成或三番五次了上层类加载器加载的类或接口,上层类加载器加载的类获取到下层类加载的类的实例就能够将其强迫转型为父类,并调用父类的方式。

层次 加载器
上层 BootStrapClassLoader Looger
下层 AppClassLoader InvocationDispatcher

这句话的精通:
这里我们得以看到自定义类InvocationDispatcher是由AppClassLoader加载的,
大家在运行RewriterAgent(AppClassLoader加载卡塔尔国类时,通过反射的措施将InvocationDispatcher对象放入Looger(由于引用了Looger.class,所以那时候logger已经被BootStrapClassLoader加载卡塔尔类的treelock对象中,即下层类加载器加载的类完结了上层类加载器加载的类;当大家由此ProcessBuilderMethodVisitor类管理ProcessBuilder.class文件时,能够由此Logger提取成员变量,插入对应的调用逻辑。当运营到ProcessBuilder时,再经过这段代码动态代理的艺术调用对应的业务。能够将其挟持转型为父类,并调用父类的办法
,请参考http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface,
这里详细介绍了invokeInterface 和 invokeVirtual 的差距。

新匍京娱乐场最全网站 4

5. CallSiteReplace 和 WrapReturn

兑现上大家前段时间任重(rèn zhòng卡塔尔国而道远做那二种, 意气风发种是代码调用替换,
另风华正茂种是代码包裹重返。首假使提前写好相应法规的轮流代码,
生成配置文件表, 在agent中visit每三个class代码,
碰到对应相称调用时将张开代码替换。

高可用系统卓越实行

7. 宗旨代码

ProcessBuilderMethodVisitor
DexClassTransformer#createDexerMainClassAdapter
InvocationDispatcher
BytecodeBuilder

public BytecodeBuilder loadInvocationDispatcher() {
        this.adapter.visitLdcInsn(Type.getType(TransformConstant.INVOCATION_DISPATCHER_CLASS));
        this.adapter.visitLdcInsn(TransformConstant.INVOCATION_DISPATCHER_FILED_NAME);
        this.adapter.invokeVirtual(Type.getType(Class.class), new Method("getDeclaredField", "(Ljava/lang/String;)Ljava/lang/reflect/Field;"));
        this.adapter.dup();
        this.adapter.visitInsn(Opcodes.ICONST_1);
        this.adapter.invokeVirtual(Type.getType(Field.class), new Method("setAccessible", "(Z)V"));
        this.adapter.visitInsn(Opcodes.ACONST_NULL);
        this.adapter.invokeVirtual(Type.getType(Field.class), new Method("get", "(Ljava/lang/Object;)Ljava/lang/Object;"));
        return this;
    }

解析

顺序 指令 描述
8 InvocationDispatcher object invokeVirtual 调用get方法返回具体实例对象
7 null ACONST_NULL null 入栈
6 Field object invokeVirtual 调用setAccessible,改为可访问的,目前栈中只剩一个对象
5 true ICONST_1 1 即为true,入栈
4 Field object dup 拷贝一份,目前栈中只剩两个对象
3 Field object invokeVirtual 调用getDeclaredField 获取treeLock存储的Field
2 treelock ldc treelock 入栈
1 Logger.class Type ldc Logger.class type 入栈

WrapMethodClassVisitor#MethodWrapMethodVisitor

private boolean tryReplaceCallSite(int opcode, String owner, String name, String desc, boolean itf) {
            Collection<ClassMethod> replacementMethods = this.context.getCallSiteReplacements(owner, name, desc);
            if (replacementMethods.isEmpty()) {
                return false;
            }
            ClassMethod method = new ClassMethod(owner, name, desc);
            Iterator<ClassMethod> it = replacementMethods.iterator();
            if (it.hasNext()) {
                ClassMethod replacementMethod = it.next();
                boolean isSuperCallInOverride = (opcode == Opcodes.INVOKESPECIAL) && !owner.equals(this.context.getClassName())
                        && this.name.equals(name) && this.desc.equals(desc);
                //override 方法
                if (isSuperCallInOverride) {
                    this.log.info(MessageFormat.format("[{0}] skipping call site replacement for super call in overriden method : {1}:{2}",
                            this.context.getFriendlyClassName(), this.name, this.desc));
                    return false;
                }

                Method originMethod = new Method(name, desc);
                //处理init方法, 构造对象, 调用替换的静态方法来替换init。
                if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {
                    //调用父类构造方法
                    if (this.context.getSuperClassName() != null && this.context.getSuperClassName().equals(owner)) {
                        this.log.info(MessageFormat.format("[{0}] skipping call site replacement for class extending {1}",
                                this.context.getFriendlyClassName(), this.context.getFriendlySuperClassName()));
                        return false;
                    }
                    this.log.info(MessageFormat.format("[{0}] tracing constructor call to {1} - {2}", this.context.getFriendlyClassName(), method.toString(), owner));
                    //开始处理创建对象的逻辑
                    //保存参数到本地
                    int[] arguments = new int[originMethod.getArgumentTypes().length];
                    for (int i = arguments.length -1 ; i >= 0; i--) {
                        arguments[i] = this.newLocal(originMethod.getArgumentTypes()[i]);
                        this.storeLocal(arguments[i]);
                    }
                    //由于init 之前会有一次dup,及创建一次, dup一次, 此时如果执行了new 和 dup 操作树栈中会有两个对象。
                    this.visitInsn(Opcodes.POP);
                    if (this.newInstructionFound && this.dupInstructionFound) {
                        this.visitInsn(Opcodes.POP);
                    }
                    //载入参数到操作数栈
                    for (int arg : arguments) {
                        this.loadLocal(arg);
                    }
                    //使用要替换的方法,执行静态方法进行对象创建
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                    //如果此时才调用了dup,也需要pop, (这一部分的场景暂时还没有构造出来, 上面的逻辑为通用的)
                    if (this.newInstructionFound && !this.dupInstructionFound) {
                        this.visitInsn(Opcodes.POP);
                    }
                } else if (opcode == Opcodes.INVOKESTATIC) {
                    //替换静态方法
                    this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString()));
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                } else {
                    // 其他方法调用, 使用新方法替换旧方法的调用。 先判断创建的对象是否为null,
                    Method newMethod = new Method(replacementMethod.getMethodName(), replacementMethod.getMethodDesc());
                    this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString()));
                    //从操作数栈上取原始参数类型到本地变量中
                    int[] originArgs = new int[originMethod.getArgumentTypes().length];
                    for (int i = originArgs.length -1 ; i >= 0; i--) {
                        originArgs[i] = this.newLocal(originMethod.getArgumentTypes()[i]);
                        this.storeLocal(originArgs[i]);
                    }
                    //操作数栈中只剩操作对象了, 需要dup, 拷贝一份作为检查新method的第一个参数。
                    this.dup();
                    //检查操作数栈顶对象类型是否和新method的第一个参数一致。
                    this.instanceOf(newMethod.getArgumentTypes()[0]);

                    Label isInstanceOfLabel = new Label();
                    //instanceof 结果不等于0 则跳转到 isInstanceofLabel,执行替换调用
                    this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel);
                    //否则执行原始调用
                    for (int arg : originArgs) {
                        this.loadLocal(arg);
                    }
                    super.visitMethodInsn(opcode, owner, name, desc, itf);

                    Label endLabel  = new Label();
                    //跳转到结束label
                    this.visitJumpInsn(Opcodes.GOTO, endLabel);

                    this.visitLabel(isInstanceOfLabel);
                    //处理替换的逻辑
                    //load 参数, 第一个为 obj, 后面的为原始参数
                    this.checkCast(newMethod.getArgumentTypes()[0]);
                    for (int arg: originArgs) {
                        this.loadLocal(arg);
                    }
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                    //结束
                    this.visitLabel(endLabel);
                }
                this.context.markModified();
                return true;
            }
            return false;
        }

解析

详细见tryReplaceCallSite评释就能够。

谈论上的话,当图中保有的作业都做完,我们就能够感到系统是一个确实的高可用系统。但正是如此啊?

8. 验证

将扭转的apk反编写翻译,查看class
字节码。大家日常会由此JD-GUI来查阅。我们来查阅一下sample生成的结果:

private void testOkhttpCall()
  {
    OkHttpClient localOkHttpClient = new OkHttpClient.Builder().build();
    Object localObject = new Request.Builder().url("https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey");
    if (!(localObject instanceof Request.Builder))
    {
      localObject = ((Request.Builder)localObject).build();
      if ((localOkHttpClient instanceof OkHttpClient)) {
        break label75;
      }
    }
    label75:
    for (localObject = localOkHttpClient.newCall((Request)localObject);; localObject = OkHttp3Instrumentation.newCall((OkHttpClient)localOkHttpClient, (Request)localObject))
    {
      ((Call)localObject).enqueue(new Callback()
      {
        public void onFailure(Call paramAnonymousCall, IOException paramAnonymousIOException)
        {

        }

        public void onResponse(Call paramAnonymousCall, Response paramAnonymousResponse)
          throws IOException
        {

        }
      });
      return;
      localObject = OkHttp3Instrumentation.build((Request.Builder)localObject);
      break;
    }
  }

上边的代码揣测未有几人能够看懂,
非常for循环里面包车型大巴逻辑。其实是由于分化的反编写翻译工具以致的剖判难题产生的,所以看起来逻辑混乱,不可能符合预期。

想用查看真实的结果, 大家来看下反编写翻译后的smail。

详细smail指令参考

.method private testOkhttpCall()V
    .locals 6
    .prologue
    .line 35
    const-string v3, "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey"
    .line 36
    .local v3, "url":Ljava/lang/String;
    new-instance v4, Lokhttp3/OkHttpClient$Builder;
    invoke-direct {v4}, Lokhttp3/OkHttpClient$Builder;-><init>()V
    invoke-virtual {v4}, Lokhttp3/OkHttpClient$Builder;->build()Lokhttp3/OkHttpClient;
    move-result-object v1
//new OkHttpClient.Builder().build(); 即为okhttpclient,放到 v1 中
    .line 37
    .local v1, "okHttpClient":Lokhttp3/OkHttpClient;
    new-instance v4, Lokhttp3/Request$Builder;
    invoke-direct {v4}, Lokhttp3/Request$Builder;-><init>()V
    invoke-virtual {v4, v3}, Lokhttp3/Request$Builder;->url(Ljava/lang/String;)Lokhttp3/Request$Builder;
    move-result-object v4
    //new Request.Builder().url(url)执行了这一段语句,将结果放到了v4中。
    instance-of v5, v4, Lokhttp3/Request$Builder;
    if-nez v5, :cond_0
    invoke-virtual {v4}, Lokhttp3/Request$Builder;->build()Lokhttp3/Request;
    move-result-object v2
    .line 38
    .local v2, "request":Lokhttp3/Request;
    //判断v4中存储的是否为Request.Builder类型,如果是则跳转到cond_0, 否则执行Request.Builder.build()方法,将结果放到v2中.
    :goto_0
    instance-of v4, v1, Lokhttp3/OkHttpClient;
    if-nez v4, :cond_1
    invoke-virtual {v1, v2}, Lokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;
    move-result-object v0
    .line 39
    .end local v1    # "okHttpClient":Lokhttp3/OkHttpClient;
    .local v0, "call":Lokhttp3/Call;
    //goto_0 标签:判断v1 中的值是否为 OKHttpclient 类型, 如果是跳转为cond_1 , 否则调用OKHttpclient.newCall, 并将结果放到v0 中。
    :goto_1
    new-instance v4, Lcom/paic/apm/sample/MainActivity$1;
    invoke-direct {v4, p0}, Lcom/paic/apm/sample/MainActivity$1;-><init>(Lcom/paic/apm/sample/MainActivity;)V
    invoke-interface {v0, v4}, Lokhttp3/Call;->enqueue(Lokhttp3/Callback;)V
    .line 51
    return-void
    //goto_1 标签: 执行 v0.enqueue(new Callback());并return;
    .line 37
    .end local v0    # "call":Lokhttp3/Call;
    .end local v2    # "request":Lokhttp3/Request;
    .restart local v1    # "okHttpClient":Lokhttp3/OkHttpClient;
    :cond_0
    check-cast v4, Lokhttp3/Request$Builder;
    invoke-static {v4}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->build(Lokhttp3/Request$Builder;)Lokhttp3/Request;
    move-result-object v2
    goto :goto_0
    //cond_0:标签: 执行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4), 并将结果放到v2中,并goto 到 goto_0
    .line 38
    .restart local v2    # "request":Lokhttp3/Request;
    :cond_1
    check-cast v1, Lokhttp3/OkHttpClient;
    .end local v1    # "okHttpClient":Lokhttp3/OkHttpClient;
    invoke-static {v1, v2}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->newCall(Lokhttp3/OkHttpClient;Lokhttp3/Request;)Lokhttp3/Call;
    move-result-object v0
    goto :goto_1
    //cond_1 标签: 执行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2), 并将结果放到v0中, goto 到goto_1 
.end method

拆解深入分析后的伪代码

String v3 = "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey";
object v1 = new OkhttpClient.Builder().build();
object v4 = new Reqeust.Builder().url(v3);
object v2 ;
object v0 ;

if (v4 instanceof Request.Builder) {
    cond_0:
    v2 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4); 
} else {
    v2 = (Request.Builder)v4.build();
}

goto_0:
if (v1 instanceof OkHttpClient) {
    cond_1:
    v0 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2);
} else {
    v0 = v1.newCall(v2); // v0 is Call
}

goto_1:
v4 = new Callback();
v0.enqueue(v4);
return;

查看伪代码, 切合预期结果。验证完成。

那么故障演习平台就欢乐上台了。当上述的高可用实施都做完,利用故障练习平台做叁遍真正的故障演练,在系统运营期动态地注入一些故障,进而来验证下系统是或不是根据故障预案去执行相应的降级或然熔断计谋。

三、故障演习平台

故障演习平台:侦察故障预案是还是不是确实的起功效的平台。

故障类型:重在包含运维期非常、超时等等。通过对系统有些服务动态地流入运营期极度来到达模拟故障的指标,系统依据预案实行相应的政策验证系统是不是是真正的高可用。

1、故障演习平台的总体构造

故障练习平台构造首要分为四片段:

新匍京娱乐场最全网站 5

  • 前台体现系统(WEB):呈现系统之间的拓扑关系以致各样AppCode对应的集群和办法,能够选择具体的秘籍举行故障的流入和消逝;
  • 发布连串(Deploy):其黄金时代类别主要用来将故障演习平台的Agent和Binder包公布到指标APP的机械上还要运维试行。前台体现系统会传递给发表平台要开展故障注入的AppCode以至指标应用程式的IP地址,通过那三个参数发表系统可以找到呼应的机械实行Jar包的下载和开发银行;
  • 服务和指令分发系统(Server):其后生可畏种类主要是用以命令的散发、注入故障的动静记录、故障注入和打消操作的逻辑、权限校验以致相关的Agent的回来音信选拔效果。前台页面已经接入QSSO会对当前人能够操作的IP列表做故障注入,防御风险。后端命令分发的模块会和结构在指标应用程式上的Agent实行通讯,将下令推送到Agent上进行字节码编织,Agent实施命令后回去的内容通过Server和Agent的长连接传回Server端;
  • Agent和Binder程序:Agent担任对指标APP做代办何况做字节码巩固,具体代理的点子能够因此传输的下令来调节,代理方法后对章程做动态的字节码巩固,这种字节码巩固全体无侵入、实时生效、动态可插拔的特性。Binder程序主固然因而公布种类传递过来的AppCode和开发银行端口(ServerPort)找到对象应用软件的JVM进程,之后施行动态绑定,达成运转期代码巩固的成效。

2、 Agent全部构造

一时AOP的实现成二种方法:

  • 静态编织:静态编织发生在字节码生成时依照早晚框架的规行矩步提前将AOP字节码插入到目的类和章程中;
  • 动态编织:在JVM运营期对内定的章程成功AOP字节码巩固。麻木不仁的措施大多数采取重命名原有艺术,再新建贰个同名方法做代办的劳作情势来形成。

静态编织的标题是就算想纠正字节码必得重启,那给支付和测验进度引致了相当的大的不方便。动态的章程纵然能够在运转期注入字节码落成动态增进,但尚无统后生可畏的API超级轻便操作不当。基于此,大家运用动态编织的不二等秘书诀、标准的API来标准字节码的转移——Agent组件。

Agent组件:经过JDK所提供的Instrumentation-API达成了运用HotSwap本领在不重启JVM的境况下促成对专擅方法的增加,无论大家是做故障练习、调用链追踪(QTrace)、流量录像平台(Ares)以至动态扩张日志输出BTrace,都急需二个存有无侵入、实时生效、动态可插拔的字节码加强组件。

Agent的事件模型

如图所示,事件模型紧要可分为三类事件:

新匍京娱乐场最全网站 6

BEFORE在格局推行前事件、THROWS抛出特别事件、RETUTucsonN重临事件。那三类事件能够在章程施行前、再次回到和抛出万分这两种意况做字节码编织。

如下代码:

// BEFORE

try {

/*

* do something…

*/

foo();

// RETURN

return;

} catch (Throwable e) {

// THROWS

}

相关文章