关于分布式:一文看懂分布式链路监控系统

10次阅读

共计 21273 个字符,预计需要花费 54 分钟才能阅读完成。

背景

传统的大型单体零碎随着业务体量的增大曾经很难满足市场对技术的需要,通过对将整块业务零碎拆分为多个互联依赖的子系统并针对子系统进行独立优化,可能无效晋升整个零碎的吞吐量。在进行零碎拆分之后,残缺的业务事务逻辑所对应的性能会部署在多个子系统上,此时用户的一次点击申请会触发若干子系统之间的互相性能调用,如何剖析一次用户申请所触发的屡次跨零碎的调用过程、如何定位存在响应问题的调用链路等等问题是链路追踪技术所要解决的问题。

举一个网络搜寻的示例,来阐明这样一个链路监控零碎须要解决的一些挑战。当用户在搜索引擎中输出一个关键词后,一个前端服务可能会将这次查问分发给数百个查问服务,每个查问服务在其本人的索引中进行搜寻。该查问还能够被发送到许多其余子系统,这些子系统能够解决敏感词汇、查看拼写、用户画像剖析或寻找特定畛域的后果,包含图像、视频、新闻等。所有这些服务的后果有选择地组合在一起,最终展现在搜寻后果页面中,咱们将这个模型称为一次残缺的搜寻过程。

在这样一次搜寻过程中,总共可能须要数千台机器和许多不同的服务来解决一个通用搜寻查问。此外,在网络搜寻场景中,用户的体验和提早严密相干,一次搜寻延时可能是因为任何子系统的性能不佳造成的。开发人员仅思考提早可能晓得整个零碎存在问题,但却无奈猜想哪个服务有问题,也无奈猜想其行为不良的起因。首先,开发人员可能无奈精确晓得正在应用哪些服务,随时都可能退出新服务和批改局部服务,以减少用户可见的性能,并改良性能和安全性等其余方面;其次,开发人员不可能是宏大零碎中每个外部微服务的专家,每一个微服务可能有不同团队构建和保护;另外,服务和机器能够由许多不同的客户端同时共享,因而性能问题可能是因为另一个利用的行为引起。

Dapper 简介

在分布式链路追踪方面,Google 早在 2010 年针对其外部的分布式链路跟踪零碎 Dapper,发表了相干论文对分布式链路跟踪技术进行了介绍(强烈推荐浏览)。其中提出了两个根本要求。第一,领有宽泛的覆盖面。针对宏大的分布式系统,其中每个服务都须要被监控零碎笼罩,即便是整个零碎的一小部分没有被监控到,该链路追踪零碎也可能是不牢靠的。第二,提供继续的监控服务。对于链路监控零碎,须要 7 *24 小时继续保障业务零碎的衰弱运行,保障任何时刻都能够及时发现零碎呈现的问题,并且通常状况下很多问题是难以复现的。依据这两个根本要求,分布式链路监控零碎的有如下几个设计指标:

  • 利用级通明

链路监控组件应该以根底通用组件的形式提供给用户,以进步稳定性,利用开发者不须要关怀它们。对于 Java 语言来说,办法能够说是调用的最小单位,想要实现对调用链的监控埋点势必对办法进行加强。Java 中对办法加强的形式有很多,比方间接硬编码、动静代理、字节码加强等等。利用级通明其实是一个比拟绝对的概念,透明度越高意味着难度越大,对于不同的场景能够采纳不同的形式。

  • 低开销

低开销是链路监控零碎最重要的关注点,分布式系统对于资源和性能的要求自身就很刻薄,因而监控组件必须对原服务的影响足够小,将对业务主链路的影响降到最低。链路监控组件对于资源的耗费主除了体现在加强办法的耗费上,其次还有网络传输和数据存储的耗费,因为对于链路监控零碎来说,想要监控一次申请势必会产生出申请自身外的额定数据,并且在申请过程中,这些额定的数据不仅会临时保留在内存中,在分布式场景中还会随同着该申请从上游服务传输至上游服务,这就要求产生的额定数据尽可能地少,并且在随同申请进行网络传输的时候只保留大量必要的数据。

  • 扩展性和开放性

无论是何种软件系统,可扩展性和开放性都是掂量其品质优劣的重要规范。对于链路监控零碎这样的根底服务零碎来说,上游业务零碎对于链路监控零碎来说是通明的,在一个规模较大的企业中,一个根底服务零碎往往会承载成千上万个上游业务零碎。每个业务零碎由不同的团队和开发人员负责,尽管应用的框架和中间件在同一个企业中有大抵的标准和要求,然而在各方面还是存在差别的。因而作为一个基础设施,链路监控零碎须要具备十分好的可扩展性,除了对企业中罕用中间件和框架的撑持外,还要可能不便开发人员针对非凡的业务场景进行定制化的开发。

数据模型

OpenTracing 标准

