分布式链路追踪专栏——分布式链路追踪:Skywalking 探针模型设计

这些精彩的技术类型的体系化文章,后面我会放到公众号上,并集中在合集“分布式链路追踪专栏”中,欢迎大家去订阅我的公众号和视频号“架构随笔录”,大家可以订阅合集,这样更加方便喔,后面会出电子版本,更加方便。

另外本人也出版过“Spring Cloud Alibaba微服务架构实战派上下册”的技术类书籍,另外新书“RocketMQ分布式架构实战派”即将上架。

SkyWalking 是一个开源 APM 系统,包括针对 Cloud Native 体系结构中的分布式系统的监视,跟踪,诊断功能。核心功能如下:

  • 服务、服务实例、端点指标分析

  • 根本原因分析,在运行时分析代码

  • 服务拓扑图分析

  • 服务,服务实例和端点依赖性分析

  • 检测到慢速服务和端点

  • 性能优化

  • 分布式跟踪和上下文传播

  • 数据库访问指标。检测慢速数据库访问语句(包括 SQL 语句)

  • 报警

SkyWalking 目前是 Apache 顶级项目,作为这么优秀的开源项目,它的架构设计理念肯定会有很多值得我们借鉴。

本文会包含如下内容:

  • 基于 SDK 的探针模型

  • Java Agent 字节码原理

  • Skywalking 探针模型设计

  • Skywalking 探针采集数据如何收集

本篇文章适合人群:架构师、技术专家以及对全链路监控非常感兴趣的高级工程师。

基于SDK的探针模型

基于 SDK 的探针模型,其实在我们常规的框架技术中都有使用,比如事务原理,就是最典型的场景,里面用到了动态代理,并通过 AOP 切面来完成事务逻辑的植入。那么在传统的分布式链路追踪系统中,基于 Spring Cloud 的 ZipKin 就是采用的基于 SDK 的探针模型。

ZipKin 的探针具体分布在 instrumentation 项目中,会根据不同的组件形成不同的子项目,以便业务通过 SDK 依赖探针的时候,可以比较灵活的应用各种不同的组件。

但是如果只是 ZipKin 的探针还不能完成侵入,需要封装切面,通过 SDK 零侵入到业务代码,那么是否有组件已经实现了这种解决方案呢?答案就是 Spring Cloud Sleuth。

Spring Cloud Sleuth 为分布式跟踪提供了基于 Spring Boot 的自动配置。封装了 Brave 的跟踪程序库,Brave 库又是 openzipkin 的探针组件库。Sleuth 配置业务需要的所有分布式追踪能力。包括跟踪数据(span)的传输通道、保留多少跟踪数据(采样率)、是否发送 baggage 以及具备开关功能的链路探针。

Spring Cloud Sleuth 如何上手?

第一步:将 Sleuth 添加到应用工程中

spring.application.name 定义要规范,因为在 Sleuth 整合 ZipKin 中,链路追踪的隔离级别基本都是应用级。

Sleuth 整合 ZipKin,通信通道为 HTTP。

  1. <dependencyManagement> (1)
  2. <dependencies>
  3. <dependency>
  4. <groupId>org.springframework.cloud</groupId>
  5. <artifactId>spring-cloud-dependencies</artifactId>
  6. <version>${release.train.version}</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>
  12. <dependency> (2)
  13. <groupId>org.springframework.cloud</groupId>
  14. <artifactId>spring-cloud-starter-zipkin</artifactId>
  15. </dependency>

Sleuth 整合 ZipKin,通信通道为 Kafka 或者 RabbitMQ。配置属性 spring.zipkin.sender.type: kafka:

  1. <dependencyManagement> (1)
  2. <dependencies>
  3. <dependency>
  4. <groupId>org.springframework.cloud</groupId>
  5. <artifactId>spring-cloud-dependencies</artifactId>
  6. <version>${release.train.version}</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>
  12. <dependency> (2)
  13. <groupId>org.springframework.cloud</groupId>
  14. <artifactId>spring-cloud-starter-zipkin</artifactId>
  15. </dependency>
  16. <dependency> (3)
  17. <groupId>org.springframework.amqp</groupId>
  18. <artifactId>spring-rabbit</artifactId>
  19. </dependency>

第二步:覆盖 Zipkin 的自动配置

通过 ZipkinAutoConfiguration.REPORTER_BEAN_NAME 和 ZipkinAutoConfiguration.SENDER_BEAN_NAME,重新定义 Bean,并覆盖原有 Zipkin 对应的 bean 的定义。

