网络性能监控澳门在线威尼斯网址,通常是jvm要加载一个文件的字节码到内存时

澳门在线威尼斯网址 8

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

澳门在线威尼斯网址 1

Classloader负责将Class加载到JVM中,并且确定由那个ClassLoader来加载(父优先的等级加载机制)。还有一个任务就是将Class字节码重新解释为JVM统一要求的格式

在面向对象编程实践中,我们通过众多的类来组织一个复杂的系统,这些类之间相互关联、调用使他们的关系形成了一个复杂紧密的网络。当系统启动时,出于性能、资源利用多方面的考虑,我们不可能要求
JVM
一次性将全部的类都加载完成,而是只加载能够支持系统顺利启动和运行的类和资源即可。那么在系统运行过程中如果需要使用未在启动时加载的类或资源时该怎么办呢?这就要靠类加载器来完成了。

涉及知识点:APM, java Agent, plugin, bytecode, asm, InvocationHandler,
smail

作者介绍

阿里妹导读:减少故障的最好方法就是让故障经常性的发生。通过不断重复失败过程,持续提升系统的容错和弹性能力。今天,阿里巴巴把六年来在故障演练领域的创意和实践汇浓缩而成的工具进行开源,它就是
“ChaosBlade”。如果你想要提升开发效率,不妨来了解一下。

1.Classloader类结构分析

什么是类加载器

类加载器(ClassLoader)就是在系统运行过程中动态的将字节码文件加载到 JVM
中的工具,基于这个工具的整套类加载流程,我们称作类加载机制。我们在 IDE
中编写的都是源代码文件,以后缀名 .java
的文件形式存在于磁盘上,通过编译后生成后缀名 .class
的字节码文件,ClassLoader 加载的就是这些字节码文件。

APM : 应用程序性能管理。 2011年时国外的APM行业 NewRelic 和 APPDynamics
已经在该领域拔得头筹,国内近些年来也出现一些APM厂商,如: 听云,
OneAPM, 博睿 云智慧,阿里百川码力。
(据分析,国内android端方案都是抄袭NewRelic公司的,由于该公司的sdk未混淆,业界良心)

王鹏,2017年加入去哪儿机票事业部,主要从事后端研发工作,目前在机票事业部负责行程单和故障演练平台以及公共服务ES、数据同步中间件等相关的研发工作。

高可用架构是保障服务稳定性的核心。

(1)主要由四个方法,分别是defineClass,findClass,loadClass,resolveClass
  • <1>defineClass(byte[] , int ,int)
    将byte字节流解析为JVM能够识别的Class对象(直接调用这个方法生成的Class对象还没有resolve,这个resolve将会在这个对象真正实例化时resolve)

  • <2>findClass,通过类名去加载对应的Class对象。当我们实现自定义的classLoader通常是重写这个方法,根据传入的类名找到对应字节码的文件,并通过调用defineClass解析出Class独享

  • <3>loadClass运行时可以通过调用此方法加载一个类(由于类是动态加载进jvm,用多少加载多少的?)

  • <4>resolveClass手动调用这个使得被加到JVM的类被链接(解析resolve这个类?)

有哪些类加载器

Java 默认提供了三个 ClassLoader,分别是
AppClassLoader、ExtClassLoader、BootStrapClassLoader,依次后者分别是前者的「父加载器」。父加载器不是「父类」,三者之间没有继承关系,只是因为类加载的流程使三者之间形成了父子关系,下文会详细讲述。

能做什么:
crash监控,卡顿监控,内存监控,增加trace,网络性能监控,app页面自动埋点,等。

去哪儿网2005年成立至今,随着系统规模的逐步扩大,已经有成百上千个应用系统,这些系统之间的耦合度和链路的复杂度不断加强,对于我们构建分布式高可用的系统架构具有极大挑战。我们需要一个平台在运行期自动注入故障,检验故障预案是否起效——故障演练平台。

阿里巴巴在海量互联网服务以及历年双11场景的实践过程中,沉淀出了包括全链路压测、线上流量管控、故障演练等高可用核心技术,并通过开源和云上服务的形式对外输出,以帮助企业用户和开发者享受阿里巴巴的技术红利,提高开发效率,缩短业务的构建流程。

(2)实现自定义ClassLoader一般会继承URLClassLoader类,因为这个类实现了大部分方法。

BootStrapClassLoader

BootStrapClassLoader 也叫「根加载器」,它是脱离 Java 语言,使用 C/C++
编写的类加载器,所以当你尝试使用 ExtClassLoader 的实例调用 getParent()
方法获取其父加载器时会得到一个 null 值。

// 返回一个 AppClassLoader 的实例ClassLoader appClassLoader = this.getClass().getClassLoader();// 返回一个 ExtClassLoader 的实例ClassLoader extClassLoader = appClassLoader.getParent();// 返回 null,因为 BootStrapClassLoader 是 C/C++ 编写的,无法在 Java 中获得其实例ClassLoader bootstrapClassLoader = extClassLoader.getParent();

根加载器会默认加载系统变量 sun.boot.class.path 指定的类库(jar 文件和
.class 文件),默认是 $JRE_HOME/lib 下的类库,如 rt.jar、resources.jar
等,具体可以输出该环境变量的值来查看。

String bootClassPath = System.getProperty("sun.boot.class.path");String[] paths = bootClassPath.split(":");for (String path : paths) {    System.out.println;}// output// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/resources.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/sunrsasign.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jsse.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jce.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/charsets.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jfr.jar// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/classes

除了加载这些默认的类库外,也可以使用 JVM 参数 -Xbootclasspath/a
来追加额外需要让根加载器加载的类库。比如我们自定义一个
com.ganpengyu.boot.DateUtils 类来让根加载器加载。

package com.ganpengyu.boot;import java.text.SimpleDateFormat;import java.util.Date;public class DateUtils {    public static void printNow() {        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        System.out.println(sdf.format(new Date;    }}

我们将其制作成一个名为 gpy-boot 的 jar 包放到 /Users/yu/Desktop/lib
下,然后写一个测试类去尝试加载 DateUtils。

public class Test {    public static void main(String[] args) throws Exception {        Class<?> clz = Class.forName("com.ganpengyu.boot.DateUtils");        ClassLoader loader = clz.getClassLoader();        System.out.println(loader == null);    }}

运行这个测试类:

java -Xbootclasspath/a:/Users/yu/Desktop/lib/gpy-boot.jar -cp /Users/yu/Desktop/lib/gpy-boot.jar:. Test

可以看到输出为 true,也就是说加载 com.ganpengyu.boot.DateUtils
的类加载器在 Java
中无法获得其引用,而任何类都必须通过类加载器加载才能被使用,所以推断出这个类是被
BootStrapClassLoader 加载的,也证明了 -Xbootclasspath/a
参数确实可以追加需要被根加载器额外加载的类库。

总之,对于 BootStrapClassLoader 这个根加载器我们需要知道三点:

  1. 根加载器使用 C/C++ 编写,我们无法在 Java 中获得其实例
  2. 根加载器默认加载系统变量 sun.boot.class.path 指定的类库
  3. 可以使用 -Xbootclasspath/a 参数追加根加载器的默认加载类库

性能监控其实就是hook
代码到项目代码中,从而做到各种监控。常规手段都是在项目中增加代码,但如何做到非侵入式的,即一个sdk即可。

一、背景

例如,借助阿里云性能测试 PTS,高效率构建全链路压测体系,通过开源组件
Sentinel 实现限流和降级功能。这一次,经历了 6
年时间的改进和实践,累计在线上执行演练场景达数万次,我们将阿里巴巴在故障演练领域的创意和实践,浓缩成一个混沌工程工具,并将其开源,命名为
ChaosBlade。

2.ClassLoader的等级加载机制

ExtClassLoader

ExtClassLoader 也叫「扩展类加载器」,它是一个使用 Java
实现的类加载器(sun.misc.Launcher.ExtClassLoader),用于加载系统所需要的扩展类库。默认加载系统变量
java.ext.dirs 指定位置下的类库,通常是 $JRE_HOME/lib/ext
目录下的类库。

public static void main(String[] args) {    String extClassPath = System.getProperty("java.ext.dirs");    String[] paths = extClassPath.split(":");    for (String path : paths) {        System.out.println;    }}// output// /Users/leon/Library/Java/Extensions// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext// /Library/Java/Extensions// /Network/Library/Java/Extensions// /System/Library/Java/Extensions// /usr/lib/java

我们可以在启动时修改java.ext.dirs
变量的值来修改扩展类加载器的默认类库加载目录,但通常并不建议这样做。如果我们真的有需要扩展类加载器在启动时加载的类库,可以将其放置在默认的加载目录下。总之,对于
ExtClassLoader 这个扩展类加载器我们需要知道两点:

  1. 扩展类加载器是使用 Java
    实现的类加载器,我们可以在程序中获得它的实例并使用。
  2. 通常不建议修改java.ext.dirs
    参数的值来修改默认加载目录,如有需要,可以将要加载的类库放到这个默认目录下。

1. 如何hook

切面编程-- AOP。我们的方案是AOP的一种,通过修改app
class字节码的形式将我们项目的class文件进行修改,从而做到嵌入我们的监控代码。

澳门在线威尼斯网址 2androidbuilder.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就登场了,参考第三节。

这是某事业部的系统拓扑图:

ChaosBlade 是什么?

ChaosBlade
是一款遵循混沌工程实验原理,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具,可实现底层故障的注入,特点是操作简洁、无侵入、扩展性强。

ChaosBlade 基于 Apache License v2.0 开源协议,目前有 chaosblade 和
chaosblade-exe-jvm 两个仓库。

chaosblade 包含 CLI 和使用 Golang
实现的基础资源、容器相关的混沌实验实施执行模块。chaosblade-exe-jvm
是对运行在 JVM 上的应用实施混沌实验的执行器。

ChaosBlade 社区后续还会添加 C++、Node.js 等其他语言的混沌实验执行器。

澳门在线威尼斯网址 3

(1)JVM平台提供三层的ClassLoader,这三层ClassLoader可以分为两类,分别是服务JVM自身的,和服务广大普通类的。分别是:
  • <1>BootstrapClassLoader:主要加载JVM自身工作所需要的类,该ClassLoader没有父类加载器和子类加载器

  • <2>ExtClassLoader:这个类加载器同样是JVM自身的一部分,但是不是由JVM实现,主要用于加载System.getProperty(“java.ext.dirs”)目录地下的类,如本机的值“D:\java\jdk7\jre\lib\ext;C:\Windows\Sun\Java\lib\ext”

  • <3>AppClassLoader:加载System.getProperty(“java.class.path”)(注意了在ide中运行程序时,该值通常是该项目的classes文件夹)中的类。所有的自定义类加载器不管直接实现ClassLoader,是继承自URLClassLoader或其子类,其父加载器(注意:父加载器与父类的分别)都是AppClassLoader,因为不管调用哪个父类的构造器,最终都将调用getSystemClassLoader作为父加载器,而该方法返回的正是AppClassLoader。(当应用程序中没有其他自定义的classLoader,那么除了System.getProperty(“java.ext.dirs”)目录中的类,其他类都由AppClassLoader加载)

AppClassLoader

AppClassLoader 也叫「应用类加载器」,它和 ExtClassLoader 一样,也是使用
Java
实现的类加载器(sun.misc.Launcher.AppClassLoader)。它的作用是加载应用程序
classpath
下所有的类库。这是我们最常打交道的类加载器,我们在程序中调用的很多
getClassLoader()
方法返回的都是它的实例。在我们自定义类加载器时如果没有特别指定,那么我们自定义的类加载器的默认父加载器也是这个应用类加载器。总之,对于
AppClassLoader 这个应用类加载器我们需要知道两点:

  1. 应用类加载器是使用 Java 实现的类加载器,负责加载应用程序 classpath
    下的类库。
  2. 应用类加载器是和我们最常打交道的类加载器。
  3. 没有特别指定的情况下,自定义类加载器的父加载器就是应用类加载器。

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)

有了javaagent, 我们就可以在编译app时重新修改dex
的Main类,对应修改processClass方法。

如何修改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

查看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。
  • 小而快、非常可靠。
  • 拥有庞大的用户社区,可以为新用户􏰁供支持。
  • 源许可开放,几乎允许任意使用。

澳门在线威尼斯网址 4ASM_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包路径)。

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().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注释即可。

澳门在线威尼斯网址 5

为什么要开源?

很多公司已经开始关注并探索混沌工程,渐渐成为测试系统高可用,构建对系统信息不可缺少的工具。但混沌工程领域目前还处于一个快速演进的阶段,最佳实践和工具框架没有统一标准。实施混沌工程可能会带来一些潜在的业务风险,经验和工具的缺失也将进一步阻止
DevOps 人员实施混沌工程。

混沌工程领域目前也有很多优秀的开源工具,分别覆盖某个领域,但这些工具的使用方式千差万别,其中有些工具上手难度大,学习成本高,混沌实验能力单一,使很多人对混沌工程领域望而却步。

阿里巴巴集团在混沌工程领域已经实践多年,将混沌实验工具 ChaosBlade
开源目的,我们希望:

  • 让更多人了解并加入到混沌工程领域;
  • 缩短构建混沌工程的路径;
  • 同时依靠社区的力量,完善更多的混沌实验场景,共同推进混沌工程领域的发展。
(2)Jvm加载class文件到内存有两种方式,隐式加载和显示加载,通常这两种方式是混合使用的
  • <1>隐式加载:是通过JVM来自动加载需要的类到内存的方式,当某个类被使用时,JVM发现该类不在内存中,那么它就会自动加载该类到内存

  • <2>显示加载:通过调用this.getClasss.getClassLoader.loadClass(),Class.forName,自己实现的ClassLoader的findClass方法

自定义类加载器

除了上述三种 Java 默认提供的类加载器外,我们还可以通过继承
java.lang.ClassLoader
来自定义一个类加载器。如果在创建自定义类加载器时没有指定父加载器,那么默认使用
AppClassLoader
作为父加载器。关于自定义类加载器的创建和使用,我们会在后面的章节详细讲解。

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;

查看伪代码, 符合预期结果。验证完毕。

系统之间的依赖非常复杂、调用链路很深、服务之间没有分层。在这种复杂的依赖下,系统发生了几起故障:

ChaosBlade 能解决哪些问题?

衡量微服务的容错能力

通过模拟调用延迟、服务不可用、机器资源满载等,查看发生故障的节点或实例是否被自动隔离、下线,流量调度是否正确,预案是否有效,同时观察系统整体的
QPS 或 RT
是否受影响。在此基础上可以缓慢增加故障节点范围,验证上游服务限流降级、熔断等是否有效。最终故障节点增加到请求服务超时,估算系统容错红线,衡量系统容错能力。

验证容器编排配置是否合理

通过模拟杀服务 Pod、杀节点、增大 Pod
资源负载,观察系统服务可用性,验证副本配置、资源限制配置以及 Pod
下部署的容器是否合理。

测试 PaaS 层是否健壮

通过模拟上层资源负载,验证调度系统的有效性;模拟依赖的分布式存储不可用,验证系统的容错能力;模拟调度节点不可用,测试调度任务是否自动迁移到可用节点;模拟主备节点故障,测试主备切换是否正常。

验证监控告警的时效性

通过对系统注入故障,验证监控指标是否准确,监控维度是否完善,告警阈值是否合理,告警是否快速,告警接收人是否正确,通知渠道是否可用等,提升监控告警的准确和时效性。

定位与解决问题的应急能力

通过故障突袭,随机对系统注入故障,考察相关人员对问题的应急能力,以及问题上报、处理流程是否合理,达到以战养战,锻炼人定位与解决问题的能力。

(3)上级委托机制:当一个加载器加载类字时,先委托其父加载器加载,若加载成功则反馈给该加载器,若父加载器不能加载,则由该加载器加载

类加载器的启动顺序

上文已经提到过 BootStrapClassLoader 是一个使用 C/C++
编写的类加载器,它已经嵌入到了 JVM 的内核之中。当 JVM
启动时,BootStrapClassLoader
也会随之启动并加载核心类库。当核心类库加载完成后,BootStrapClassLoader
会创建 ExtClassLoader 和 AppClassLoader 的实例,两个 Java
实现的类加载器将会加载自己负责路径下的类库,这个过程我们可以在
sun.misc.Launcher 中窥见。

  • 弱依赖挂掉,主流程挂掉,修改报销凭证的支付状态,下单主流程失败;
  • 核心服务调用量陡增,某服务超时引起相关联的所有服务“雪崩”;
  • 机房网络或者某些机器挂掉,不能提供核心服务。

功能和特点

场景丰富度高

ChaosBlade 支持的混沌实验场景不仅覆盖基础资源,如 CPU 满载、磁盘 IO
高、网络延迟等,还包括运行在 JVM 上的应用实验场景,如 Dubbo
调用超时和调用异常、指定方法延迟或抛异常以及返回特定值等,同时涉及容器相关的实验,如杀容器、杀
Pod。后续会持续的增加实验场景。

使用简洁,易于理解

ChaosBlade 通过 CLI
方式执行,具有友好的命令提示功能,可以简单快速的上手使用。命令的书写遵循阿里巴巴集团内多年故障测试和演练实践抽象出的故障注入模型,层次清晰,易于阅读和理解,降低了混沌工程实施的门槛。

场景扩展方便

所有的 ChaosBlade
实验执行器同样遵循上述提到的故障注入模型,使实验场景模型统一,便于开发和维护。模型本身通俗易懂,学习成本低,可以依据模型方便快捷的扩展更多的混沌实验场景。

澳门在线威尼斯网址 6

3.如何加载class文件:

分为三个步骤 加载字节码到内存、Linking、类字节初始化赋值

ExtClassLoader 的创建过程

我们将 Launcher 类的构造方法源码精简展示如下:

public Launcher() {    // 创建 ExtClassLoader    Launcher.ExtClassLoader var1;    try {        var1 = Launcher.ExtClassLoader.getExtClassLoader();    } catch (IOException var10) {        throw new InternalError("Could not create extension class loader", var10);    }    // 创建 AppClassLoader    try {        this.loader = Launcher.AppClassLoader.getAppClassLoader;    } catch (IOException var9) {        throw new InternalError("Could not create application class loader", var9);    }    // 设置线程上下文类加载器    Thread.currentThread().setContextClassLoader(this.loader);    // 创建 SecurityManager}

可以看到当 Launcher 被初始化时就会依次创建 ExtClassLoader 和
AppClassLoader。我们进入 getExtClassLoader()
方法并跟踪创建流程,发现这里又调用了 ExtClassLoader
的构造方法,在这个构造方法里调用了父类的构造方法,这便是 ExtClassLoader
创建的关键步骤,注意这里传入父类构造器的第二个参数为
null。接着我们去查看这个父类构造方法,它位于 java.net.URLClassLoader
类中:

URLClassLoader(URL[] urls, ClassLoader parent,                          URLStreamHandlerFactory factory)

通过这个构造方法的签名和注释我们可以明确的知道,第二个参数 parent
表示的是当前要创建的类加载器的父加载器。结合前面我们提到的
ExtClassLoader 的父加载器是 JVM 内核中 C/C++ 开发的
BootStrapClassLoader,且无法在 Java
中获得这个类加载器的引用,同时每个类加载器又必然有一个父加载器,我们可以反证出,ExtClassLoader
的父加载器就是 BootStrapClassLoader。

三个故障原因:

ChaosBlade 的演进史

EOS(2012-2015):故障演练平台的早期版本,故障注入能力通过字节码增强方式实现,模拟常见的
RPC 故障,解决微服务的强弱依赖治理问题。

MonkeyKing(2016-2018):故障演练平台的升级版本,丰富了故障场景(如:资源、容器层场景),开始在生产环境进行一些规模化的演练。

AHAS(2018.9-至今):阿里云应用高可用服务,内置演练平台的全部功能,支持可编排演练、演练插件扩展等能力,并整合了架构感知和限流降级的功能。

ChaosBlade:是 MonkeyKing
平台底层故障注入的实现工具,通过对演练平台底层的故障注入能力进行抽象,定义了一套故障模型。配合用户友好的
CLI 工具进行开源,帮助云原生用户进行混沌工程测试。

澳门在线威尼斯网址 7

(1)加载字节码到内存:(这一步通常通过findclass()方法实现)

以URLClassLoader为例:该类的构造函数返现必须制定一个URL数据才能创建该对象,该类中包含一个URLClassPath对象,URLClassPath会判断传过来的URL是文件还是Jar包,创建相应的FileLoader或者JarLoader或者默认加载器,当jvm调用findclass时,这些加载器将class文件的字节码加载到内存中

AppClassLoader 的创建过程

理清了 ExtClassLoader 的创建过程,我们来看 AppClassLoader
的创建过程就清晰很多了。跟踪 getAppClassLoader()
方法的调用过程,可以看到这个方法本身将 ExtClassLoader
的实例作为参数传入,最后还是调用了 java.net.URLClassLoader
的构造方法,将 ExtClassLoader 的实例作为父构造器 parent
参数值传入。所以这里我们又可以确定,AppClassLoader 的父构造器就是
ExtClassLoader。

  • 系统强弱依赖混乱、弱依赖无降级;
  • 系统流量陡增,系统容量不足,没有限流熔断机制;
  • 硬件资源网络出现问题影响系统运行,没有高可用的网络架构。

近期规划

功能迭代:

  • 增强 JVM 演练场景,支持更多的 Java 主流框架,如 Redis,GRPC
  • 增强 Kubernetes 演练场景
  • 增加对 C++、Node.js 等应用的支持
(2)Linking:验证与解析,包含3步:
  • <1>字节码验证

  • <2>类准备:准备代表每个类中定义的字段、方法和实现接口所需的数据结构

  • <3>解析:这个阶段类装入器转入类所应用的其他类

怎么加载一个类

将一个 .class 字节码文件加载到 JVM 中成为一个 java.lang.Class
实例需要加载这个类的类加载器及其所有的父级加载器共同参与完成,这主要是遵循「双亲委派原则」。

各种各样的问题,在这种复杂的依赖结构下被放大,一个依赖30个SOA服务的系统,每个服务99.99%可用。99.99%的30次方≈99.7%。0.3%意味着一亿次请求会有3,000,00次失败,换算成时间大约每月有2个小时服务不稳定。随着服务依赖数量的变多,服务不稳定的概率会呈指数性提高,这些问题最后都会转化为故障表现出来。

社区共建:

欢迎访问 ChaosBlade@GitHub,参与社区共建,包括但不限于:

  • 架构设计
  • 模块设计
  • 代码实现
  • Bug Fix
  • Demo样例
  • 文档、网站和翻译

本文作者:中亭

阅读原文

本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

(3)初始化class对象,执行静态初始化器并在这阶段末尾初始化静态字段为默认值

双亲委派

当我们要加载一个应用程序 classpath 下的自定义类时,AppClassLoader
会首先查看自己是否已经加载过这个类,如果已经加载过则直接返回类的实例,否则将加载任务委托给自己的父加载器
ExtClassLoader。同样,ExtClassLoader
也会先查看自己是否已经加载过这个类,如果已经加载过则直接返回类的实例,否则将加载任务委托给自己的父加载器
BootStrapClassLoader。

BootStrapClassLoader
收到类加载任务时,会首先检查自己是否已经加载过这个类,如果已经加载则直接返回类的实例,否则在自己负责的加载路径下搜索这个类并尝试加载。如果找到了这个类,则执行加载任务并返回类实例,否则将加载任务交给
ExtClassLoader 去执行。

ExtClassLoader
同样也在自己负责的加载路径下搜索这个类并尝试加载。如果找到了这个类,则执行加载任务并返回类实例,否则将加载任务交给
AppClassLoader 去执行。

由于自己的父加载器 ExtClassLoader 和 BootStrapClassLoader
都没能成功加载到这个类,所以最后由 AppClassLoader
来尝试加载。同样,AppClassLoader 会在 classpath
下所有的类库中查找这个类并尝试加载。如果最后还是没有找到这个类,则抛出
ClassNotFoundException 异常。

综上,当类加载器要加载一个类时,如果自己曾经没有加载过这个类,则层层向上委托给父加载器尝试加载。对于
AppClassLoader 而言,它上面有 ExtClassLoader 和
BootStrapClassLoader,所以我们称作「双亲委派」。但是如果我们是使用自定义类加载器来加载类,且这个自定义类加载器的默认父加载器是
AppClassLoader
时,它上面就有三个父加载器,这时再说「双亲」就不太合适了。当然,理解了加载一个类的整个流程,这些名字就无关痛痒了。

二、系统高可用的方法论

4.常见加载类错误分析

为什么需要双亲委派机制

「双亲委派机制」最大的好处是避免自定义类和核心类库冲突。比如我们大量使用的
java.lang.String 类,如果我们自己写的一个 String
类被加载成功,那对于应用系统来说完全是毁灭性的破坏。我们可以尝试着写一个自定义的
String 类,将其包也设置为 java.lang

package java.lang;public class String {    private int n;    public String {        this.n = n;    }    public String toLowerCase() {        return new String(this.n + 100);    }}

我们将其制作成一个 jar 包,命名为 thief-jdk,然后写一个测试类尝试加载
java.lang.String 并使用接收一个 int 类型参数的构造方法创建实例。

import java.lang.reflect.Constructor;public class Test {    public static void main(String[] args) throws Exception {        Class<?> clz = Class.forName("java.lang.String");        System.out.println(clz.getClassLoader() == null);        Constructor<?> c = clz.getConstructor(int.class);        String str =  c.newInstance;        str.toLowerCase();    }}

运行测试程序

java -cp /Users/yu/Desktop/lib/thief/thief-jdk.jar:. Test

程序抛出 NoSuchMethodException 异常,因为 JVM 不能够加载我们自定义的
java.lang.String,而是从 BootStrapClassLoader
的缓存中返回了核心类库中的 java.lang.String 的实例,且核心类库中的
String 没有接收 int 类型参数的构造方法。同时我们也看到 Class
实例的类加载器是 null,这也说明了我们拿到的 java.lang.String
的实例确实是由 BootStrapClassLoader 加载的。

总之,「双亲委派」机制的作用就是确保类的唯一性,最直接的例子就是避免我们自定义类和核心类库冲突。

如何构建一个高可用的系统呢?首先要分析一下不可用的因素都有哪些:

(1)ClassNotFoundException:

通常是jvm要加载一个文件的字节码到内存时,没有找到这些字节码(如forName,loadClass等方法)

JVM 怎么判断两个类是相同的

「双亲委派」机制用来保证类的唯一性,那么 JVM
通过什么条件来判断唯一性呢?其实很简单,只要两个类的全路径名称一致,且都是同一个类加载器加载,那么就判断这两个类是相同的。如果同一份字节码被不同的两个类加载器加载,那么它们就不会被
JVM 判断为同一个类。

Person 类

public class Person {    private Person p;    public void setPerson(Object obj) {        this.p =  obj;    }}

setPerson(Object obj) 方法接收一个对象,并将其强制转换为 Person
类型赋值给变量 p。

测试类

import java.lang.reflect.Method;public class Test {    public static void main(String[] args) {        CustomClassLoader classLoader1 = new CustomClassLoader("/Users/yu/Desktop/lib");        CustomClassLoader classLoader2 = new CustomClassLoader("/Users/yu/Desktop/lib");        try {            Class c1 = classLoader1.findClass("Person");            Object instance1 = c1.newInstance();            Class c2 = classLoader2.findClass("Person");            Object instance2 = c2.newInstance();            Method method = c1.getDeclaredMethod("setPerson", Object.class);            method.invoke(instance1, instance2);        } catch (Exception e) {            e.printStackTrace();        }    }}

CustomClassLoader
是一个自定义的类加载器,它将字节码文件加载为字符数组,然后调用
ClassLoader 的 defineClass()
方法创建类的实例,后文会详细讲解怎么自定义类加载器。在测试类中,我们创建了两个类加载器的实例,让他们分别去加载同一份字节码文件,即
Person 类的字节码。然后在实例一上调用 setPerson()
方法将实例二传入,将实例二强制转型为实例一。

运行程序会看到 JVM 抛出了 ClassCastException 异常,异常信息为
Person cannot be cast to Person。从这我们就可以知道,同一份字节码文件,如果使用的类加载器不同,那么
JVM 就会判断他们是不同的类型。

澳门在线威尼斯网址 8

(2)NoClassDefFoundError:

通常是使用new关键字,属性引用了某个类,继承了某个类或接口,但JVM加载这些类时发现这些类不存在的异常

全盘负责

「全盘负责」是类加载的另一个原则。它的意思是如果类 A 是被类加载器 X
加载的,那么在没有显示指定别的类加载器的情况下,类 A
引用的其他所有类都由类加载器 X
负责加载,加载过程遵循「双亲委派」原则。我们编写两个类来验证「全盘负责」原则。

Worker 类

package com.ganpengyu.full;import com.ganpengyu.boot.DateUtils;public class Worker {    public Worker() {    }    public void say() {        DateUtils dateUtils = new DateUtils();        System.out.println(dateUtils.getClass().getClassLoader() == null);        dateUtils.printNow();    }}

DateUtils 类

package com.ganpengyu.boot;import java.text.SimpleDateFormat;import java.util.Date;public class DateUtils {    public void printNow() {        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        System.out.println(sdf.format(new Date;    }}

测试类

import com.ganpengyu.full.Worker;import java.lang.reflect.Constructor;public class Test {    public static void main(String[] args) throws Exception {        Class<?> clz = Class.forName("com.ganpengyu.full.Worker");        System.out.println(clz.getClassLoader() == null);        Worker worker =  clz.newInstance();        worker.say();    }}

运行测试类

java -Xbootclasspath/a:/Users/yu/Desktop/lib/worker.jar Test

运行结果

truetrue2018-09-16 22:34:43

我们将 Worker 类和 DateUtils 类制作成名为worker 的 jar
包,将其设置为由根加载器加载,这样 Worker
类就必然是被根加载器加载的。然后在 Worker 类的 say() 方法中初始化了
DateUtils 类,然后判断 DateUtils
类是否由根加载器加载。从运行结果看到,Worker 和其引用的 DateUtils
类都被跟加载器加载,符合类加载的「全盘委托」原则。

「全盘委托」原则实际是为「双亲委派」原则提供了保证。如果不遵守「全盘委托」原则,那么同一份字节码可能会被
JVM
加载出多个不同的实例,这就会导致应用系统中对该类引用的混乱,具体可以参考上文「JVM
怎么判断两个类是相同的」这一节的示例。

高可用系统典型实践

(3)UnsatisfiedLinkErrpr:

如native的方法找不到本机的lib

自定义类加载器

除了使用 JVM 预定义的三种类加载器外,Java
还允许我们自定义类加载器以让我们系统的类加载方式更灵活。要自定义类加载器非常简单,通常只需要三个步骤:

  1. 继承 java.lang.ClassLoader 类,让 JVM 知道这是一个类加载器
  2. 重写 findClass(String name) 方法,告诉 JVM
    在使用这个类加载器时应该按什么方式去寻找 .class 文件
  3. 调用 defineClass(String name, byte[] b, int off, int len) 方法,让
    JVM 加载上一步读取的 .class 文件

import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;public class CustomClassLoader extends ClassLoader {    private String classpath;        public CustomClassLoader(String classpath) {        this.classpath = classpath;    }    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        String classFilePath = getClassFilePath;        byte[] classData = readClassFile(classFilePath);        return defineClass(name, classData, 0, classData.length);    }    public String getClassFilePath(String name) {        if (name.lastIndexOf(".") == -1) {            return classpath + "/" + name + ".class";        } else {            name = name.replace(".", "/");            return classpath + "/" + name + ".class";        }    }    public byte[] readClassFile(String filepath) {        Path path = Paths.get;        if (!Files.exists {            return null;        }        try {            return Files.readAllBytes;        } catch (IOException e) {            throw new RuntimeException("Can not read class file into byte array");        }    }    public static void main(String[] args) {        CustomClassLoader loader = new CustomClassLoader("/Users/leon/Desktop/lib");        try {            Class<?> clz = loader.loadClass("com.ganpengyu.demo.Person");            System.out.println(clz.getClassLoader().toString;            Constructor<?> c = clz.getConstructor(String.class);            Object instance = c.newInstance("Leon");            Method method = clz.getDeclaredMethod("say", null);            method.invoke(instance, null);        } catch (Exception e) {            e.printStackTrace();        }    }}

示例中我们通过继承 java.lang.ClassLoader
创建了一个自定义类加载器,通过构造方法指定这个类加载器的类路径(classpath)。重写
findClass(String name) 方法自定义类加载的方式,其中
getClassFilePath(String filepath) 方法和
readClassFile(String filepath) 方法用于找到指定的 .class
文件并加载成一个字符数组。最后调用
defineClass(String name, byte[] b, int off, int len)
方法完成类的加载。

main() 方法中我们测试加载了一个 Person 类,通过
loadClass(String name) 方法加载一个 Person 类。我们自定义的
findClass(String name)
方法,就是在这里面调用的,我们把这个方法精简展示如下:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {    synchronized (getClassLoadingLock {        // 先检查是否已经加载过这个类        Class<?> c = findLoadedClass;        if (c == null) {            long t0 = System.nanoTime();            try {                // 否则的话递归调用父加载器尝试加载                if (parent != null) {                    c = parent.loadClass(name, false);                } else {                    // 所有父加载器都无法加载,使用根加载器尝试加载                    c = findBootstrapClassOrNull;                }            } catch (ClassNotFoundException e) {}            if (c == null) {                // 所有父加载器和根加载器都无法加载                // 使用自定义的 findClass() 方法查找 .class 文件                c = findClass;            }        }        return c;    }}

可以看到 loadClass(String name)
方法内部是遵循「双亲委派」机制来完成类的加载。在「双亲」都没能成功加载类的情况下才调用我们自定义的
findClass(String name) 方法查找目标类执行加载。

理论上来说,当图中所有的事情都做完,我们就可以认为系统是一个真正的高可用系统。但真是这样吗?

5.常用classLoader(书本此处其实是对tom加载servlet使用的classLoader分析)

为什么需要自定义类加载器

自定义类加载器的用处有很多,这里简单列举一些常见的场景。

  1. 从任意位置加载类。JVM
    预定义的三个类加载器都被限定了自己的类路径,我们可以通过自定义类加载器去加载其他任意位置的类。
  2. 解密类文件。比如我们可以对编译后的类文件进行加密,然后通过自定义类加载器进行解密。当然这种方法实际并没有太大的用处,因为自定义的类加载器也可以被反编译。
  3. 支持更灵活的内存管理。我们可以使用自定义类加载器在运行时卸载已加载的类,从而更高效的利用内存。

那么故障演练平台就隆重登场了。当上述的高可用实践都做完,利用故障演练平台做一次真正的故障演练,在系统运行期动态地注入一些故障,从而来验证下系统是否按照故障预案去执行相应的降级或者熔断策略。

(1)AppClassLoader:

加载jvm的classpath中的类和tomcat的核心类

就这样吧

类加载器是 Java
中非常核心的技术,本文仅对类加载器进行了较为粗浅的分析,如果需要深入更底层则需要我们打开
JVM 的源码进行研读。「Java 有路勤为径,JVM 无涯苦作舟」,与君共勉。

三、故障演练平台

(2)StandardClassLoader:

加载tomcat容器的classLoader,另外webAppClassLoader在loadclass时,发现类不在JVM的classPath下,在PackageTriggers(是一个字符串数组,包含一组不能使用webAppClassLoader加载的类的包名字符串)下的话,将由该加载器加载(注意:StandardClassLoader并没有覆盖loadclass方法,所以其加载的类和AppClassLoader加载没什么分别,并且使用getClassLoader返回的也是AppClassLoader)(另外,如果web应用直接放在tomcat的webapp目录下该应用就会通过StandardClassLoader加载,估计是因为webapp目录在PackageTriggers中?)