Dapper 将申请依照三个维度划分为 Trace、Segment、Span 三种模型,该模型曾经造成了 OpenTracing 标准。OpenTracing 是为了形容分布式系统中事务的语义,而与特定上游跟踪或监控零碎的具体实现细节无关,因而形容这些事务不应受到任何特定后端数据展现或者解决的影响。大的概念就不多介绍了,重点看一下 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 的调用等。

依照下面的模型定义,一次用户申请的调用链路图如下所示:

惟一 id

每个申请有惟一的 id 还是很必要的,那么在海量的申请下如何保障 id 的唯一性并且可能蕴含申请的信息?Eagleeye 的 traceId 设计如下:

依据这个 id,咱们能够晓得这个申请在 2022-10-18 10:10:40 收回,被 11.15.148.83 机器上过程号为 14031 的 Nginx(对应标识位 e)接管到。其中的四位原子递增数从 0 -9999,目标是为了避免单机并发造成 traceId 碰撞。

关系形容

将申请划分为 Trace、Segment、Span 三个档次的模型后,如何形容他们之间的关系?

从【OpenTracing 标准】一节的调用链路图中能够看出,Trace、Segment 能够作为整个调用链路中的逻辑构造,而 Span 才是真正串联起整个链路的单元,零碎能够通过若干个 Span 串联起整个调用链路。

在 Java 中,办法是以入栈、出栈的模式进行调用,那么零碎在记录 Span 的时候就能够通过模拟出栈、入栈的动作来记录 Span 的调用程序,不难发现最终一个链路中的所有 Span 出现树形关系,那么如何形容这棵 Span 树?Eagleeye 中的设计很奇妙,EagleEye 设计了 RpcId 来区别同一个调用链下多个网络调用的程序和嵌套档次。如下图所示:

RpcId 用 0.X1.X2.X3…..Xi 来示意,根节点的 RpcId 固定从 0 开始,id 的位数(”.” 的数量)示意了 Span 在这棵树中的层级,Id 最初一位示意了 Span 在这一层级中的程序。那么给定同一个 Trace 中的所有 RpcId,便能够很容易还原出一个实现的调用链:

- 0
  - 0.1
    - 0.1.1
    - 0.1.2
      - 0.1.2.1
  - 0.2
    - 0.2.1
  - 0.3
    - 0.3.1
      - 0.3.1.1
    - 0.3.2

跨过程传输

再进一步,在整个调用链的收集过程中,不可能将整个 Trace 信息随着申请携带到下个利用中,为了将跨过程传输的 trace 信息缩小到最小,每个利用(Segment)中的数据肯定是分段收集的,这样在 Eagleeye 的实现下跨 Segment 的过程只须要携带 traceId 和 rpcid 两个简短的信息即可。在服务端收集数据时,数据天然也是分段达到服务端的,但因为种种原因分段数据可能存在乱序和失落的状况:

如上图所示,收集到一个 Trace 的数据后,通过 rpcid 即可还原出一棵调用树,当呈现某个 Segment 数据缺失时,能够用第一个子节点代替。

数据埋点

如何进行办法加强(埋点)是分布式链路追零碎的关键因素,在 Dapper 提出的要求中能够看出,办法加强同时要满足利用级通明和低开销这两个要求。之前咱们提到利用级通明其实是一个比拟绝对的概念,透明度越高意味着难度越大,对于不同的场景能够采纳不同的形式。本文咱们介绍阿里的 Eagleye 和开源的 SkyWalking 来比拟两种埋点形式的优劣。

编码

阿里 Eagleeye 的埋点形式是间接编码的形式,通过中间件预留的扩大点实现。然而依照咱们通常的了解来说,编码对于 Dapper 提出的扩展性和开放性仿佛并不敌对,那为什 Eagleye 么要采纳这样的形式?集体认为有以下几点:

  1. 阿里有中间件的应用标准,不是想用什么就用什么,因而对于埋点的覆盖范围是无限的;
  2. 阿里有给力的中间件团队专门负责中间件的保护,中间件的埋点对于下层利用来说也是利用级通明的,对于埋点的笼罩是全面的;
  3. 阿里利用有接入 Eagleye 监控零碎的要求,因而对于可插拔的诉求并没有十分强烈。从下面几点来说,编码方式的埋点齐全能够满足 Eagleye 的须要,并且间接编码的形式在保护、性能耗费方面也是十分有劣势的。

字节码加强

相比于 Eagleye,SkyWalking 这样开源的分布式链路监控零碎,在开源环境下就没有这么好做了。开源环境下面临的问题其实和阿里团体外部的环境正好相同:

  1. 开源环境下每个开发者应用的中间件可能都不一样,想用什么就用什么,因而对于埋点的覆盖范围简直是有限的;
  2. 开源环境下,各种中间件都由不同组织或集体进行保护,甚至开发者还能够进行二次开发,不可能压服他们在代码中退出链路监控的埋点;
  3. 开源环境下,并不一定要接入链路监控体系,大多数集体开发者因为资源无限或其余起因没有接入链路监控零碎的需要。

从下面几点来说,编码方式的埋点必定是无奈满足 SkyWalking 的需要的。针对这样的状况,Skywalking 采纳如下的开发模式:

Skywalking 提供了外围的字节码加强能力和相干的扩大接口,对于零碎中应用到的中间件能够应用官网或社区提供的插件打包后植入利用进行埋点,如果没有的话甚至能够本人开发插件实现埋点。Skywalking 采纳字节码加强的形式进行埋点,上面简略介绍字节码加强的相干常识和 Skywalking 的相干实现。

对 Java 利用实现字节码加强的形式有 Attach 和 Javaagent 两种,本文做一个简略的介绍。

  • Attach

Attach 是一种绝对动静的形式,在阿尔萨斯(Arthas)这样的诊断系统中宽泛应用,利用 JVM 提供的 Attach API 能够实现一个 JVM 对另一个运行中的 JVM 的通信。用一个具体的场景举例:咱们要实现 Attach JVM 对一个运行中 JVM 的监控。如下图所示:

  1. Attach JVM 利用 Attach API 获取指标 JVM 的实例,底层会通过 socketFile 建设两个 JVM 间的通信;
  2. Attach JVM 指定指标 JVM 须要挂载的 agent.jar 包,挂载胜利后会执行 agent 包中的 agentmain 办法,此时就能够对指标 JVM 中类的字节码进行批改;
  3. Attach JVM 通过 Socket 向指标 JVM 发送命令,指标 JVM 收到后会进行响应,以达到监控的目标。

尽管 Attach 能够灵便地对正在运行中的 JVM 进行字节码批改,但在批改时也会受到一些限度,比方不能增减父类、不能减少接口、不能调整字段等。

  • Javaagent

Javaagent 大家应该绝对相熟,他的启动形式是在启动命令中退出 javaagent 参数,指定须要挂载的 agent:

java-javaagent:/path/agent.jar=key1=value1,key2=value2-jarmyJar.jar

Javaagent 在 IDE 的 Debug 模式、链路监控零碎等场景中宽泛应用。它的外围是在指标 JVM 执行 main 办法前执行 agent 的 premain 办法,以插入前置逻辑:

  1. 指标 JVM 通过 javaagent 参数启动后找到指定的 agent,执行 agent 的 premain 办法;
  2. agent 中通过 JVM 裸露的接口增加一个 Transformer,顾名思义它能够 Transform 字节码;
  3. 指标 JVM 在类加载的时候会触发 JVM 内置的事件,回调 Transformer 以实现字节码的加强。

和 Attach 形式相比,Javaagent 只能在 main 办法之前执行。然而在批改字节码时较为灵便,甚至能够批改 JDK 的外围类库。

  • 字节码加强类库

Java 提供了很多字节码加强类库,比方大家耳熟能详的 cglib、Javassist,原生的 Jdk Proxy 还有底层的 ASM 等。在 2014 年,一款名为 Byte Buddy 的字节码加强类库横空出世,并在 2015 年取得 Duke’s Choice award。Byte Buddy 兼顾高性能、易用、功能强大 3 个方面,上面是摘自其官网的一张常见字节码加强类库性能比拟图(单位: 纳秒):

上图中的比照项咱们能够大抵分为两个方面:生成疾速代码(办法调用、父类办法调用)和疾速生成代码(简略类创立、接口实现、类型扩大),咱们理所应当要优先选择前者。从数据能够看出 Byte Buddy 在纳秒级的精度下,在办法调用和父类办法调用上和基线根本没有差距,而位于其后的是 cglib。

Byte Buddy 和 cglib 有较为杰出的性能得益于它们底层都是基于 ASM 构建,如果将 ASM 也退出比照那么它的性能肯定是最高的。然而用过 ASM 的同学尽管不肯定能感触到它的高性能,但肯定能感触到它噩梦般的开发体验:

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("begin of sayhello().");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

Skywalking 案例剖析

介绍了这么多,上面联合 Skywalking 中应用 Byte Buddy 的案例和大家一起体验下字节码加强的开发过程,其中只简略介绍相干主流程代码,各种细节就不介绍了。Skywalking 为开发者提供了简略易用的插件接口,对于开发者来说不须要晓得怎么加强办法的字节码,只须要关怀以下几点:

  • 要加强哪个类的哪个办法?

Skywalking 提供了 ClassMatch,反对各种类、办法的匹配形式。包含类名、前缀、正则、注解等形式的匹配,除此之外还提供了与、或、非逻辑链接,以反对用户通过各种形式精确定位到一个具体的办法。咱们看一个插件中的代码:

这段逻辑示意须要加强不带 annotation1 注解,并且带有 annotaion2 注解或 annotaion3 注解的办法的字节码。ClassMatch 通过 Builder 模式提供用户流式编程的形式,最终 Skywalking 会将用户提供的一串 ClassMatch 构建出一个外部应用的类匹配逻辑。

  • 须要增加 / 批改什么逻辑?

