来自 互联网科技 2019-12-22 16:14 的文章
当前位置: 永利皇宫登录 > 互联网科技 > 正文

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

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

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

作者介绍

APM : 应用程序质量管理。 二〇一一年时国外的APM行当 NewRelic 和 应用软件Dynamics 已经在该领域拔得头筹,我国近几年来也忍俊不禁部分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. 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卡塔尔(英语:State of Qatar),那一个名字也截然展现了其最本色的功力:正是专程为Java语言编写的插桩服务提供支持的。

二种加载agent的法门:

  • 在运转时加载, 运转JVM时钦点agent类。这种措施,Instrumentation的实例通过agent class的premain方法被传到。
  • 在运作时加载,JVM提供风度翩翩种当JVM运转完毕后开启agent机制。这种场地下,Instrumention实例通过agent代码中的的agentmain传入。

参照例子instrumentation 成效介绍(javaagent卡塔尔

有了javaagent, 大家就能够在编写翻译app时再一次修正dex 的Main类,对应改正processClass方法。

怎么校正class文件? 我们需求明白java字节码,然后要求明白ASM开垦。通过ASM编程来匡正字节码,进而校勘class文件。(也得以行使javaassist来扩充改变)

在介绍字节代码指令在此以前,有必不可少先来介绍 Java 虚构机实施模型。大家领悟,Java 代码是 在线程内部进行的。各种线程都有和好的实施栈,栈由帧组成。各种帧表示叁个艺术调用:每便调用贰个情势时,会将三个新帧压入当前线程的实践栈。当方法重临时,也许是正规重返,可能是因为十一分重回,会将这几个帧从奉行栈中弹出,实行进程在发生调用的点子中继续开展(这么些方 法的帧今后坐落于栈的最上端卡塔尔国。

每生龙活虎帧包罗两有的:一个片段变量部分和一个操作数栈部分。局地变量部分含有可借助索引 以随机顺序访问的变量。由名字能够看见,操作数栈部分是一个栈,个中带有了供字节代码指令 用作操作数的值。

字节代码指令 字节代码指令由一个标志该指令的操作码和稳固数指标参数组成:

  • 操作码是四个无符号字节值——即字节代码名
  • 参数是静态值,显明了标准的吩咐行为。它们紧跟在操作码之后给出.举个例子GOTO标志指令(其操作码的值为 167卡塔尔(英语:State of Qatar)以一个指明下一条待推行命令的标志作为参数标志。不要 将指令参数与指令操作数相混淆:参数值是静态已知的,存款和储蓄在编写翻译后的代码中,而 操作数值来自操作数栈,唯有到运维时技术掌握。

参考:

常见指令:

  • 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的第4个参数是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包路线卡塔尔。

agent的兑现相对plugin则复杂超多,首先须要提供agentmain(String args, Instrumentation inst卡塔尔国方法,并给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(卡塔尔(英语:State of Qatar).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加载卡塔尔(英语:State of Qatar)类时,通过反射的主意将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;

翻看伪代码, 适合预期结果。验证完成。

系统里面的注重性极其复杂、调用链路很深、服务时期未有分支。在这里种复杂的注重下,系统爆发了几起故障:

  • 弱信赖挂掉,主流程挂掉,改良报废凭据的花费意况,下单主流程失败;
  • 大旨服务调用量陡增,某服务超时引起相关联的有着服务“雪崩”;
  • 机房网络恐怕某个机器挂掉,不可能提供基本服务。

多少个故障原因:

  • 系统强弱正视混乱、弱正视无降级;
  • 系统流量猛增,系统容积不足,未有限流熔断机制;
  • 硬件财富网络现身难点影响系统运作,未有高可用的互联网布局。

一应俱全的难题,在此种复杂的依赖构造下被放大,一个依据26个SOA服务的系统,各种服务99.99%可用。99.99%的30回方≈99.7%。0.3%意味生机勃勃亿次倡议会有3,000,00次战败,换算成时间大致每月有2个钟头服务动荡。随着服务注重数量的变多,服务不安静的可能率会呈指数性进步,那么些题目最后都会转化为故障表现出来。

二、系统高可用的方法论

怎么样营造二个高可用的连串啊?首先要分析一下不可用的成分皆有怎么着:

图片 4

高可用系统独立施行

反驳上的话,当图中所有的事务都做完,大家就足以以为系统是叁个当真的高可用系统。但便是如此啊?

那就是说故障练习平台就热闹进场了。当上述的高可用实行都做完,利用故障演练平台做壹遍真正的故障练习,在系统运维期动态地注入一些故障,进而来注明下系统是还是不是坚决守住故障预案去奉行相应的降级只怕熔断计谋。

三、故障演习平台

故障练习平台:查实故障预案是不是真的的起功能的阳台。

故障类型:首要不外乎运转期非凡、超时等等。通过对系统某个服务动态地流入运维期万分来到达模拟故障的目标,系统根据预案实施相应的国策验证系统是不是是真正的高可用。

1、故障演练平台的完全布局

故障演习平台构造首要分为四局部:

图片 5

  • 前台显示系统(WEB):显示系统之间的拓扑关系以至各样AppCode对应的集群和艺术,能够接收具体的方法实行故障的注入和清除;
  • 宣布系统(Deploy):那些系统关键用来将故障演练平台的Agent和Binder阎罗包老布到对象APP的机器上同临时候运行实践。前台浮现系统会传送给发表平台要开展故障注入的AppCode以至目的应用程式的IP地址,通过那三个参数发表种类能够找到相应的机器进行Jar包的下载和开发银行;
  • 服务和下令分发系统(Server):那个连串重要是用以命令的分发、注入故障的情况记录、故障注入和灭亡操作的逻辑、权限校验以至有关的Agent的回来音讯选用效果。前台页面已经接入QSSO会对当前人能够操作的IP列表做故障注入,防卫风险。后端命令分发的模块会和配备在对象APP上的Agent进行通讯,将指令推送到Agent上施行字节码编织,Agent实施命令后重临的内容通过Server和Agent的长连接传回Server端;
  • Agent和Binder程序:Agent担当对指标APP做代办何况做字节码加强,具体代理的格局能够因此传输的吩咐来支配,代理方法后对章程做动态的字节码加强,这种字节码巩固全体无侵入、实时生效、动态可插拔的特点。Binder程序首假如因而公布种类传递过来的AppCode和开发银行端口(ServerPort)找到对象APP的JVM进度,之后实行动态绑定,完结运转期代码加强的作用。

2、 Agent全体布局

日前AOP的实现存三种办法:

  • 静态编织:静态编织爆发在字节码生成时依据早晚框架的法则提前将AOP字节码插入到目的类和办法中;
  • 动态编织:在JVM运维期对内定的办法成功AOP字节码巩固。不问不闻的点子大相当多选择重命名原有办法,再新建一个同名方法做代办的办事方式来完结。

静态编织的主题素材是如若想改进字节码必需重启,那给支付和测量试验进度引致了超大的劳碌。动态的办法即使能够在运转期注入字节码完毕动态拉长,但从未统豆蔻梢头的API超级轻易操作不当。基于此,我们利用动态编织的诀要、规范的API来标准字节码的变化——Agent组件。

Agent组件:经过JDK所提供的Instrumentation-API实现了接纳HotSwap技巧在不重启JVM的气象下促成对专擅方法的增长,不论大家是做故障演习、调用链追踪(QTrace)、流量摄像平台(Ares)以至动态扩张日志输出BTrace,都急需二个具备无侵入、实时生效、动态可插拔的字节码加强组件。

Agent的风浪模型

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

图片 6

BEFORE在章程实践前事件、THROWS抛出特别事件、RETU大切诺基N再次来到事件。那三类事件能够在艺术施行前、重返和抛出非常那三种状态做字节码编织。

平时来讲代码:

// BEFORE

try {

/*

* do something...

*/

foo();

// RETURN

return;

} catch (Throwable e) {

// THROWS

}

事件模型可以变成八个职能:

  • 在方法体实行在此之前一贯回到自定义结果对象,原有办法代码将不会被实践;
  • 在方法体重回从前再次协会新的结果对象,以致足以转移为抛出十三分;
  • 在方法体抛出极度之后重新抛出新的不胜,甚至足以转移为符合规律重临。

Agent如何防范“类污染”

在支付Agent的时候,第三个应用是故障练习平台,那么那个时候其实我们并无需Agent推行的进度中有自定义结果对象的归来,所以率先个版本的Agent接纳硬编码的章程开展动态织入:

图片 7

故障类加载模型

先是介绍下多少个类加载器:

  • BootstrapClassLoader教导类加载器加载的是JVM本身需求的类,那个类加载使用C++语言落成的,是虚拟机自身的一片段;
  • ExtClassLoader它担任加载<JAVA_HOME>/lib/ext目录下或许由系统变量-Djava.ext.dir钦命位路线中的类库;
  • AppClassLoader它担任加载系统类路线java-classpath或-D java.class.path内定路径下的类库,也正是大家日常采纳的classpath路线;
  • CommonClassLoader以致上边的都是Tomcat定义的ClassLoader。

Agent和连锁的lib会放到AppClassLoader这风流浪漫层去加载,利用Javasist做字节码的织入,所以Javasist的加载器就是AppClassLoader。

可是想纠正的是汤姆cat WebClassLoader所加载的com.xxx.InvocationHandler那一个类的Invoke方法,不相同的ClassLoader之间的类是无法相互拜见的,做字节码的退换并无需那些类的实例,也不供给回到结果,所以能够经过Instrument API得到这一个类加载器,并且能够依据类名称获取到这些类的字节码实行字节码调换。故障类Drill.class和变形后的com.xxx.InvocationHandler.class重新load到JVM中,完毕了插桩操作。

以Dubbo为例表明下怎么样注入故障和消逝故障:

图片 8

Dubbo调用的流入进度

  • 服务A调用服务B在Client端的Proxy层做AOP;
  • 运营Agent何况生成多个Drill类invoke方法,抛出一个运营期相当;
  • 字节码变形:在代码第大器晚成行早前扩张Drill.invoke(卡塔尔(قطر‎;
  • 如若想改造相当类型,改动Drill类就能够,换来Sleep 3s ClassRedifine从今以往会重新load到JVM达成故障类型的转向也许免除。

碰着的难题

上面包车型地铁方法平时很康健的解决了难点,不过随着平台的行使专门的学问线要对广大接口和格局同不经常间拓宽故障练习,那么我们转换的Drill类里面就能够有各样:

if method==业务线定义方法

do xxx

还要相当的轻易拼接出错而且难以调节和测量试验,只好把变化的类输出为文件,查看自身写的字节码编写翻译成class文件是还是不是正确,差不离太痛心了!

怎么消除?

新的布局要求减轻多个难题:

  • 类隔开分离的主题素材:不要污染原生应用软件;
  • 事件的兑现是可编写翻译的;
  • 支撑回到自定义的结果。

下生机勃勃版本的Agent实现就产生了,把装有Agent的类和完毕的功能抽象出来,放到一个自定义的AgentClassLoader里面,字节码注入到对象APP后能够透过反射的法子来调用具体的风云完成。

图片 9

类加载模型

  • 在BootstrapClassLoader里面注入Drill类作为通讯类;
  • Agent会接纳命令,遵照事件类型对InvocationHandler做字节码变形,注入到对象APP;
  • 在对象应用软件调用的时候,调用Drill.invoke(targetJavaClass,targetJavaMethod, targetThis, args)传递过来多少个参数(指标类、方法、实例、本身参数等);
  • Drill类通过反射的不二等秘书技调用AppClassLoader里面包车型地铁切实事件完结,比方BEFORE事件的实践代码,来完毕注入后的逻辑实施。

Agent的总体结构

Agent的风华正茂体化布局如图所示:

图片 10

  • 支撑区别的模块的参与,比方Mock、流量摄像、故障演习等;
  • 帮助QSSO的权能验证;
  • 支撑测验和虚伪景况的无开销接入;
  • 支撑活动铺排没有要求人工参预;
  • 支撑各样故障命令的发布和实行、 超时 、万分甚至数额的归来;
  • 支撑方式级其他编写制定以致代码实行流程的编写制定;
  • 支撑在任意的Web容器实行Agent代理。

四、怎么样运用

利用的补益是很明朗的:

  • 零开销接入,没有必要申请别的财富;
  • 故障注入驱除,不供给重启服务;
  • 能够提供全体集群的拓扑布局。

然则怎样技巧科学使用啊?如下图所示:

图片 11

接收格局

步骤一、输入AppCode;

手续二、采取故障方法;

步骤三、钦定机器;

步骤四、注入故障。

五、总结

故障演习平台最宗旨的就是Agent组件——字节码编织框架,那一个框架是纯Java的基于Instrumentation-API的AOP施工方案。它能够方便研究开发人士对于字节码插桩拆桩操作,能够十分轻便的兑现故障演习、流量摄像甚至其它的行使模块。

作者:王鹏

来源:Qunar技巧沙龙订阅号(ID:QunarTL)

dbaplus社会群众体育款待广大技艺职员投稿,投稿邮箱:editor@dbaplus.cn归来和讯,查看越多

主要编辑:

本文由永利皇宫登录发布于互联网科技,转载请注明出处:Android性能监控实现原理,去哪儿系统高可用之法

关键词: