写在前面:
Skywalking-Agent作为集成到目标系统中的代理或SDK库,是Skywalking主要用来收集应用程序的各个维度的数据及应用程序链路跟踪,然后上报到SkyWalking-OAP(也是Skywalking的一部分)。不知道Skywalking的,先去了解了解Skywalking再来看哈。
虽然Skywalking-Agent提供了丰富的插件来跟踪不同服务组件的链路,不过有一些中间件还没法支持,为了支持对我们使用中间件的链路跟踪,我们不得不对SkyWalking-Agent底层源码进行二次开发来满足业务链路跟踪的需求以及熟悉Agent的一些高级用法。借此也了解一下 SkyWalking的Agent的设计和核心技术。
Skywalking-Agent简介
Skywalking-Agent采用插件化 + javaagent的形式来实现了数据的自动采集,这样可以做到对代码的无侵入性,插件化意味着可插拔,扩展性好(后文会介绍如何定义自己的插件)。其实很多主流框架都采用框架+插件的模式开发。如DataX、FlinkX通过插件支持众多异构数据源。
名词解释:
- SPI:Skywalking暴露的插件规范接口,支持构造方法拦截、实例方法拦截及静态方法拦截。
- Core:负责加载插件并且利用Byte Buddy提供的字节码增强逻辑对应用中指定类和方法的字节码进行增强。
- Plugin:自定的插件,依赖Skywalking暴露的插件规范接口及利用Byte Buddy实现字节码增强。开发者根据这些接口实现自定义插件。
Skywalking-Agent高级用法
如下图所示,Skywalking-Agent完成了插件的定义和实现及管理,其主要组成部分也就是定义插件的要素:增强类、拦截器、Interceptor,下面会详细介绍。
自定义插件
根据OpenTracing规范(什么是OpenTracing规范,后面有介绍)自研Oceanbase(华为研发的企业级分布式数据库, 后面见链接)插件。
插件如何实现呢,可以看到它主要由三个部分组成:
- 插件定义类: 指定插件的定义类,最终会根据这里的定义类打包生成plugin(在skywalking-plugin.def中定义了插件类)
- Instrumentation: 增强器:指定切面,切点,要对哪个类的哪个方法进行增强
- Interceptor: 拦截器指定步骤2中要在方法的前置,后置还是异常中写增强逻辑
下面进入创建环节:
增强器:
org.apache.skywalking.apm.plugin.oceanbase.v4.define.OceanBaseStatementInstrumentation
拦截器:
org.apache.skywalking.apm.plugin.oceanbase.v4.StatementExecuteMethodsInterceptor
- 创建Instrumentation
- 创建Interceptor
首先 beforeMethod代表在执行PreparedStatement的execute、executeQuery、executeUpdate方法之前会调用这里的方法,与之对应的是afterMethod,代表执行切点方法后做增强处理。
其次 在beforeMethod中定义创建Span(每一个组件对应一个Span,也就是当执行被增强类的前置处理时创建一个Span,用于记录本次增强的信息),往Span中添加tag,上图中添加了db_type(数据库类型:例如:Mysql、OceanBase等等)、db_instance(数据库ip+端口)、db_statement(所致行的sql)
- 创建skywalking-plugin.def
最后创建def文件,文件中通过k=v方式定义该插件所包含的增强类(agent启动时首先会扫描该def文件,然后初始化插件的增强类)
oceanbase-4.x=org.apache.skywalking.apm.plugin.oceanbase.v4.define.OceanBaseStatementInstrumentation
自定义tag
Skywalking-Agent为调用的增强类准备了多个tag属性,这些属性在
org.apache.skywalking.apm.agent.core.context.tag.Tags中,绝大部分属性能满足我们的业务场景中的使用,但我们可以自定义tag来满足我们个性化的业务场景,例如:Tags并没有一个tag用来记录本次增强处理的时间,就让我们来定义一个tag来记录它。
添加tag
在
org.apache.skywalking.apm.agent.core.context.tag.Tags中添加tag key,如下所示添加tag:timestamp
注:Tags中本身有很多个tag属性,此处为了较少code,省略了大部分tag
public final class Tags {
private static final Map TAG_PROTOTYPES = new ConcurrentHashMap<>();
private Tags() {
}
public static final StringTag URL = new StringTag(1, "url");
public static final IntegerTag HTTP_RESPONSE_STATUS_CODE = new IntegerTag(2, "http.status_code", true);
// add timestamp tag
public static final StringTag TIMESTAMP = new StringTag(3, "timestamp");
}
使用tag
我们只需要在beforeMethod中,向Span中添加tag key/value即可
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
Object request = ContextManager.getRuntimeContext().get(REQUEST_KEY_IN_RUNTIME_CONTEXT);
......
if (request != null) {
if (stackDepth == null) {
......
if (IN_SERVLET_CONTAINER && IS_JAVAX && HttpServletRequest.class.isAssignableFrom(request.getClass())) {
......
AbstractSpan span = ContextManager.createEntrySpan(operationName, contextCarrier);
Tags.TIMESTAMP.set(span,String.valueOf(System.currentTimeMillis()));
......
}
......
stackDepth = new StackDepth();
ContextManager.getRuntimeContext().put(CONTROLLER_METHOD_STACK_DEPTH, stackDepth);
}
......
}
}
插件运行起来的效果:
为了方便理解Tag在Skywalking中的角色,请看如下图及图中的Span部分:
首先说一下OpenTracing的规范,Skywalking按照OpenTracing将请求按照三个维度划分为Trace、Segment、Span三种模型,重点看一下Trace、Segment、Span这三种模型到底是什么。
- Trace: 表示一整条调用链,包括跨进程、跨线程的所有Segment的集合。Segment: 表示一个进程(JVM)或线程内的所有操作的集合,即包含若干个Span。Span: 表示一个具体的操作。Span在不同的实现里可能有不同的划分方式,这里介绍一个比较容易理解的定义方式:
- Tag:表示一个Span的具体操作的属性信息,例如:一次RPC查询请求是一个Span操作,这次查询请求包含的属性有:method(GET/POST等)、url(如: api/vi/report/search)、instance(127.0.0.1:8080)等等这些信息需要绑定到Span中,随着Span出栈上报到Skywalking-OAP。
自定义Endpoint
在使用Skywalking中很常见的一个问题是,Agent插件会对Controller的method进行增强,对于method中的XXXMapping书写方式会有value="detail/{id}"、value="/**"、value=""等等,对于这种restfull书写方式。Skywalking默认上报的Endpoint名称为detail/1、/**,我们无法定位到具体的请求url或者有太多的url,所以针对这种情况我们可以自定义Endpoint名称。如下所示。
Controller的method的增强器为:
org.apache.skywalking.apm.plugin.spring.mvc.v4.define.RestControllerInstrumentation
过滤器:
org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor.AbstractMethodInterceptor
如图中,operationName为Endpoint名称, 很明显是个不完整的url。我们可以通过以下code获取到完整的url,对于detail/{id}的处理也是一样有用
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
Object request = ContextManager.getRuntimeContext().get(REQUEST_KEY_IN_RUNTIME_CONTEXT);
......
if (request != null) {
if (stackDepth == null) {
......
if (IN_SERVLET_CONTAINER && IS_JAVAX && HttpServletRequest.class.isAssignableFrom(request.getClass())) {
......
//operationName: endpoint name,在此处更新自定上报的endpoint
String operationName = this.buildOperationName(method, httpServletRequest.getMethod(),
(EnhanceRequireObjectCache) objInst.getSkyWalkingDynamicField());
String operationName = ((RequestFacade) request).getRequestURI().toString();
AbstractSpan span = ContextManager.createEntrySpan(operationName, contextCarrier);
......
}
......
stackDepth = new StackDepth();
ContextManager.getRuntimeContext().put(CONTROLLER_METHOD_STACK_DEPTH, stackDepth);
}
......
}
}
忽略特定Endpoint
我们使用Skywalking过程中发现Endpoint列表有一些没有实际作用的Endpoint,例如:
我们需要在agent\config下面新建一个配置文件
apm-trace-ignore-plugin.config,如下所示,trace.ignore_path为所需要忽略的Path路径。
trace.ignore_path=httpAsyncClient/**,httpasyncclient/**,SpringAsync
trace.ignore_path路径配置规则支持匹配风格,比如: /path/*, /path/**, /path/?。匹配的Path路径将不会被记录。
注:
由于是可选插件,因此,默认情况下并没有激活,我们需要将如下图中的jar包拷贝或剪切到上面的plugins目录下并添加相关配置来激活该插件,即:
Skywalking-Agent启动流程
启动及加载插件流程
Skywalking-Agent启动流程主要分5步骤:
Skywalking-Agent原理剖析
Trace Segment Span 详解
首先说一下OpenTracing的规范,Skywalking按照OpenTracing将请求按照三个维度划分为Trace、Segment、Span三种模型,重点看一下Trace、Segment、Span这三种模型到底是什么。
- Trace: 表示一整条调用链,包括跨进程、跨线程的所有Segment的集合。
- Segment: 表示一个进程(JVM)或线程内的所有操作的集合,即包含若干个Span。
- Span: 表示一个具体的操作。Span在不同的实现里可能有不同的划分方式,这里介绍一个比较容易理解的定义方式:Entry Span:入栈Span。Segment的入口,一个Segment有且仅有一个Entry Span,比如HTTP或者RPC的入口,或者MQ消费端的入口等。Local Span:通常用于记录一个本地方法的调用。
- Exit Span:出栈Span。Segment的出口,一个Segment可以有若干个Exit Span,比如HTTP或者RPC的出口,MQ生产端,或者DB、Cache的调用等。
- Tag:表示一个Span的具体操作的属性信息,例如:一次RPC查询请求是一个Span操作,这次查询请求包含的属性有:method(GET/POST等)、url(如: api/vi/report/search)、instance(127.0.0.1:8080)等等这些信息需要绑定到Span中,随着Span出栈上报到Skywalking-OAP。
插件机制
Skywalking Agent提供了多种插件,实现不同框架的透明接入SkyWalking。Skywalking Agent插件体系主要设计三个流程:插件加载、插件匹配、插件拦截
插件体系的管理主要在
org.apache.skywalking.apm.agent.core.plugin中完成
插件的加载
插件加载的整体流程如下图 :
- PluginBootstrap#loadPlugins() 方法开始加载插件
- AgentClassLoader:作为类加载器对plugin下的所有jar进行加载
- PluginResourcesResolver:插件资源解析器,读取所有插件的定义文件(skywalking-plugin.def)
- PluginCfg: 插件定义配置,读取 skywalking-plugin.def 文件,生成插件定义(PluginDefine)
插件的匹配
我们提到SkyWalking Agent基于JavaAgent机制,实现应用透明接入SkyWalking
通过 JavaAgent 机制,我们可以在 #premain(String, Instrumentation) 方法里,调用Instrumentation#addTransformer(ClassFileTransformer) 方法,向 Instrumentation 注册ClassFileTransformer 对象,可以修改 Java 类的二进制,从而动态修改 Java 类的代码实现。
// org.apache.skywalking.apm.agent.SkyWalkingAgent
//以下为部分premain中的代码
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
//创建agentBuilder,并设置相关属性。
AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
nameStartsWith("net.bytebuddy.")
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameContains(".reflectasm."))
.or(nameStartsWith("sun.reflect"))
.or(allSkyWalkingAgentExcludeToolkit())
.or(ElementMatchers.isSynthetic()));
agentBuilder.type(pluginFinder.buildMatch()) //设置需要拦截的类
.transform(new Transformer(pluginFinder)) //设置Java类的修改逻辑。
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new RedefinitionListener())
.with(new Listener())
.installOn(instrumentation);
PluginFinder.pluginInitCompleted();
以上代码中agentBuilder在agentBuilder.type方法设置了需要拦截的类,transform方法设置了Java类的修改逻辑,及通过byte-buddy动态修改Java类的二进制。因为代码量大,逻辑复杂,我们跳过中间逻辑,直接到具体的匹配逻辑:
Skywalking-Agent实现了不同方式的匹配模式,具体的插件匹配在ClassMatch的实现类中完成
- NameMatch :基于完整的类名进行匹配,例如:"com.alibaba.dubbo.monitor.support.MonitorFilter" 。
- IndirectMatch :间接匹配接口。相比 NameMatch 来说,确实比较 “委婉” 。
- ClassAnnotationMatch :基于类注解进行匹配,可设置同时匹配多个。例如:"@RequestMapping"。
- HierarchyMatch :基于父类 / 接口进行匹配,可设置同时匹配多个。
- MethodAnnotationMatch :基于方法注解进行匹配,可设置同时匹配多个。目前项目里主要用于匹配方法上的org.skywalking.apm.toolkit.trace.@Trace 注解。
插件的拦截
在上文代码中我们已经设置需要拦截的类的方法以及类的修改逻辑,此部分要进行方法切面拦截。
拦截会涉及到以下元素 :
- 拦截切面 InterceptPoint
- 拦截器 Interceptor
- 拦截类的定义Define :一个类有哪些拦截切面及对应的拦截器
如下图是插件拦截涉及到的类。如图所示:
根据方法类型的不同,使用不同 ClassEnhancePluginDefine 的实现类。其中,构造方法和静态方法使用相同的实现类。
- AbstractClassEnhancePluginDefine :SkyWalking 类增强插件定义抽象基类。
- ClassEnhancePluginDefine :SkyWalking 类增强插件定义抽象类。
- AbstractClassEnhancePluginDefine 注重在定义( Define ),ClassEnhancePluginDefine 注重在增强( Enhance)。
因为代码量大,逻辑复杂,我们跳过中间逻辑,直接到具体的拦截器及逻辑:
插件拦截最终会到以下四个拦截器实现类中
- InstanceConstructorInterceptor ,构造方法拦截器接口。
- AroundInterceptor before-after拦截器接口
- StaticMethodsAroundInterceptor ,静态方法拦截器接口。
- InstanceMethodsAroundInterceptor ,实例方法拦截器接口。
执行步骤:
- InterceptorInstanceLoader#load(String, classLoader) 方法,加载实例方法拦截器。
- intercept(...) 方法,
- Before-After 方式拦截实例方法,代码如下 :
- 调用nstanceMethodsAroundInterceptor#beforeMethod(...) 方法,执行在实例方法之前的逻辑。
- org.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult ,方法拦截器执行结果。当调用MethodInterceptResult.defineReturnValue(Object) 方法,设置执行结果,并标记不再继续执行。
- 调用 Callable#call() 方法,执行原有实例方法。
- 调用 InstanceMethodsAroundInterceptor#handleMethodException(...) 方法,处理异常。
- 调用 InstanceMethodsAroundInterceptor#afterMethod(...) 方法,执行后置逻辑。
写在后面:
Skwyalking作为Apache旗下的优秀开源项目,其社区活跃,在国内一枝独秀,且中文文档丰富。Skywalking-Agent基于主流的Byte Buddy提供的字节码增强技术实现的自定义插件具有高性能,易维护,操作简单的特性。关于Skywalking,我会持续输出相关文章帮朋友深入了解Skwyalking,谢谢大家的关注
附件
Oceanbase(华为研发的企业级分布式数据库):
https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000000639550
Skywalking入门文档:
https://skywalking.apache.org/zh/2020-04-19-skywalking-quick-start/#