以实例办法为例,Skywalking 提供了如下实例办法拦截器:

public interface InstanceMethodsAroundInterceptor {
    // 办法执行前置扩大点
    void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
                      MethodInterceptResult result) throws Throwable;
    // 办法执行后置扩大点
    Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
                       Object ret) throws Throwable;
    // 办法抛出异样时扩大点
    void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
                               Class<?>[] argumentsTypes, Throwable t);
}

开发者通过实现该接口即可对一个实例办法进行逻辑扩大(字节码加强)。办法参数列表中的第一个类型为 EnhancedInstance 的参数其实就是以后对象(this),Skywalking 中所有实例办法或构造方法被加强的类都会实现 EnhancedInstance 接口。

假如咱们有一个 Controller,外面只有一个 sayHello 办法返回 ”Hello”,通过 Skywalking 加强后,反编译一下它被加强后的字节码文件:

能够看到:

  1. Skywalking 在其中插入了一个名为_$EnhancedClassField_ws 的字段,开发者在某些场合能够正当利用该字段存储一些信息。比方存储 Spring MVC 中 Controller 的跟门路,或者 Jedis、HttpClient 链接中对端信息等。
  2. 原来的 syHello 办法名被批改了但仍保留下来,并且新生成了一个加强后的 sayHello 办法,动态代码块里将通过字节码加强后的 sayHello 办法存入缓存字段。
  • 加强的前置条件是什么?

在某些时候,并不是只有引入了对应插件就肯定会对相干的代码进行字节码加强。比方咱们想对 Spring MVC 的 Controller 进行埋点,咱们应用的是 Spring 4.x 版本,然而插件却是 5.x 版本的,如果间接对源码进行加强可能会因为版本的差异带来意料之外的问题。Skywalking 提供了一种 witness 机制,简略来说就是当咱们的代码中存在指定的类或形式时,以后插件才会进行字节码加强。比方 Spring 4.x 版本中须要 witness 这两个类:

如果粒度不够,还能够对办法进行 witness。比方 Elastic Search 6.x 版本中 witness 了这个办法:

意思就是 SearchHits 类中必须有名为 getTotalHits、参数列表为空并且返回 long 的办法。

除了下面的扩大点外,Skywalking 还反对对 jdk 外围类库的字节码加强,比方对 Callable 和 Runnable 进行加强已反对异步模式下的埋点透传。这就须要和 BootstrapClassLoader 打交道了,Skywalking 帮咱们实现了这些简单的逻辑。Skywalking Agent 局部整体的模型如下图所示:

左侧 SPI 局部是 Skywalking 裸露的插件标准接口,开发者依据这些接口实现插件。右侧 Core 局部负责加载插件并且利用 Byte Buddy 提供的字节码加强逻辑对利用中指定类和办法的字节码进行加强。

  • 源码剖析

介绍了 Skywalking 的插件模型后,上面从 Javaagent 的入口 premain 开始介绍下次要的流程:

public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
    // 1. 加载所有的 Skywalking 插件 
    // 插件实现了 Skywalking 接口标准,包含须要加强哪个类、须要怎么加强这两个因素
    final PluginFinder  = new PluginFinder(new PluginBootstrap().loadPlugins());

    // 2. 构建 ByteBuddy
    // ByteBuddy 提供了流式的 API 来指定 ByteBuddy 类库的行为(用于各种配置)final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));

    // 3. 构建 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(nameStartsWith("org.apache.skywalking.")
            .and(not(nameStartsWith("org.apache.skywalking.apm.toolkit."))))
        // 疏忽 Java 中的【Synthetic】// Synthetic 指所有存在于字节码文件中,然而不存在于源代码中的【结构】,即 JVM 帮咱们生产的货色
        // 比方外部类指向外部类实例的 this$0 字段;外部类拜访外部类的公有变量时 JDK 帮咱们生产的办法等等
        // JDK11 后引入了 NBAC 机制,引入了新的嵌套类组织形式,不再生成 Synthetic 办法
        .or(ElementMatchers.isSynthetic()));

    // 4. 解决 JDK9 的 module 个性,解决跨模块类拜访的问题

    // 5. 将 AgentBuilder 插桩在 Instrumentation 上
    agentBuilder
        // 依据插件的内容构建须要加强类的匹配器
        .type(pluginFinder.buildMatch())
        // Transformer 就是字节码加强逻辑的次要入口
        .transform(new Transformer(pluginFinder))
        // Retransform 模式:保留被批改的字节码
        // Redifine 模式:笼罩被批改的字节码
        .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
        // 一些监听器
        .with(new RedefinitionListener())
        .with(new Listener())
        // 将 AgentBuilder 插桩在 JVM 提供的 Instrumentation 上
        .installOn(instrumentation);
}

下面的流程次要做了两件事:

  1. 从指定的目录加载所有插件到内存中;
  2. 构建 Byte Buddy 外围的 AgentBuilder 插桩到 JVM 的 Instrumentation API 上,包含须要加强哪些类以及外围的加强逻辑 Transformer。
private static class Transformer implements AgentBuilder.Transformer {
    private PluginFinder pluginFinder;

    Transformer(PluginFinder pluginFinder) {this.pluginFinder = pluginFinder;}

    /**
     * 这个办法在类加载的过程中会由 JVM 调用(Byte Buddy 做了封装)* @param builder          原始类的字节码构建器
     * @param typeDescription  类形容信息
     * @param classLoader      这个类的类加载器
     * @param module           jdk9 中模块信息
     * @return                 批改后的类的字节码构建器
     */
    @Override
    public DynamicType.Builder<?> transform(final DynamicType.Builder<?> builder,
                                            final TypeDescription typeDescription,
                                            final ClassLoader classLoader,
                                            final JavaModule module) {LoadedLibraryCollector.registerURLClassLoader(classLoader);
        // 依据类信息找到针对这个类进行字节码加强的插件,可能有多个
        List<AbstractClassEnhancePluginDefine> pluginDefines = pluginFinder.find(typeDescription);
        if (pluginDefines.size() > 0) {
            DynamicType.Builder<?> newBuilder = builder;
            EnhanceContext context = new EnhanceContext();
            for (AbstractClassEnhancePluginDefine define : pluginDefines) {
                // 调用插件的 define 办法失去新的字节码
                DynamicType.Builder<?> possibleNewBuilder = define.define(typeDescription, newBuilder, classLoader, context);
                if (possibleNewBuilder != null) {newBuilder = possibleNewBuilder;}
            }
            // 返回加强后的字节码给 JVM,实现字节码加强
            return newBuilder;
        }
        return builder;
    }
}

JVM 在类加载的时候会触发 JVM 内置事件,回调 Transformer 传入原始类的字节码、类加载器等信息,从而实现对字节码的加强。其中的 AbstractClassEnhancePluginDefine 就是一个插件的形象。

public abstract class AbstractClassEnhancePluginDefine {
    public DynamicType.Builder<?> define(TypeDescription typeDescription, DynamicType.Builder<?> builder,
                                         ClassLoader classLoader, EnhanceContext context) throws PluginException {
        // witness 机制
        WitnessFinder finder = WitnessFinder.INSTANCE;

        // 通过类加载器找 witness 类,没有就间接返回,不进行字节码的革新
        String[] witnessClasses = witnessClasses();
        if (witnessClasses != null) {for (String witnessClass : witnessClasses) {if (!finder.exist(witnessClass, classLoader)) {return null;}
            }
        }

        // 通过类加载器找 witness 办法,没有就间接返回,不进行字节码的革新
        List<WitnessMethod> witnessMethods = witnessMethods();
        if (!CollectionUtil.isEmpty(witnessMethods)) {for (WitnessMethod witnessMethod : witnessMethods) {if (!finder.exist(witnessMethod, classLoader)) {return null;}
            }
        }

        // enhance 开始批改字节码
        DynamicType.Builder<?> newClassBuilder = this.enhance(typeDescription, builder, classLoader, context);

        // 批改实现,返回新的字节码
        context.initializationStageCompleted();
        return newClassBuilder;
    }

    protected DynamicType.Builder<?> enhance(TypeDescription typeDescription, DynamicType.Builder<?> newClassBuilder,
                                             ClassLoader classLoader, EnhanceContext context) throws PluginException {
        // 加强静态方法
        newClassBuilder = this.enhanceClass(typeDescription, newClassBuilder, classLoader);
        // 加强实例办法 & 构造方法
        newClassBuilder = this.enhanceInstance(typeDescription, newClassBuilder, classLoader, context);
        return newClassBuilder;
    }
}

通过 witness 机制检测满足条件后,对静态方法、实例办法和构造方法进行字节码加强。咱们以实例办法和构造方法为例:

