写点什么

vivo 调用链 Agent 原理及实践

发布于: 2020 年 11 月 23 日
vivo 调用链 Agent 原理及实践

一、项目背景


2017 年,vivo 互联网研发团队认为调用链系统对实际业务具有较大的价值,于是开始了研发工作。3 年的时间,调用链系统整体框架不断演进……本文将介绍 vivo 调用链系统 Agent 技术原理及实践经验。


vivo 调用链系统的研发,始于对 Google 的《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》这篇经典文章的学习,我们调研了行业内相关的系统:鹰眼(EagleEye)、分布式服务跟踪系统(SGM)、实时应用监控平台(CAT)、ZipkinPinPointSkyWalking 、博睿等。通过研究分析,我们重点参考学习了  SkyWalking 的埋点方式。接下来我将逐步介绍 Agent 中用到的一些重点技术。

二、调用链入门

1、整体架构


为了方便读者先有个整体的认知,我们先看下图 vivo 当前调用链系统整体的架构,Agent 承担了调用链数据的埋点及采集工作,当然这个是当前最新架构,相比项目之初有一些变化。



2、核心领域概念


调用链内部有两个非常核心的概念,分别是 trace 和 span,都源自最初 google 介绍 dapper 的文章,无论是国内大厂的调用链产品还是开源调用链的实现,领域模型一般都借鉴了这两个概念,因此如果想很好理解调用链,这两个概念首先需要有清晰的理解。



上图模拟了一个简单的场景:


一次请求从手机端发起,路由到后端后首先由 nginx 转发给服务 A 来处理,服务 A 先从数据库里查询数据,简单处理后继续向服务 B 发起请求,服务 B 处理完成将结果返回给 A,最终手机端成功接收到响应,整个过程是同步处理的。


结合上面模拟的场景,我给出定义:


Trace:相同业务逻辑的调用请求经过的分布式系统完整链路。


我们用 traceId 标志具体某一次请求调用,当然 traceId 是分布式唯一的,它串联了整个链路,后文中会介绍 traceId 的生成规则。注意,相同业务逻辑的请求调用,可以理解为发起调用的入口是同一个接口。由于程序逻辑中存在 if/else 等分支结构,某一次调用不能完整反映出一个 trace 链路,只有相同业务逻辑的多次请求调用触达的链路,合成后才算是一个完整的 trace 链路。


Span:某一次局部请求调用。


一次调用会产生多个 span,这些 span 组成一个不完整的 trace;span 需要标注本次调用所在调用链路(即 span 数据中要有 traceId 信息),以及其所在链路中的层级;spanId 同一层级原子自增,跨层级将拼接“.”以及子序列;例如上图中 span 1.1 和 1.2 属同一层级,span 1 与 1.1 或者 1.2 是跨层级;B 与 D 之间的通信是 rpc 调用,这个过程有 4 个步骤:B 发起调用,接着 D 接收到请求,然后 D 将结果返回给 B,然后 B 接收到 D 的响应。这 4 个步骤组成一个完整的 span,所以 B 和 D 各只有这个 span 的一半,因此 spanId 需要跨进程传递,后面将介绍如何传递。


3、调用链中数据采集基本逻辑


vivo 调用链系统的定位是服务层监控,是 vivo 互联网监控体系中的重要一环。像服务异常、rpc 调用耗时、慢 sql 等都是基本的监控点。如果埋点采集的数据需要满足调用耗时监控,那么至少在 rpc 调用及慢 sql 监控场景下,将以 AOP 的形式来实现埋点数据采集。vivo 调用链 Agent 除了 JVM 的指标采集直接使用了 java.lang.management.ManagementFactory 外,其他都是以类似 AOP 的形式来实现的。以下为伪代码:


beginDataCollection(BizRequest req);try{ runBusiness();// 业务代码执行}catch(Throwable t){ recordBizRunError(Throwable t); throw t;}finally{ endDataCollection(BizResponse resp);}
复制代码

三、基础技术原理


调用链 Agent 开发,涉及到了大量的技术点,以下挑一些关键的来简单介绍。

1、分布式 ID(traceId)的生成规则

调用链中的 traceId 扮演着非常重要的角色,在上面的章节中提到了它用于串联多个进程间分散生成的 span,除此之外,Agent 端采样控制、入口服务识别、后端 flink 关键指标计算、用户查询完整调用链路、全局业务日志串联以及 Kafka、HBase 和 ES 数据散列等都依赖于它。vivo 调用链系统 traceId 是长度为 30 的字符串,下图中我对有特殊含义的分段进行了着色。



  • 0e34:

16 进制表示的 Linux 系统 PID,用于单机多进程的区分,做到同一个机器不同的进程 traceId 不可能重复。


  • c0a80001:

16 进制的 ipv4 的表示,可以识别生成这个 traceId 的机器 ip,比如 127.0.0.1 的 16 进制表示过程为 127.0.0.1->127 0 0 1->7f 00 00 01。


  • d:

代表着 vivo 内部的业务运行环境。一般我们会区分线下和线上环境,线下又可分开发、测试、压测等等环境,而这个 d 代表着某个线上的环境。


  • 1603075418361:

毫秒时间戳。用于增加唯一性,可通过此读取入口请求发生的时间。


  • 0001:

  原子自增的 ID,主要用于分布式 ID 增加唯一性,当前的设计可容忍单机每秒 10000*1000=1 千万的并发。


2、全链路数据传递能力

全链路数据传递能力是 vivo 调用链系统功能完整性的基石,也是 Agent 最重要的基础设施,前面提到过的 spanId、traceId 及链路标志等很多数据传递都依赖于全链路数据传递能力,系统开发中途由于调用链系统定位更加具体,当前无实际功能依赖于链路标志,本文将不做介绍。项目之初全链路数据传递能力,仅用于 Agent 内部数据跨线程及跨进程传递,当前已开放给业务方来使用了。


一般 Java 研发同学都知道 JDK 中的 ThreadLocal 工具类用于多线程场景下的数据安全隔离,并且使用较为频繁,但是鲜有人使用过 JDK 1.2 即存在的 InheritableThreadLocal,我也是从未使用过。


InheritableThreadLocal 用于在通过 new Thread()创建线程时将 ThreadLocalMap 中的数据拷贝到子线程中,但是我们一般较少直接使用 new Thread()方法创建线程,取而代之的是 JDK1.5 提供的线程池 ThreadPoolExecutor,而 InheritableThreadLocal 在线程池场景下就无能为力了。你可以想象下,一旦跨线程或者跨线程池了,traceId 及 spanId 等等重要的数据就丢失不能往后传递,导致一次请求调用的链路断开,不能通过 traceId 连起来,对调用链系统来说是多么沉重的打击。因此这个问题必须解决。


其实跨进程的数据传递是容易的,比如 http 请求我们可以将数据放到 http 请求的 header 中,Dubbo 调用可以放到 RpcContext 中往后传递,MQ 场景可以放到消息头中。而跨线程池的数据传递是无法做到对业务代码无侵入的,vivo 调用链 Agent 是通过拦截 ThreadPoolExecutor 的加载,通过字节码工具修改线程池 ThreadPoolExecutor 的字节码来实现的,这个也是一般开源的调用链系统不具备的能力。


3、javaagent 介绍


在今年初,调用链在 vivo 互联网业务中的接入率达 94%之高,这个数据是值得自豪的,因为项目之初自我安慰的错误认知是调用链这种大数据系统无需服务于全部互联网业务,或者当初认为服务于一些核心的业务系统即可。


个人认为能达到这么高的接入率,至少有两个核心的底层逻辑:

  • 之一是 Agent 使用了 javaagent 技术,做到业务方无侵入无感知的接入;

  • 之二是 Agent 的稳定性得到了互联网业务线的认可,从 17 年项目伊始到 19 年底只有过一次与之相关的业务端故障复盘。


然而一切并不是一开始就如此顺利的,一开始 Agent 埋点模块需要侵入业务逻辑,第一个版本对 SpringMVC 和 Dubbo 进行了埋点,需要用户在代码中配置 mvc filter 和 dubbo filter,效率极其低,对那个极力配合第一版试用的业务线的兄弟,现在依旧心怀感恩。后面我们就毅然决然换了 javaagent 方案,下面我介绍下 javaagent 技术。


javaagent 是一个 JVM 参数,调用链通过这个参数实现类加载的拦截,修改对应类的字节码,插入数据采集逻辑代码。


开发 javaagent 应用需要掌握以下知识点:

  • javaagent 参数使用;

  • 了解 JDK 的 Instrumentation 机制( premain 方法、 ClassFileTransformer 接口)及 MANIFEST.MF 文件中关于 Premain-Class 参数配置;

  • 字节码工具的使用;

  • 类加载隔离技术原理及应用。


下面我逐个说明:


(1)javaagent 配置示例如下:


java -javaagent:/test/path/my-agent.jar myApp.jar
复制代码


此处 javaagent 参数配置的 jar(这里是 my-agent.jar)是由 AppClassLoader 来加载的,后续章节有介绍。


(2)所谓 Instrumentation 机制指的是通过 jdk 中 java.lang.instrument.Instrumentation 与 java.lang.instrument.ClassFileTransformer 这两个接口协同进行类的字节码替换,当然替换逻辑的入口在于拦截类的加载。Java 的 jar 中有一个标准的配置文件 META-INF/MANIFEST.MF,可以在文件中添加 k-v 配置。这里我们需要配置的 k 是 Premain-Class,v 是一个全限定名的 Java 类,这个 Java 类必须有一个方法是 public static void premain(String agentOps,Instrumention instr)。这样当你使用 Java 命令启动可执行 jar 时,就会执行到这个方法,我们需要在这个方法里完成字节码转换逻辑的注册,当匹配到特定的类时,就会执行字节码转换逻辑,注入你的埋点逻辑。




(3)MANIFEST.M 文件 中的配置


图中 Can-Retransform-Classes 参数意为是否允许 jvm 执行转换逻辑,可以阅读 Instrumentation 这个类中的 JavaDoc 加深理解。Boot-Class-Path 参数用于指定后面的 jar 中的类由 BootstapClassLoader 来加载。


(4)关于字节码工具的使用,vivo 调用链 Agent 用到了以下操作:

  • 修改指定方法的逻辑(嵌入类似 AOP 的逻辑);

  • 给类增加实例字段;

  • 让类实现某个特定接口;

  • 获取类实例字段与静态字段值、读取父类与接口等等读操作。

4、核心模型数据结构


在上文中我们讲到 span 含义为一次局部调用,这次调用将分别在服务调用双方产生半个 span 的数据,在内存中半个 span 的定义(17 年底的定义)如下:


public class Span { final transient AtomicInteger nextId = new AtomicInteger(0);//用于同一层级的spanId自增 String traceId; String spanId; long start; long end; SpanKind type;//Client,Server,Consumer,Producer Component component;//DUBBO,HTTP,REDIS...... ResponseStatus status = ResponseStatus.SUCCESS; int size;//调用结果大小 Endpoint endpoint;//记录ip、port、http接口、redis命令 List<Annotation> annotations;//记录事件,比如sql、未捕获异常、异常日志 Map<String, String> tags;//记录标签tag}
复制代码


看了上面的定义你就能大致知道调用链的各个功能是如何计算出来的了。

5、各组件埋点详情


这里我罗列了截止 2019 年底 vivo 调用链 Agent 埋点覆盖的组件,及埋点的具体位置。据了解,今年 vivo 调用链系统进入 3.0 版本后,新增了超过 8 个埋点组件,采集到的数据越来越丰富了。


6、半自动化埋点能力介绍


经过对埋点能力较深的封装后,Agent 中新增加一个组件的埋点是非常高效的,一般情况步骤如下,可以结合上图来了解:


  • 对需要埋点的第三方框架/组件核心逻辑执行流程进行 debug,了解其执行过程,选定合适的 aop 逻辑切入点,切入点的选取要易于拿到 span 中各个字段的数据;


  • 创建埋点切面类,继承特定的父类,实现抽象方法,在方法中标注要切入埋点的方法,以及用于实现 aop 逻辑的 interceptor;


  • 实现 interceptor 逻辑,在 openSpan 方法中获取部分数据,在 closeSpan 中完成剩余数据的获取;


  • 设置/控制 interceptor 逻辑所在类可以被 Thread.currentThread().getContextClassLoader()这个类加载器加载到,然后打开此组件埋点逻辑生效的开关。


可见,当前新增一个组件的埋点是非常容易的,2018 年 2.0 版本项目中期的目标是全自动化,期望通过配置即可实现部分类的自动生成,尽可能少的代码,新增埋点更加高效,但是由于个人精力不足的原因,未能持续优化来实现。

7、span 数据流图


我们再来看下 span 从产生到发送到 kafka 的完整生命周期。



图中可以看出,在生成完整的(closeSpan()完成调用)半个(参考调用链入门之核心领域概念小节)span 后,会首先缓存在 ThreadLocal 空间。在完成本线程全部逻辑处理后,执行 finish()转储到 disruptor,再由 disruptor 的消费者线程定时刷到 kafka 的客户端缓存,最终发送到 kafka 队列。


在做内部分享的时候,这里有两个问题有被问到,一是 kafka 客户端自身有缓存,为啥中间还要有个 disruptor,第二个是执行 finish 的时机。这里原因也很简单,首先因为 disruptor 是无锁不阻塞并且队列容量可限定的,jdk 中的线程安全的要么是阻塞的要么是无法限制初始容量的,kafka 客户端的缓冲区显然也不满足这个条件,我们决不可阻塞业务线程的执行。第二个问题用栈(LinkedList)这种数据结构来解决即可,线程执行到第一个埋点切点处执行 openSpan 时进行压栈,执行 closeSpan 时执行弹栈,当栈中无数据时即应当执行 finish。

8、丰富的内部治理策略



项目之初的主要目标是业务的接入量及产品能力的适用性,不会太多考虑内部治理,但是数据量大了后必然要更多的考虑自身的可治理性了,上图中展示了截止 2018 年底 Agent 中的主要的内部治理能力。下面我逐个介绍下各项治理能力的背景。


(1)配置广播:

置下发能力是其他各项治理能力的基石,Agent 在 premain 方法执行时会去 vivo 配置中心主动拉取配置,如果配置中心配置有变动,也会主动将配置推送下来。另外,Agent 内部依赖配置的地方众多,内部配置的生效也是基于 JDK 中的 Observer 监听机制实现配置分发的。


(2)日志策略:

在 2017 年的时候,vivo 互联网业务方兴未艾,统一日志中心的能力较弱,大量的异常日志会对日志中心造成冲击,因此需要做异常流控。在异常情况下减少异常堆栈的打印,并且 Agent 还要能响应业务的需求采集指定级别的业务日志,比如由于日志打印规范不明,日志打印混乱的原因,有业务希望将 warn 或者某个类的 info 级别的日志,采集到调用链系统中供问题排查。另外,Agent 自身是需要打印日志的,这个日志打印的代码在字节码增强后是嵌入到三方框架中的,也就是说业务逻辑执行到三方框架中时可能造成执行变慢,影响业务性能,因此需要异步输出日志。最后需要提到的一点是,日志的打印在 Agent 中是自己实现的,为了避免与业务方使用的日志框架造成类冲突,是不能使用第三方日志框架的。


(3)采样策略:

在 2018 年初,接入不到 200 个服务时,采集的 span 数据已经占据了 10 台 Kafka 物理机的容量了,必须进行流量控制,采样是重点。但是当初的采样逻辑会带来新的问题,就是导致业务 tps 不精准,因此后面将 tps 等数据独立进行采集了。


(4)降级:

这个容易理解,就是要支持动态控制不采集某个服务的数据,或者不采集某个组件的数据,或者业务方希望在活动的时候关闭调用链。


(5)异常流控:

调用链对日志组件进行了埋点,也能拦截到业务方未捕获的异常,会将这些数据采集并存储到调用链系统中,如果太多异常了,系统自身也撑不住,因此这里的异常流控指以一定频率控制相同异常不传递到后端。


(6)全流程 span 流转监控:

Agent 中会监控 span 的流转过程进行计数(产生、入队、出队、入 Kafka 成功/失败、数据丢失),当发现数据丢失时,可选择调大内存无锁队列的容量或者调小 Kafka 发送间隔,当发现发送 Kafka 失败时,意味着网络或者 kafka 队列出了问题。


(7)数据聚合频率控制:

在 18 年中,据评估 span 原始数据后期将会增长到每天 1500 亿条,调用链系统无足够资源处理这么大规模数据量,因此我们很快在 Agent 端实现了端的数据聚合能力,将初步聚合后的数据丢给 flink 做最终的计算,减少 Kafka 和大数据集群的压力。


(8)JVM 采样和 kafka 发送频率控制:

Agent 会定时采集 JVM 指标,比如 gc、cpu、JVM 使用内存、各状态线程数等等,在经过 flink 计算后会在页面显示出折线图,这个采集间隔是严格的 5s,为了控制数据量,需要做到动态调控采集间隔。另外 Agent 端生成 span 数据首先缓存到了内存无锁队列,然后定时批量发送 Kafka,为了兼顾告警的实时性及 Agent 端的 cpu 的损耗,这个频率默认是 200ms,同时也支持远程调控。

四、Agent 稳定性保障


上文提到过,当前 Agent 在几千个应用中接入率达 94%之高,个人认为有一个重要原因是其稳定性被业务方认可。那么如果要保障自身的稳定性,不对业务造成影响,对于调用链 Agent 来说,首先一定要尽可能的减少对业务线程执行的干扰,其次要尽可能多的考虑到边界问题

1、全程不阻塞业务流程


减少对业务线程执行干扰的出发点在于不阻塞业务线程,我们来梳理下对业务线程的阻塞点,然后逐个介绍下处理办法。


(1)线程阻塞点 1——日志打印:

disruptor 处理。使用 disruptor 对日志进行无阻塞缓存,同时坚持令可直接丢弃日志也不要阻塞的原则。


(2)线程阻塞点 2——埋点逻辑:


  • 措施 1:span 生成时缓存到 ThreadLocal 中,高效批量转储到 disruptor,避免多次的 disruptor 生产者屏障的竞争;

  • 措施 2:埋点过程中必不可少的会使用到反射,但是反射是有坑的(点击此处了解),分析反射逻辑的源码,矫正反射使用的姿势;

  • 措施 3:可能的话不要使用反射,而是通过字节码技术让埋点类实现自定义特定接口,通过执行正常的方法调用来获取对象实例数据。

  • 措施 4:在 ThreadLocal 与 disruptor,及 disruptor 与 Kafka 的数据转储过程中,池化大的集合对象,避免过多的大内存的消耗。


(3)线程阻塞点 3——span 数据发送 :

同样,使用 disruptor 来解决线程阻塞的问题。

2、健壮性


边界问题的考虑及解决是极大依赖开发人员的个人经验及技术能力的,下面我列了几个重点的问题,也是业务方担忧较多的问题。


(1)如果 Agent 自身逻辑有问题怎么办?

全程 try-catch、自身异常的话相同异常日志 2 分钟内只打印一条。


(2)如果无法及时避免阻塞业务线程怎么办?

降级,直接退出单次埋点流程。


(3)如果业务太繁忙 cpu 消耗大怎么办?

  • 采样控制+频率控制+降级;

  • 直接丢弃数据;

  • 自定义 disruptor 的消费者等待策略,在高性能与高消耗之间做平衡。


(4)如果消耗过多内存怎么办?

严格对内存数据对象进行计数限制;

数据流转过程中难以控制的大内存消耗点使用 SoftReference。


(5)如果 Kafka 连不上/断连怎么办?

支持降级的同时,可选启动连不上直接退出 Agent 阻止程序启动,运行时断连直接丢弃数据。


五、难点技术及关键实现简介


下面会简单介绍下 Agent 中的一些关键的难点技术。其中最为难以掌控的是 Agent 中的类需要控制被哪个类加载器来加载,不然你一定会痛苦的面对各种 ClassNotFoundException 的。

1、启动流程



Agent 启动流程看起来是简单的,这里贴出来可以方便内部的同学阅读源码。需要注意的是启动伊始是以 premain 方法作为入口,这个方法所在类由 AppClasssLoader 来加载。启动流程中需要控制好 Agent 中的哪些类或者模块由哪个类加载器来加载,并且部分类是通过自定义类加载器来主动加载的,不同的类加载器逻辑执行空间的衔接,是通过 jdk 中的代理模式(InvocationHandler)来解决的,后面会做介绍。

2、微内核应用架构


Agent 的主要职责是埋点和数据采集,埋点理当是整个 Agent 中最为核心的逻辑,以下简单介绍下围绕核心的各个功能块功能,图中除了类隔离功能外,其他功能块都是可以直接去掉而不影响其他模块的功能,遵循了微内核应用架构模式。


日志:自定义实现

  • 适配环境,不同环境不同行为;

  • 适配 slf4j;

  • 日志级别动态可控;

  • 自动识别相同 error 日志,避免冲击日志中心。


监控:可靠性的基石

  • 监控埋点数据完整的生命周期(产生、入队、出队、入 kafka 成功/失败、内存队列消耗状况、数据丢失情况);

  • 监控 jvm 采样延时状况。


策略控制功能块:

  • 基于观察者模式广播配置变更事件;

  • 控制着采样、日志级别、业务日志拦截级别、降级、异常流控、监控频率、jvm 打点频率、数据聚合。


字节码转换控制功能块:

  • 组件增强插件化(可配置);

  • 增强逻辑之间相互隔离;

  • 增强逻辑高度封装,实现配置化;

  • 核心流程模仿 spring 类的继承体系,具备强可扩展性。


流程控制功能块:

  • 应用内部高度模块化;

  • SPI 机制高可扩展。


类隔离控制单元:

  • 自定义多个类加载器,加载不同位置及 jar 中的类;

  • 兼容 Tomcat 和 JDK 的类加载器的继承关系,主动让 Tomcat 或者 JDK 中特定类加载器显式加载类;

  • 干扰类的双亲委派模型,控制特定类的父类或者接口的加载。

3、核心技术栈



图中箭头的方向,意为由上而下的技术使用难度的增大,同时需要用来研究及调优的时间消耗也增加。其中 Java 探针技术即是上文中介绍的 javaagent,ByteBuddy 的选型报告及背景在下文中有介绍,disruptor 主要是需要花费较多时间进行技术背景理解、源码阅读及调优,后文也有介绍,而类加载控制的应用,是项目之初最为头疼的难点,犹记得 17 年底处理 ClassNotFoundException 时的绝望,远远不是了解如何自定义类加载器及双亲委派这些知识能解决的。当初买了好几本有相关知识介绍的书籍来研究,哪怕是在这本书的目录中仅仅发现了可能不到 1 页的并且也只是可能相关的篇幅,买书投入都花了好几百块。

4、类加载及隔离控制


需要注意的是,类加载隔离的控制目标是自己用到的三方包不与业务方的三方包因版本产生冲突,并且保证 Agent 中逻辑执行时不出现找不到类的问题,这里简单画了 Agent 中的类加载隔离情况,可以结合上面的小节来简单理解。



这里我尝试罗列需要掌握的知识点:

  • 类加载的 4 大时机;

  • premain 所在类的加载及执行逻辑;

  • JDK 的双亲委派模型,及如何实现自定义加载器,如何更改加载顺序;

  • JDK 中的全部类加载器的研究。当初误入歧途,恨不得去研究 JVM 部分看不懂的 C++源码;

  • Tomcat 类加载架构,相应部分源码阅读;

  • 类加载器执行空间的跳转。

六、部分选型报告


整个调用链系统在开发时涉及到了非常多的关键技术的选型,这里仅给出 Agent 相关的两个关键技术。

1、字节码操控工具 ByteBuddy


字节码编程对于普通的 Java 程序员来说,算是能玩的最牛的黑科技了。什么是字节码编程呢?相信你一定多多少少了解过 javassist、asm 等字节码编辑库,我们在进行字节码编程时,一般会借助这些库动态的修改或者生成 Java 字节码。例如 Dubbo 就借助了 javassist 来动态生成部分类的字节码。选择 ByteBuddy 的原因主要是项目之初参考了 SkyWalking 的埋点逻辑,而那时 SkyWalking 就是使用的 ByteBuddy。如果现在来选择,我会优先 Javassist,下面罗列了几个框架个人理解的优缺点。

(1)ByteBuddy

基于 ASM 做的封装,使用到的开源项目:Hibernate、Jackson。


优点:

  • 在特定场景下使用非常方便;

  • 17 年框架的作者非常活跃,支持最新 jdk 几乎全部新特性;

  • 容易定制扩展。

缺点:

  • 领域模型定义混乱,类图设计复杂,内部类可以深达 8 层,eclipse 都无法反编译若干类,源码难以调试及阅读,对深度使用者极度不友好,我们一般开发使用的内部类极少会超过 3 层,想象下 8 层深的内部类是怎么样的!

(2)ASM

开源项目:Groovy/Kotlin 编译器、CGLIB、Spring。

优点:

  • 写出来的代码很显野蛮牛逼的气息,面向字节码编程是 Java 语言级别的黑科技;

  • 愿景致力于性能和精小,全部代码只有 28 个类。

缺点:

  • 使用起来比较复杂,编码效率低下;

  • 需要比较了解 Java 语言字节码指令集,需要比较清楚 class 文件内容布局。

(3)Javassist

开源项目:Dubbo、MyBatis。

优点:

  • 使用简单快速易上手;

  • 先生成字符串再编译成字节码的使用方式,对程序员来说很容易理解;

  • 官方文档示例易理解,且很丰富。

缺点:

  • 自带的编译器与 Javac 有一定差距,难以实现复杂功能和新版 jdk 新特性。

2、环形无锁队列 Disruptor


使用 Disruptor 的原因,主要是其高性能的同时,能做到限制容量也不阻塞,这简直太让人满意了,而 JDK 中的线程安全相关集合皆无法满足。


(1)主要特点:无阻塞、低延迟、高消耗。

(2)使用场景:

  • 高并发无阻塞低延迟系统;

  • 分段式事件驱动架构。

(3)为何这么快?

  • 使用了 volatile 和 cas 无锁操作;

  • 使用了缓存行填充手段避免伪共享;

  • 数组实现预先分配内存,减少了内存申请和垃圾收集带来延迟影响;

  • 快速指针操作,将模运算转换成与运算(m % 2^n = m & ( 2^n - 1 ))。

(4)使用注意事项:

消费者等待策略:综合业务线程阻塞、cpu 损耗、数据丢失情况做的综合考虑。


七、总结


要做好调用链系统的研发,显然是一个困难的工作,难点不仅仅在于 Agent 技术难点解决,也在于产品能力的决策与挖掘,在于怎样用最少的资源满足产品需求,更在于当初不懂大数据的 Java 开发在有限资源前提下来做海量数据计算。


希望本文能给正在从事以及将会从事调用链系统研发的公司及团队一点参考。感谢阅读。


作者:Shi Zhengxing


发布于: 2020 年 11 月 23 日阅读数: 81
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
vivo 调用链 Agent 原理及实践