可以这样理解,Spring Cloud Sleuth 是在 Zipkin 的升级版,可以兼容 Zipkin。

Java Agent字节码原理

Java Instrumentation

从 Java 5 开始,JDK 中新增了一个 java.lang.instrument.Instrumentation 类,它提供在运行时重新加载某个类的的 class 文件的 API。

Java Agent

通过操作 Instrumentation 的 API 就可以实现不重启服务对单个类进行简单的修改。

通常有两种方式拿到 Instrumentation 对象:

  • 在 JVM 启动时指定 agent,Instrumentation 对象会通过 agent 的 premain 方法传递。

  • 在 JVM 启动后通过 JVM 提供的机制加载 agent,Instrumentation 对象会通过 agent 的 agentmain 方法传递。

Skywalking 就是通过在 JVM 启动时指定 agent,因为这样其实对于业务来说会减少一些不确定性,从用户角度,谁也不希望自己的代码被毫无感知的情况下被侵入,如果实在运行期间侵入,至少在启动的过程中能够预判一些不确定性,具体详情代码请参考:org.apache.skywalking.apm.agent.SkyWalkingAgent。

业务应用在 Skywalking 中如何被零侵入,配置属性文件 env.properties,内容如下,注意要将 skywalking-agent.jar 放到指定的路径上。

  1. JAVA_HOME="/data/java/jdk1.8.0_161"
  2. JAVA_PARAMS="$JAVA_PARAMS -DSW_AGENT_NAME=application-name"
  3. JAVA_PARAMS="$JAVA_PARAMS -javaagent:/data/skywalking/skywalking-agent.jar"

然后写入 shell 脚本,并通过脚本启动程序的 jar 包。

作为一个中间件框架,Skywalking 不可能自己去造一个轮子来实现 Java Agent 字节码原理,所以它也利用了轮子 byte-buddy。

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,并且无需编译器的帮助。除了 Java 类库附带的代码生成实用程序之外,Byte Buddy 允许创建任意类,而且不限于实现创建运行时代理的接口。此外,Byte Buddy 提供了一个方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

为了使用字节码框架 Buddy,我们可以不需要理解 Java 字节代码或类文件格式。相反,Byte Buddy 的 API 的目标是代码简洁且易于理解。在 Buddy 中字节码仍然是完全可自定义的,甚至可以是自定义的字节代码。此外,API 是零侵入的,因此,Byte Buddy 不会在它创建的类中留下任何被侵入的痕迹。

Skywalking 如何应用和落地 Byte Buddy?答案就在如下模块包:

  • apm-agent

  • apm-agent-core

SkyWalkingAgent 类中定义了 premain 函数,传参:agentArgs 和 Instrumentation,并且通过插件加载器 PluginFinder 和 PluginBootstrap,从插件生态模块中加载各种资源。如何加载,答案就在 skywalking-plugin.def 文件。

每个插件模块的 resources 目录下面都会有一个文件 skywalking-plugin.def,文件内容如下,当然下面这个是 dubbo-2.7.x-plugin 的举例。

dubbo=org.apache.skywalking.apm.plugin.asf.dubbo.DubboInstrumentation

找到插件目录路径之后,再通过 Byte Buddy 框架构建 AgentBuilder,这个类是框架的核心,然后整个字节码植入都是这个类的功劳。

从如下的代码片段可以理解,插件需要匹配,然后通过 Transformer 将插件流转换为字节码,并植入到业务应用中。

  1. agentBuilder.type(pluginFinder.buildMatch())
  2. .transform(new Transformer(pluginFinder))
  3. .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
  4. .with(new Listener())
  5. .installOn(instrumentation);

Skywalking探针模型设计

Skywalking 的核心领域概念包括:探针、OAP 平台、UI 和存储,本文作者就会探讨探针模型及其架构。

探讨探针之前,我们得先了解探针生态,Skywalking 是如何支持探针生态的。

领域核心模型

Instrumentation

Skywalking 通过 Instrumentation 定义切面点,包括:需要增强的类域和方法域,比如要侵入 Dubbo,就会定义要侵入的 ENHANCE_CLASS 为 org.apache.dubbo.monitor.support.MonitorFilter;定义构造方法拦截点,比如 ConstructorInterceptPoint,定义实例方法拦截点,比如 InstanceMethodsInterceptPoint。当然也会有静态方法拦截点,比如 StaticMethodsInterceptPoint。

Interceptor