public abstract class ClassEnhancePluginDefine extends AbstractClassEnhancePluginDefine {
    protected DynamicType.Builder<?> enhanceInstance(TypeDescription typeDescription,
                                                     DynamicType.Builder<?> newClassBuilder, ClassLoader classLoader,
                                                     EnhanceContext context) throws PluginException {
        // 获取插件定义的构造方法拦挡点 ConstructorInterceptPoint
        ConstructorInterceptPoint[] constructorInterceptPoints = getConstructorsInterceptPoints();
        // 获取插件定义的实例办法拦挡点 InstanceMethodsInterceptPoint
        InstanceMethodsInterceptPoint[] instanceMethodsInterceptPoints = getInstanceMethodsInterceptPoints();
        String enhanceOriginClassName = typeDescription.getTypeName();
        // 非空校验
        boolean existedConstructorInterceptPoint = false;
        if (constructorInterceptPoints != null && constructorInterceptPoints.length > 0) {existedConstructorInterceptPoint = true;}
        boolean existedMethodsInterceptPoints = false;
        if (instanceMethodsInterceptPoints != null && instanceMethodsInterceptPoints.length > 0) {existedMethodsInterceptPoints = true;}
        if (!existedConstructorInterceptPoint && !existedMethodsInterceptPoints) {return newClassBuilder;}

        // 这里就是之前提到的让类实现 EnhancedInstance 接口,并增加_$EnhancedClassField_ws 字段
        if (!typeDescription.isAssignableTo(EnhancedInstance.class)) {if (!context.isObjectExtended()) {
                // Object 类型、private volatie 修饰符、提供办法进行拜访
                newClassBuilder = newClassBuilder.defineField("_$EnhancedClassField_ws", Object.class, ACC_PRIVATE | ACC_VOLATILE)
                    .implement(EnhancedInstance.class)
                    .intercept(FieldAccessor.ofField("_$EnhancedClassField_ws"));
                context.extendObjectCompleted();}
        }

        // 构造方法加强
        if (existedConstructorInterceptPoint) {for (ConstructorInterceptPoint constructorInterceptPoint : constructorInterceptPoints) {
                // jdk 外围类
                if (isBootstrapInstrumentation()) {newClassBuilder = newClassBuilder.constructor(constructorInterceptPoint.getConstructorMatcher())
                        .intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.withDefaultConfiguration()
                                                                    .to(BootstrapInstrumentBoost
                                                                    .forInternalDelegateClass(constructorInterceptPoint
                                                                                              // 非 jdk 外围类                                                                                                         .getConstructorInterceptor()))));
                } else {
                    // 找到对应的构造方法,并通过插件自定义的 InstanceConstructorInterceptor 进行加强
                    newClassBuilder = newClassBuilder.constructor(constructorInterceptPoint.getConstructorMatcher())
                        .intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.withDefaultConfiguration()
                                                                    .to(new ConstructorInter(constructorInterceptPoint
                                                                                             .getConstructorInterceptor(), classLoader))));
                }
            }
        }

        // 实例办法加强
        if (existedMethodsInterceptPoints) {for (InstanceMethodsInterceptPoint instanceMethodsInterceptPoint : instanceMethodsInterceptPoints) {
                // 找到插件自定义的实例办法拦截器 InstanceMethodsAroundInterceptor
                String interceptor = instanceMethodsInterceptPoint.getMethodsInterceptor();
                // 这里在插件自定义的匹配条件上加了一个【不为静态方法】的条件
                ElementMatcher.Junction<MethodDescription> junction = not(isStatic()).and(instanceMethodsInterceptPoint.getMethodsMatcher());

                // 须要重写入参
                if (instanceMethodsInterceptPoint.isOverrideArgs()) {
                    // jdk 外围类
                    if (isBootstrapInstrumentation()) {newClassBuilder = newClassBuilder.method(junction)
                            .intercept(MethodDelegation.withDefaultConfiguration()
                                       .withBinders(Morph.Binder.install(OverrideCallable.class))
                                       .to(BootstrapInstrumentBoost.forInternalDelegateClass(interceptor)));
                    // 非 jdk 外围类 
                    } else {newClassBuilder = newClassBuilder.method(junction)
                                .intercept(MethodDelegation.withDefaultConfiguration()
                                           .withBinders(Morph.Binder.install(OverrideCallable.class))
                                           .to(new InstMethodsInterWithOverrideArgs(interceptor, classLoader)));
                    }
                // 不须要重写入参
                } else {
                    // jdk 外围类    
                    if (isBootstrapInstrumentation()) {newClassBuilder = newClassBuilder.method(junction)
                                .intercept(MethodDelegation.withDefaultConfiguration()
                                           .to(BootstrapInstrumentBoost.forInternalDelegateClass(interceptor)));
                    // 非 jdk 外围类                                                                  
                    } else {
                        // 找到对应的实例办法,并通过插件自定义的 InstanceMethodsAroundInterceptor 进行加强
                        newClassBuilder = newClassBuilder.method(junction)
                                .intercept(MethodDelegation.withDefaultConfiguration()
                                           .to(new InstMethodsInter(interceptor, classLoader)));
                    }
                }
            }
        }

        return newClassBuilder;
    }
}

依据是否要重写入参、是否是外围类走到不同的逻辑分支,大抵的加强逻辑大差不差,就是依据用户自定义的插件找到须要加强的办法和加强逻辑,利用 Byte Buddy 类库进行加强。

用户通过办法拦截器实现加强逻辑,然而它是面向用户的,并不能间接用来进行字节码加强,Skywalking 加了一个中间层来连贯用户逻辑和 Byte Buddy 类库。上述代码中的 XXXInter 便是中间层,比方针对实例办法的 InstMethodsInter:

InstMethodsInter 封装用户自定义的逻辑,并且对接 ByteBuddy 的外围类库,当执行到被字节码加强的办法时会执行 InstMethodsInter 的 intercept 办法(能够和下面反编译被加强后类的字节码文件进行比照):

