原题目:去哪儿系统高可用之法:搭建故障练习平台

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

笔者介绍

APM : 应用程序质量管理。 贰零壹壹年时外国的APM行业 NewRelic 和 APPDynamics
已经在该领域拔得头筹,本国近几年来也鬼使神差那么一点APM厂家,如: 听云,
OneAPM, 博睿 云智慧,Ali百川码力。
(据解析,本国android端方案都以抄袭NewRelic公司的,由于该商厦的sdk未混淆,产业界良心卡塔尔(قطر‎

王鹏,二〇一七年加盟去哪儿机票职业部,首要从事后端研发专门的学问,前段时间在机票工作部负担路程单和故障演习平台以致公共服务ES、数据同步中间件等连锁的研究开发专门的职业。

能做哪些:
crash监察和控制,卡顿监察和控制,内部存款和储蓄器监控,增加trace,网络品质监察和控制,app页面自动埋点,等。

去何方网二零零五年树立到现在,随着系统规模的日渐扩张,已经有无数个利用种类,这么些系统之间的耦合度和链路的复杂度不断坚实,对于大家构建分布式高可用的种类构造具有宏大挑战。大家须求三个阳台在运转期自动注入故障,查证故障预案是不是起效——故障演练平台。

天性监察和控制其实便是hook
代码到项目代码中,进而产生各样监督。常规手腕都是在档期的顺序中加进代码,但什么做到非侵入式的,即一个sdk就可以。

一、背景

1. 如何hook

断面编制程序-- AOP。大家的方案是AOP的生机勃勃种,通过改正app
class字节码的款式将大家项目标class文件实行修改,从而产生放权我们的督察代码。

图片 1androidbuilder.jpg

经过查看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节。

这是某职业部的类别拓扑图:

2. hook 到哪里

生机勃勃期首就算网络品质监察和控制。如何能收获到互联网数据经过调查探讨开采脚下有上边集中方案:

  • root手提式有线电话机,通过adb 命令实行收缴。
  • 成立vpn,将兼具网络哀告实行收缴。
  • 参照听云,newrelic等产物,针对一定库开展代理截获。

莫不还应该有其余的法门,须要延续应用商量。

当下大家参谋newrelic等营业所出品,针对一定互联网央求库开展代理的的方法实行网络数据截获。举例okhtt3,
httpclient, 等网络库。

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卡塔尔(英语:State of Qatar)

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

如何改善class文件?
大家需求明白java字节码,然后须要驾驭ASM开辟。通过ASM编程来改进字节码,从而校订class文件。(也足以行使javaassist来展开校勘)

在介绍字节代码指令早前,有不能够贫乏先来介绍 Java 设想机试行模型。大家了然,Java
代码是
在线程内部实行的。每种线程都有投机的施行栈,栈由帧组成。各样帧表示二个艺术调用:每次调用一个方式时,会将一个新帧压入当前线程的实践栈。当方法再次来到时,或然是正规重回,或然是因为十二分重返,会将那么些帧从执行栈中弹出,实施进程在发生调用的不二秘籍中持续开展(这几个方
法的帧以往坐落于栈的最上端卡塔尔(英语:State of Qatar)。

每朝气蓬勃帧满含两部分:二个有的变量部分和叁个操作数栈部分。局地变量部分含有可依据索引
以自由顺序访谈的变量。由名字能够观望,操作数栈部分是一个栈,个中累积了供字节代码指令
用作操作数的值。

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

  • 操作码是三个无符号字节值——即字节代码名
  • 参数是静态值,分明了确切的指令行为。它们紧跟在操作码之后给出.比方GOTO标志指令(其操作码的值为
    167卡塔尔国以一个指明下一条待试行命令的符号作为参数标识。不要
    将指令参数与指令操作数相混淆:参数值是静态已知的,存款和储蓄在编写翻译后的代码中,而
    操作数值来自操作数栈,只有到运维时本领分晓。

参考:

广泛指令:

  • const 将怎么着数据类型压入操作数栈。
  • push 表示将单字节或短整型的常量压入操作数栈。
  • ldc 代表将怎么样板种的多寡从常量池中压入操作数栈。
  • load 将某项指标一些变量数据压入操作数栈顶。
  • store 将操作数栈顶的多少存入钦命的有的变量中。
  • pop 从操作数栈顶弹出多少
  • dup 复制栈顶的数码并将复制的值也压入栈顶。
  • swap 调换栈顶的多寡
  • invokeVirtual 调用实例方法
  • invokeSepcial 调用超类布局方法,实例初叶化,私有方法等。
  • invokeStatic 调用静态方法
  • invokeInterface 调用接口
  • getStatic
  • getField
  • putStatic
  • putField
  • New

查看demo: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

是因为程序解析、生成和转移本领的用项众多,所以大家针对广大语言完结了成都百货上千用来深入分析、
生成和转移程序的工具,这个语言中就归纳 Java 在内。ASM 正是为 Java
语言设计的工具之生机勃勃, 用于举办运转时类生成与转移。于是,大家设计了
ASM1库,用于拍卖经过编写翻译 的 Java 类。

ASM 并非惟风度翩翩可生成和转移已编写翻译 Java
类的工具,但它是洋气、最高效的工具之朝气蓬勃,可 从
下载。其主要优点如下:

  • 有三个简短的模块API,设计宏观、使用方便。
  • 文书档案齐全,具有三个有关的Eclipse插件。
  • 扶植新型的 Java 版本——Java 7。
  • 小而快、极其可信赖。
  • 全数宏大的客商社区,可感觉新顾客ﰁ供协理。
  • 源许可开放,大致允许私自使用。

图片 2ASM_transfer.png

核心类: 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; mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn; mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitVarInsn; 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; mv.visitMethodInsn(INVOKESPECIAL, "com/paic/agent/test/TestMain", "<init>", "()V", false); mv.visitMethodInsn(INVOKEVIRTUAL, "com/paic/agent/test/TestMain", "sayHello", "()V", false); mv.visitInsn; 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; mv.visitEnd();}

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包路线卡塔尔(英语:State of Qatar)。

agent的落到实处相对plugin则复杂比较多,首先须要提供agentmain(String args,
Instrumentation
inst卡塔尔(英语:State of Qatar)方法,并给Instrumentation设置ClassFileTransformer,然后在transformer里改动dexer.Main。当jvm成功实施到大家设置的transformer时,就能发觉传进来的class根本就从未dexer.Main。坑爹呢那是。。。后边提到了,施行dexer.Main的是dx.bat,也正是说,它和plugin根本不在二个进程里。

dx.bat其实是由ProcessBuilder的start方法运维的,ProcessBuilder有一个command成员,保存的是运转指标经过引导的参数,只要大家给dx.bat带上-javaagent参数就能够给dx.bat所在经过钦定大家的agent了。于是我们得以在推行start方法前,调用command方法得到command,并往此中插入-javaagent参数。参数的值是agent.jar所在的路子,能够动用agent.jar在那之中多少个class类实例的getProtectionDomain(卡塔尔.getCodeSource(卡塔尔(قطر‎.getLocation.getPath(卡塔尔(قطر‎获得。可是到了这里大家的次序大概依旧不能正确改动class。假如大家把改变类的代码单独置于三个类中,然后用ASM生成字节码调用这几个类的措施来对command参数进行改革,就能够发掘抛出了ClassDefNotFoundError错误。这里提到到了ClassLoader的学问。

至于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时,再通过这段代码动态代理的格局调用对应的业务。能够将其压迫转型为父类,并调用父类的点子
,请参考
这里详细介绍了invokeInterface 和 invokeVirtual 的分别。

福寿绵绵上大家脚下根本做那三种, 豆蔻梢头种是代码调用替换,
另风华正茂种是代码包裹重回。首要是提前写好相应准则的替换代码,
生成配置文件表, 在agent中visit每个class代码,
蒙受对应相配调用时将拓宽代码替换。

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", "; 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 && this.desc.equals; //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 { 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); //开始处理创建对象的逻辑 //保存参数到本地 int[] arguments = new int[originMethod.getArgumentTypes().length]; for (int i = arguments.length -1 ; i >= 0; i--) { arguments[i] = this.newLocal(originMethod.getArgumentTypes; 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; } //使用要替换的方法,执行静态方法进行对象创建 super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc; //如果此时才调用了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; } 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; this.storeLocal(originArgs[i]); } //操作数栈中只剩操作对象了, 需要dup, 拷贝一份作为检查新method的第一个参数。 this.dup(); //检查操作数栈顶对象类型是否和新method的第一个参数一致。 this.instanceOf(newMethod.getArgumentTypes; Label isInstanceOfLabel = new Label(); //instanceof 结果不等于0 则跳转到 isInstanceofLabel,执行替换调用 this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel); //否则执行原始调用 for (int arg : originArgs) { this.loadLocal; } 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; for (int arg: originArgs) { this.loadLocal; } super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc; //结束 this.visitLabel; } this.context.markModified(); return true; } return false; }

解析 详细见tryReplaceCallSite批注就能够。

图片 3

8. 验证

将转换的apk反编写翻译,查看class
字节码。大家平日会透过JD-GUI来查阅。大家来查看一下sample生成的结果:

private void testOkhttpCall() { OkHttpClient localOkHttpClient = new OkHttpClient.Builder; 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.newCalllocalObject);; localObject = OkHttp3Instrumentation.newCall((OkHttpClient)localOkHttpClient, localObject)) { 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; 即为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执行了这一段语句,将结果放到了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, 并将结果放到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, 并将结果放到v0中, goto 到goto_1 .end method

深入分析后的伪代码

String v3 = "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey";object v1 = new OkhttpClient.Builder;object v4 = new Reqeust.Builder;object v2 ;object v0 ;if (v4 instanceof Request.Builder) { cond_0: v2 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build; } else { v2 = (Request.Builder)v4.build();}goto_0:if (v1 instanceof OkHttpClient) { cond_1: v0 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall;} else { v0 = v1.newCall; // v0 is Call}goto_1:v4 = new Callback();v0.enqueue;return;

翻看伪代码, 切合预期结果。验证实现。

系统里面的依附特别复杂、调用链路很深、服务时期未有分支。在这里种复杂的信任性下,系统发生了几起故障:

  • 弱注重挂掉,主流程挂掉,纠正报废凭据的付出景况,下单主流程退步;
  • 大旨服务调用量陡增,某服务超时引起相关联的具有服务“雪崩”;
  • 机房互连网也许有个别机器挂掉,无法提供基本服务。

多少个故障原因:

  • 系统强弱重视混乱、弱信赖无降级;
  • 系统流量大幅度增涨,系统体积不足,未有限流熔断机制;
  • 硬件财富互联网现身难点影响系统运行,未有高可用的互连网构造。

五花八门的标题,在此种复杂的依赖布局下被推广,一个正视二十多个SOA服务的系统,每一个服务99.99%可用。99.99%的33遍方≈99.7%。0.3%代表意气风发亿次呼吁会有3,000,00次败北,换算成时间大意每月有2个钟头服务不安静。随着服务注重数量的变多,服务不安宁的概率会呈指数性升高,这几个难点最终都会转接为故障表现出来。

二、系统高可用的方法论

哪些营造三个高可用的种类啊?首先要解析一下不可用的成分都有怎样:

图片 4

高可用系统独立施行

反对上的话,当图中颇有的事情都做完,我们就足以认为系统是二个真正的高可用系统。但正是如此呢?

那么故障练习平台就喜庆上场了。当上述的高可用推行都做完,利用故障练习平台做一次真正的故障演习,在系统运维期动态地注入一些故障,从而来验证下系统是不是信守故障预案去实行相应的降级或许熔断战术。

三、故障练习平台

故障练习平台:侦察故障预案是还是不是真的的起成效的平台。

故障类型:关键不外乎运维期卓殊、超时等等。通过对系统某个服务动态地流入运行期相当来达到模拟故障的目标,系统遵照预案实施相应的政策验证系统是还是不是是真正的高可用。

1、故障练习平台的完好布局

故障演习平台布局首要分为四片段:

图片 5

  • 前台体现系统(WEB):展现系统里面包车型大巴拓扑关系以致各种AppCode对应的集群和措施,能够挑选具体的方式举行故障的流入和消弭;
  • 发布种类(Deploy):其风流罗曼蒂克系统首要性用于将故障练习平台的Agent和Binder包发表到指标应用软件的机械上还要运转实践。前台体现系统会传递给发布平台要进行故障注入的AppCode以致目的APP的IP地址,通过那八个参数发表种类能够找到相应的机械实行Jar包的下载和开发银行;
  • 劳动和下令分发系统(Server):以此系统首若是用来命令的分发、注入故障的场合记录、故障注入和解除操作的逻辑、权限校验以至相关的Agent的回来消息采取效果。前台页面已经接入QSSO会对当前人可以操作的IP列表做故障注入,防卫风险。后端命令分发的模块会和配置在对象应用软件上的Agent进行通讯,将下令推送到Agent上实行字节码编织,Agent实行命令后回到的内容通过Server和Agent的长连接传回Server端;
  • Agent和Binder程序:Agent担当对目的应用程式做代办何况做字节码加强,具体代理的措施能够通过传输的吩咐来决定,代理方法后对艺术做动态的字节码加强,这种字节码巩固全部无侵入、实时生效、动态可插拔的特点。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抛出拾叁分事件、RETUMuranoN重返事件。那三类事件能够在措施实行前、再次回到和抛出非凡那二种状态做字节码编织。

如下代码:

// BEFORE

try {

/*

* do something…

*/

foo();

// RETURN

return;

} catch (Throwable e) {

// THROWS

}

发表评论

电子邮件地址不会被公开。 必填项已用*标注