就如 AOP 一样,有了拦截点,就需要业务切面,所以 Skywalking 就通过拦截器来完成链路切面的功能。封装了 beforeMethod、afterMethod 和 handleMethodException 方法;这里再列举 DubboInterceptor 的功能逻辑,通过拦截 Dubbo RPC 请求中的 Invoker 和 Invocation,获取整个 RPC 请求的元数据,包括 URL 元数据,当前 Consumer 和 Provider 的服务信息,然后通过通过 RpcContext,这个 Dubbo 自己封装的上下文类(线程本地变量存储全本次请求的线程范围的信息),植入基于 Span 的链路模型数据。在切面点对应的业务方法和构造函数的前后植入链路模型,并通过 Entrance 和 Exit,来标注探针组件的链路数据的走向,完成链路数据的串联,再通过 TraceID、SpanID 以及 SegmentID 来形成树状的链路数据,并最终落地存储,关于链路模型,作者将会在另外一篇文章:《分布式链路追踪:Skywalking 的链路模型设计》做详细的分析。

从具体的细节来分析基于 SDK 的探针模型,哈哈作者最喜欢图解来分析,再次展示模型图。

 Skywalking探针采集数据如何收集

既然是分布式链路平台,那么数据收集肯定的核心功能,在 Skywalking 中,数据是通过探针侵入业务,然后通过通信通道传输到 OAP Server 端。针对每一个探针,其实收集的逻辑都不太一样,这里我就重点分析下常用技术中间件的探针数据是如何收集的。

gateway-2.1.x-plugin

Spring Cloud Gateway 是一个非常火的服务治理网关,所以 Skywalking 肯定是要支持对它的性能数据的采集。

  • 定义探针 FilteringWebHandlerInstrumentation,用于增强 org.springframework.cloud.gateway.handler.FilteringWebHandler,拦截 FilteringWebHandler 中以 handle 开头的方法。

  • 定义拦截器 FilteringWebHandlerInterceptor,在 beforeMethod 方法中植入需要采集的数据,从 allArguments 参数中,获取到当前网关路由的 ServerWebExchange,然后拼装 operationName,例如: operationName + route.getId()。

  • HttpClientOperationsSendInterceptor,创建 AbstractSpan 并设置组件类型为 ComponentsDefine.SPRING_CLOUD_GATEWAY,最后调用 ContextManager.stopSpan(span) 存储链路数据。

dubbo-2.7.x-plugin

Dubbo 框架肯定是得支持的,如何支持?

定义探针 DubboInstrumentation,并增强类 org.apache.dubbo.monitor.support.MonitorFilter,这个类是 Dubbo 服务治理的核心类,拦截的方法必须是要含有 invoke 字段。

拦截器 DubboInterceptor,通过 beforeMethod,获取到 Invoker、Invocation 和 RpcContext,然后再区分是消费者还是提供者,如果是消费者,那么就是 ExitSpan,如果是提供者就是 EntrySpan,从链路数据流向来看,消费者是出去,而提供者流量是流入。createExitSpan 和 createEntrySpan,就会植入一些性能采集指标,比如 RT 等。

jedis-2.x-plugin

定义探针-JedisInstrumentation,这个探针用于植入类 redis.clients.jedis.Jedis。

定义一批特殊处理的拦截器——最常规的拦截器就是 JedisMethodInterceptor,并通过 beforeMethod、afterMethod 和 handleMethodException 采集链路信息。

其他组件我这边就不一一列举了,可以说基本都是一样的,都是通过基于字节码的 AOP 技术来植入链路采集数据。

关于 Skywalking 探针的链路数据采集我们需要知道底层核心,我在这里就列举下:

  • StaticMethodsInter

  • ConstructorInter

  • InstMethodsInter

StaticMethodsInter

用于拦截类实例方法的实际字节 buddy 拦截器。这个类用于连接 Byte-buddy 和 Skywalking plugin。

ConstructorInter

用于拦截构造函数方法的实际字节 buddy 拦截器,然后通过自定义 intercept 方法逻辑,调用拦截器的 onConstruct 逻辑,完成基于构造函数的请求拦截。

InstMethodsInter

用于拦截实例方法的实际字节 buddy 拦截器,自定义 intercept 方法,然后再方法中国封装拦截器对 beforeMethod、handleMethodException 和 afterMethod 这三个方法的调用。

总结

本人是 Skywalking 的长期使用者和爱好者,也落地了 Skywalking 的全链路监控,但是由于时间有限,有些内容没来的及细讲,但是后续如果有时间,会出一期专题,专门去讲 Skywalking 的原理及技术栈。

游侠-胡弦——一名对技术孜孜不倦的高级码农,欢迎关注本人的微信公众号——架构随笔录。