public class InstMethodsInter {private static final ILog LOGGER = LogManager.getLogger(InstMethodsInter.class);

    // 用户在插件中定义的实例办法拦截器
    private InstanceMethodsAroundInterceptor interceptor;

    public InstMethodsInter(String instanceMethodsAroundInterceptorClassName, ClassLoader classLoader) {
        try {
            // 加载用户在插件中定义的实例办法拦截器
            interceptor = InterceptorInstanceLoader.load(instanceMethodsAroundInterceptorClassName, classLoader);
        } catch (Throwable t) {throw new PluginException("Can't create InstanceMethodsAroundInterceptor.", t);
        }
    }

    /**
     * 当执行被加强办法时,会执行该 intercept 办法
     *
     * @param obj          实例对象(this)* @param allArguments 办法入参
     * @param method       参数形容
     * @param zuper        原办法调用的句柄
    *  @param method       被加强后的办法的援用 
     * @return             办法返回值
     */
    @RuntimeType
    public Object intercept(@This Object obj, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper,
        @Origin Method method) throws Throwable {EnhancedInstance targetObject = (EnhancedInstance) obj;

        MethodInterceptResult result = new MethodInterceptResult();
        try {
            // 拦截器前置逻辑
            interceptor.beforeMethod(targetObject, method, allArguments, method.getParameterTypes(), result);
        } catch (Throwable t) {LOGGER.error(t, "class[{}] before method[{}] intercept failure", obj.getClass(), method.getName());
        }

        Object ret = null;
        try {
            // 是否中断办法执行
            if (!result.isContinue()) {ret = result._ret();
            } else {
                // 执行原办法
                ret = zuper.call();
                // 为什么不能走 method.invoke?因为 method 曾经是被加强后办法,调用就死循环了!// 能够回到之前的字节码文件查看起因,看一下该 intercept 执行的机会
            }
        } catch (Throwable t) {
            try {
                 // 拦截器异样时逻辑
                interceptor.handleMethodException(targetObject, method, allArguments, method.getParameterTypes(), t);
            } catch (Throwable t2) {LOGGER.error(t2, "class[{}] handle method[{}] exception failure", obj.getClass(), method.getName());
            }
            throw t;
        } finally {
            try {
                // 拦截器后置逻辑
                ret = interceptor.afterMethod(targetObject, method, allArguments, method.getParameterTypes(), ret);
            } catch (Throwable t) {LOGGER.error(t, "class[{}] after method[{}] intercept failure", obj.getClass(), method.getName());
            }
        }
        return ret;
    }
}

上述逻辑其实就是下图中红框中的逻辑:

Byte Buddy 提供了申明式形式,通过几个注解就能够实现字节码加强逻辑。

数据收集

下一步就是将收集到的 Trace 数据发送到服务端。为了将对主链路的影响降到最小,个别都采纳先存本地、再异步采集的形式。Skywalking 和 Eagleeye 的实现有所不同,咱们别离介绍:

存储

  • Eagleeye

鹰眼采纳并发环形队列存储 Trace 数据,如下图所示:

环形队列在很多日志框架的异步写入过程中很常见,其中次要包含读指针 take,指向队列中的最初一条数据;写指针 put,指向队列中下一个数据将寄存的地位,并且反对原子读、写数据。take 和 put 指针朝一个时钟方向挪动,当生产数据的速度超过生产速度时,会呈现 put 指针“追上”take 指针的状况(套圈),此时依据不同的策略能够抛弃行将写入的数据或将老数据笼罩。

  • Skywalking

Skywalking 在实现上有所区别,采纳分区的 QueueBuffer 存储 Trace 数据,多个生产线程通过 Driver 平均分配到各个 QueueBuffer 上进行数据生产:

QueueBuffer 有两种实现,除了基于 JDK 的阻塞队列外,还有一种一般数组 + 原子下标的形式。Skywalking 对于这两种实现有不同的应用场景:基于 JDK 阻塞队列的实现用在服务端,而一般数组 + 原子下标的形式用在 Agent 端,因为后者更加轻量,性能更高。对于后者这里介绍一下其中比拟乏味的中央。

  • 乏味的原子下标

一般的 Oject 数组是无奈反对并发的,但只有保障每个线程获取下标的过程是原子的,即可保障数组的线程平安。这须要保障:

  1. 多线程获取的下标是顺次递增的,从 0 开始到数组容量 -1;
  2. 当某个线程获取的下标超过数组容量,须要从 0 开始从新获取。

这其实并不难实现,通过一个原子数和取模操作一行代码就能实现下面的两个性能。但咱们看 Skywalking 是如何实现这个性能的:

// 提供原子下标的类
public class AtomicRangeInteger {
    // JDK 提供的原子数组
    private AtomicIntegerArray values;

    // 固定值 15
    private static final int VALUE_OFFSET = 15;

    // 数组开始下标,固定为 0
    private int startValue;

