SkyWalking采⽤java的Java agent(探针),在加载字节码⽂件时, 动态织⼊代码

基于OpenTracing 规范,专⻔为微服务架构以及云原⽣架构⽽设计的,是⼀个可观测性分析平台(Observability Analysis Platform简称OAP)和应⽤性能管理系统(Application Performance Management简称APM)。
2019年从 Apache 基⾦会孵化器毕业成为顶级项⽬。⽀持多种语⾔。

官网链接:https://skywalking.apache.org/docs/main/latest/readme/

项目架构图

在这里插入图片描述
在这里插入图片描述

SkyWalking 三个核⼼部分

Agent(探针):Agent 运⾏在各个服务实例中,负责采集服务实例的 Trace 、Metrics 等数据,
然后通过 gRPC ⽅式上报给 SkyWalking 后端
OAP:SkyWalking 的后端服务,⼀个是负责接收 Agent 上报上来的 Trace、Metrics 等数据,进⾏
流式分析,最终将分析得到的结果写⼊持久化存储中,另⼀个是负责响应 SkyWalking UI 界⾯发送
来的查询请求。
UI 界⾯:展示查询结果

SkyWalking原理

如何⽆侵⼊埋点,如何动态修改字节码,如何收集信息,插件怎么实现,数据如何传输到OAP?

OpenTracing

OpenTracing(http://opentracing.io/)是分布式链路跟踪系统的设计原则、规范、标准。

⾃从 Google Dapper 的论⽂发布之后,各⼤互联⽹公司和开源社区开发的分布式链路追踪产品百花⻬放,同时也给使⽤者带来了⼀个问题,各个分布式链路追踪产品的 API 并不兼容,如果⽤户在各个产品 之间进⾏切换,成本⾮常⾼。

⽽ OpenTracing 就完美的解决了这个问题,OpenTracing 通过提供平台⽆关、⼚商⽆关的 API,帮助开 发⼈员能够⽅便地添加(或更换)追踪系统。

OpenTracing做了什么?

  • 后台⽆关的⼀套接⼝,被跟踪的服务只需要调⽤这套接⼝,就可以被任何实现这套接⼝的跟踪后台(⽐如Zipkin, Jaeger等等)⽀持,⽽作为⼀个跟踪后台,只要实现了个这套接⼝,就可以跟踪到任何调⽤这套接⼝的服务
  • 标准化了对跟踪最⼩单位Span的管理:定义了开始Span,结束Span和记录Span耗时的API。
  • 标准化了进程间跟踪数据传递的⽅式:定义了⼀套API⽅便跟踪数据的传递
  • 标准化了进程内当前Span的管理:定义了存储和获取当前Span的API

SkyWalking Agent实现原理

SkyWalking Agent是通过Java Agent的方式随着应用程序一起启动,然后通过Byte Buddy库动态插入埋点信息收集Trace信息。

Java Agent

Java Agent本质上就是⼀个Jar包,它会调⽤ Instrumentation API,来修改已经加载到JVM中的字节码。

Instrumentation API由JVM提供⽤来修改已加载类的⼯具,可以提供Java语⾔编写的插桩功能,动态修改运⾏时代码的能⼒。

Java Agent具有两种加载⽅式。
静态加载
这种⽅式下,在应⽤程序的任何代码被执⾏之前,就加载Agent以修改字节码。静态加载需要使⽤JVM 的-javaagent参数:

java -javaagent:agent.jar -jar application.jar
# 可以同时加载多个Agents
java -javaagent:agentA.jar -javaagent:agentB.jar application.jar

动态加载
这种⽅式下,Agent可以在运⾏时动态按需的加载。动态加载需要调⽤Java Attach API,下⾯是个例⼦:

// 根据PID查找⽬标JVM,并连接到JVM 
VirtualMachine jvm = VirtualMachine.attach(jvmPid); 
// 加载Agent 
jvm.loadAgent(agentFile.getAbsolutePath()); 
// 取消到JVM的连接 
jvm.detach();

Instrumentation

Instrumentation是java提供的修改已经加载到JVM中字节码的API。使⽤ Instrumentation,开发者可以构建⼀个独⽴于应⽤程序的代理程序(Agent),⽤来监测和协助运⾏在 JVM 上的程序,甚⾄能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运⾏时虚拟机监控和Java 类操作了,这样的特性实际上提供了⼀种虚拟机级别⽀持的 AOP 实现⽅式,使得开发者⽆需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。JVM级别AOP

在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。

在 Java SE 6 ⾥⾯,instrumentation 包被赋予了更强⼤的功能:启动后的instrument、本地代码 (native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有更强的动态控制、解释能⼒,它使得 Java 语⾔变得更加灵活多变。

启动时修改

在这里插入图片描述
启动时修改主要是在jvm启动时,执行native函数的Agent_OnLoad方法,在方法执行时,执行如下步骤:

  • 创建InstrumentationImpl对象
  • 监听ClassFileLoadHook事件
  • 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法
运行时修改

在这里插入图片描述
运行时修改主要是通过jvm的attach机制来请求目标jvm加载对应的agent,执行native函数的Agent_OnAttach方法,在方法执行时,执行如下步骤:

  • 创建InstrumentationImpl对象
  • 监听ClassFileLoadHook事件
  • 调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Agentmain-Class类的agentmain方法
何时进行运行时替换?

在类加载完毕后,对应的想要替换函数可能正在执行,那么何时进行类字节码的替换呢?
由于运行时类字节码替换依赖于redefineClasses,那么可以看一下该方法的定义:

jvmtiError
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
  VMThread::execute(&op);
  return (op.check_error());
} /* end RedefineClasses */

其中整体的执行依赖于VMThread,VMThread是一个在虚拟机创建时生成的单例原生线程,这个线程能派生出其他线程。同时,这个线程的主要的作用是维护一个vm操作队列(VMOperationQueue),用于处理其他线程提交的vm operation,比如执行GC等。

VmThread在执行一个vm操作时,先判断这个操作是否需要在safepoint下执行。若需要safepoint下执行且当前系统不在safepoint下,则调用SafepointSynchronize的方法驱使所有线程进入safepoint中,再执行vm操作。执行完后再唤醒所有线程。若此操作不需要在safepoint下,或者当前系统已经在safepoint下,则可以直接执行该操作了。所以,在safepoint的vm操作下,只有vm线程可以执行具体的逻辑,其他线程都要进入safepoint下并被挂起,直到完成此次操作。
因此,在执行字节码替换的时候需要在safepoint下执行,因此整体会触发stop-the-world。

Byte Buddy

Byte Buddy 是⼀个字节码⽣成和操作库,简化字节码操作。

⽬前Java字节码⽣成框架⼤致有ASM、 Javassist和Byte Buddy三种。Byte Buddy⽐ javassist、 ASM操作⽅便。

Byte Buddy 提供了⼀套类型安全的 API 和注解,可以直接使⽤这些API 和注解实现复杂的字节码操作。

Byte Buddy 提供了针对 Java Agent 的额外 API,帮助开发⼈员在 Java Agent 场景轻松增强已有代码。

感谢您的阅读,如果您感觉本篇博客还不错,请帮忙留言+点赞+收藏呗。~~

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