    // 数组最初一个元素的下标,固定为数组的最大长度 -1
    private int endValue;

    public AtomicRangeInteger(int startValue, int maxValue) {
        // 创立一个长度为 31 的原子数组
        this.values = new AtomicIntegerArray(31);
        // 将第 15 位设置为初始值 0
        this.values.set(VALUE_OFFSET, startValue);
        this.startValue = startValue;
        this.endValue = maxValue - 1;
    }

    // 外围办法,获取数组的下一个下标
    public final int getAndIncrement() {
        int next;
        do {
            // 原子递增
            next = this.values.incrementAndGet(VALUE_OFFSET);
            // 如果超过了数组范畴,CAS 重制到 0
            if (next > endValue && this.values.compareAndSet(VALUE_OFFSET, next, startValue)) {return endValue;}
        } while (next > endValue);
        return next - 1;
    }
}

Skywalking 用了一个长度固定为 31 的 JDK 原子数组的固定第 15 位进行相干原子操作,JDK8 中的原子数组利用 Unsafe 通过偏移量间接对数组中的元素进行内存操作,那为什么要这么做呢?咱们先将其称为 V1 版本,再来看看 V2 版本,这是 Skywalking 晚期版本应用的代码:

public class AtomicRangeInteger {

    private AtomicInteger value;
    private int startValue;
    private int endValue;

    public AtomicRangeInteger(int startValue, int maxValue) {this.value = new AtomicInteger(startValue);
        this.startValue = startValue;
        this.endValue = maxValue - 1;
    }

    public final int getAndIncrement() {
        int current;
        int next;
        do {
            // 获取以后下标
            current = this.value.get();
            // 如果超过最大范畴则从 0 开始
            next = current >= this.endValue ? this.startValue : current + 1;
            // CAS 更新下标,失败则循环重试
        } while (!this.value.compareAndSet(current, next));

        return current;
    }
}

肉眼可见这段 V2 版本的代码逻辑不如 V1 版本,因为在 V2 中获取以后值和 CAS 更新这两个步骤是离开的,并不具备原子性,因而并发抵触的可能性更高,从而导致循环次数减少;而应用 JDK 提供的 incrementAndGet 办法效率更高。再看下 V3 版本:

public class AtomicRangeInteger extends Number implements Serializable {
    // 用原子整型代替 V1 版本的原子数组
    private AtomicInteger value;
    private int startValue;
    private int endValue;

    public AtomicRangeInteger(int startValue, int maxValue) {this.value = new AtomicInteger(startValue);
        this.startValue = startValue;
        this.endValue = maxValue - 1;
    }

    public final int getAndIncrement() {
        int next;
        do {next = this.value.incrementAndGet();
            if (next > endValue && this.value.compareAndSet(next, startValue)) {return endValue;}
        }
        while (next > endValue);
        return next - 1;
    }
}

这个版本惟一的区别就是应用 AtomicInteger 代替原来的 AtomicIntegerArray 的第 15 位。还有最初一个最简略的 V4 版本,通过一个原子数和取模操作实现:

public class AtomicRangeInteger {

    private AtomicLong value;
    private int mask;

    public AtomicRangeInteger(int startValue, int maxValue) {this.value = new AtomicLong(startValue);
        this.mask = maxValue - 1;
    }

    public final int getAndIncrement() {return (int)(value.incrementAndGet() % mask);
    }
}

通过 Benchmark 压测数据来看看这几个版本的性能有什么差异,固定 128 线程,3 轮预热、5 轮正式,每轮 10s。

  • Skywalking 官网数据(数组大小 100):

本人在 mac 上测试的数据(数组大小 100):

本人在 mac 上测试的数据(数组大小 128):

Skywalking 官网显示通过原子数组的固定第 15 位操作的 V1 版本体现最好,而在我本人本机环境测试中 V3 版本通过原子整数代替的形式和 V1 版本有高有低,而原子数取模的性能是最高的。集体猜想 Skywalking 通过原子数组的固定第 15 位操作是为了进行缓存填充,测试后果和环境有比拟大的关系;而不应用原子数取模的起因是原子数的大小会有限递增。

传输

最初一步就是数据的传输,如下图所示:

Skywalking 提供了 GRPC 和 Kafka 两种数据传输方式,而鹰眼则先将数据存入本地日志中,再通过 agent 将数据采集到服务端。和 Skywalking 相比,用户能够间接在机器上查看 trace 日志,而 Skywalking 提供了日志插件以提供可插拔的本地 trace 存储性能。

从整体上来看,Skywalking 采取了埋点和中间件代码拆散的形式,在某种意义上实现了利用级通明,然而在前期保护的过程中中间件版本的降级须要配合插件版本的降级,在保护方面带来了一些问题。而 Eagleeye 编码方式的埋点由中间件团队保护,对于下层的利用也是通明的,更加适宜阿里团体外部的环境。

原文链接

本文为阿里云原创内容,未经容许不得转载。

正文完
 0