真相:最全面!一文让你看懂无侵入的微服务探针原理!!
优采云 发布时间: 2022-10-04 05:06真相:最全面!一文让你看懂无侵入的微服务探针原理!!
前言
随着微服务架构的兴起,应用行为的复杂性显着增加。为了提高服务的可观测性,分布式监控系统变得非常重要。
基于谷歌的Dapper论文,开发了很多知名的监控系统:Zipkin、Jaeger、Skywalking、OpenTelemetry,想要统一江湖。一群厂商和开源爱好者围绕采集、监控数据的采集、存储和展示做了很多优秀的设计。
如今,即使是个人开发者也可以依靠开源产品轻松构建完整的监控系统。但作为监控服务商,需要做好与业务的解绑工作,降低用户接入、版本更新、问题修复、业务止损等成本。因此,一个可插拔的、非侵入式的采集器成为了很多厂商的必备。
为了获取服务之间的调用链信息,采集器通常需要在方法前后进行埋藏。在Java生态中,常见的埋点方式有两种:依靠SDK手动埋点;使用Javaagent技术做无创跟踪。下面对无创埋点的技术和原理进行全面的介绍。
侵入式 采集器(探测)
在分布式监控系统中,模块可以分为:采集器(Instrument)、Transmitter(TransPort)、Collector(Collector)、Storage(Srotage)、Display(API&UI)。
zipkin的架构图示例
采集器将采集到的监控信息从应用端发送给采集器,采集器存储,最后提供给前端查询。
采集器采集信息,我们称之为Trace(调用链)。一条跟踪有一个唯一标识符 traceId,它由自上而下的树跨度组成。除了spanId,每个span还有traceId和父spanId,这样就可以恢复完整的调用链关系。
为了生成跨度,我们需要在方法调用前后放置埋点。比如对于一个http调用,我们可以在execute()方法前后添加埋点,得到完整的调用方法信息,生成一个span单元。
在Java生态中,常见的埋点方式有两种:依靠SDK手动埋点;使用Javaagent技术做无创跟踪。许多开发者在接触分布式监控系统时就开始使用 Zipkin。最经典的就是了解X-B3 trace协议,使用Brave SDK,手动埋点生成trace。但是,SDK中的埋点方式无疑是深深依赖于业务逻辑的。升级埋点时,必须进行代码更改。
那么如何将其与业务逻辑解绑呢?
Java还提供了另一种方式:依靠Javaagent技术修改目标方法的字节码,实现无创埋葬。这种使用Javaagent 的采集器 方式也称为探针。在应用启动时使用-javaagent,或者在运行时使用attach(pid)方法,可以将探针包导入应用,完成埋点的植入。以非侵入方式,可以实现无意义的热升级。用户无需了解深层原理即可使用完整的监控服务。目前很多开源监控产品都提供了丰富的java探针库,进一步降低了作为监控服务商的开发成本。
开发一个非侵入式探针,可以分为三个部分:Javaagent、字节码增强工具、跟踪生成逻辑。下面将介绍这些。
基本概念
在使用JavaAgent之前,让我们先了解一下Java相关的知识。
什么是字节码?
自 1994 年 Sun 发明类 C 语言 Java 以来,凭借“编译一次,到处运行”的特性,它迅速风靡全球。与 C++ 不同的是,Java 先将所有源代码编译成类(字节码)文件,然后依靠各种平台上的 JVM(虚拟机)来解释和执行字节码,从而与硬件解绑。class文件的结构是一个table表,由很多struct对象组成。
类型
姓名
阐明
长度
u4
魔法
幻数,识别Class文件格式
4字节
u2
次要版本
次要版本号
2 个字节
u2
主要版本
主要版本号
2 个字节
u2
常量池计数
常量池计算器
2 个字节
cp_info
常量池
常量池
n 字节
u2
访问标志
访问标志
2 个字节
u2
这节课
类索引
2 个字节
u2
超类
父索引
2 个字节
u2
接口数
接口计数器
2 个字节
u2
接口
接口索引集合
2 个字节
u2
字段数
字段数
2 个字节
字段信息
字段
字段集合
n 字节
u2
方法数
方法计数器
2 个字节
方法信息
方法
方法集合
n 字节
u2
属性计数
额外的物业柜台
2 个字节
属性信息
属性
附加属性集合
n 字节
字节码的字段属性
让我们编译一个简单的类 `Demo.java`
package com.httpserver;public class Demo { private int num = 1; public int add() { num = num + 2; return num; }}
16进制打开Demo.class文件,解析出来的字段也是由很多struct字段组成的:比如常量池、父类信息、方法信息等。
JDK自带的解析工具javap可以将class文件以人类可读的方式打印出来,结果和上面的一致
什么是JVM?
JVM(Java Virtual Machine),一种能够运行Java字节码的虚拟机,是Java架构的一部分。JVM有自己完整的硬件架构,如处理器、栈、寄存器等,也有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需要生成运行在JVM上的目标代码(字节码),无需修改即可运行在各种平台上。这是“一次性编译”。,到处跑”的真正意思。
作为一种编程语言虚拟机,它不仅专用于Java语言,只要生成的编译文件符合JVM对加载和编译文件格式的要求,任何语言都可以被JVM编译运行。
同时,JVM技术规范并没有定义使用的垃圾回收算法和优化Java虚拟机指令的内部算法等,只是描述了应该提供的功能,主要是为了避免过多的麻烦和对实施者的限制。正是因为描述得当,才给厂商留下了展示的空间。
维基百科:现有 JVM 的比较
其中性能较好的HotSpot(Orcale)和OpenJ9(IBM)受到广大开发者的喜爱。
JVM的内存模型
JVM部署完成后,每一个Java应用启动,都会调用JVM的lib库申请资源,创建一个JVM实例。JVM 将内存划分为不同的区域。下面是JVM运行时的内存模型:
父委托加载机制
当 Java 应用程序启动并运行时,一个重要的操作是加载类定义并创建一个实例。这依赖于 JVM 自己的 ClassLoader 机制。
家长委托
一个类必须由一个ClassLoader加载,对应的ClassLoader和父ClassLoader,寻找一个类定义会从下往上搜索,这就是父委托模型。
JVM为了节省内存,并没有把所有的类定义都放到内存中,而是
这个设计提醒我们,如果可以在加载时或者直接替换加载的类定义,就可以完成神奇的增强。
JVM工具接口
晦涩难懂的 JVM 屏蔽了底层的复杂性,让开发人员可以专注于业务逻辑。除了启动时通过java -jar的内存参数外,其实还有一套专门提供给开发者的接口,即JVM工具接口。
JVM TI 是一个双向接口。JVM TI Client 也称为代理,基于事件事件机制。它接受事件并执行对 JVM 的控制,以及响应事件。
它有一个重要的特性——Callback(回调函数)机制:JVM可以产生各种事件,面对各种事件,它提供了一个Callback数组。每个事件执行的时候都会调用Callback函数,所以写JVM TI Client的核心就是放置Callback函数。
正是这种机制允许我们向 JVM 发送指令以加载新的类定义。
Java代理
现在让我们试着想一想:如何神奇地改变应用程序中的方法定义?
这有点像把大象放在冰箱里,然后走几步:
根据字节码的规范生成一个新的类
使用 JVM TI,命令 JVM 将类加载到相应的内存中。
更换后,系统将使用我们的增强方法。
这并不容易,还好jdk为我们准备了这样一个上层接口指令包。它也很容易使用。我们将通过一个简单的agent例子来说明指令包的关键设计。
Javaagent的简单示例
javaagent有两种使用方式:
使用第一种方法的demo
public class PreMainTraceAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer{ @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("premain load Class:" + className); return classfileBuffer; } }}
清单版本:1.0
可以重新定义类:真
可以重新转换类:真
Premain 类:PreMainTraceAgent
然后在resources目录下新建一个目录:META-INF,在这个目录下新建一个文件:MANIFREST.MF:
最后打包成agent.jar包
到了这里,你会发现增强字节码就是这么简单。
字节码生成工具
通过前面的理解,有一种感觉就是修改字节码就是这样^_^!!!但是我们要注意另一个问题,字节是如何产生的?
大佬:熟悉JVM规范,理解每个字节码的含义。我可以手动更改类文件,所以我为此编写了一个库。
专家:我知道客户端的框架,我修改源代码,重新编译,把二进制替换进去。
小白:我看不懂字节码。我可以使用大佬写的库。
下面将介绍几种常用的字节码生成工具
ASM
ASM 是一个纯字节码生成和分析框架。它具有完整的语法分析、语义分析,可用于动态生成类字节码。不过,这个工具还是太专业了。用户必须非常了解 JVM 规范,并且必须确切地知道应该在类文件中进行哪些更改以替换函数。ASM 提供了两组 API:
如果你对字节码和JVM内存模型有初步的了解,你可以根据官方文档简单的生成类。
ASM 十分强大,被应用于 <br /> 1. OpenJDK的 lambda语法 <br /> 2. Groovy 和 Koltin 的编译器 <br /> 3. 测试覆盖率统计工具 Cobertura 和 Jacoco <br /> 4. 单测 mock 工具,比如 Mockito 和 EasyMock <br /> 5. CGLIB ,ByteBuddy 这些动态类生成工具。
字节好友
ByteBuddy 是一款优秀的运行时字节码生成工具,基于 ASM 实现,提供更易用的 API。许多分布式监控项目(如 Skywalking、Datadog 等)使用它作为 Java 应用程序的探针以 采集 监控信息。
下面是与其他工具的性能比较。
在我们实际使用中,ByteBuddy的API真的很友好,基本满足了所有字节码增强需求:接口、类、方法、静态方法、构造方法、注解等的修改。另外,内置的Matcher接口支持模糊匹配,并且您可以根据名称匹配修改符合条件的类型。
但也有不足之处。官方文件比较陈旧,中文文件很少。很多重要的特性,比如切面等,没有详细介绍,经常需要阅读代码注释和测试用例才能理解真正的含义。如果你对ByteBuddy感兴趣,可以关注我们的公众号,下面文章将对ByteBuddy做专题分享。
跟踪数据的生成
通过字节码增强,我们可以实现非侵入式埋葬,那么与trace的生成逻辑的关联就可以看作是灵魂注入。下面我们用一个简单的例子来说明这样的组合是如何完成的。
示踪剂 API
这是一个用于生成跟踪消息的简单 API。
public class Tracer { public static Tracer newTracer() { return new Tracer(); } public Span newSpan() { return new Span(); } public static class Span { public void start() { System.out.println("start a span"); } public void end() { System.out.println("span finish"); // todo: save span in db } }}
只有一种方法 sayHello(String name) 目标类 Greeting
public class Greeting { public static void sayHello(String name) { System.out.println("Hi! " + name); }}
手动生成trace消息,需要在方法前后添加手动埋点
... public static void main(String[] args) { Tracer tracer = Tracer.newTracer(); // 生成新的span Tracer.Span span = tracer.newSpan(); // span 的开始与结束 span.start(); Greeting.sayHello("developer"); span.end();}...
无侵入埋点
字节增强允许我们不修改源代码。现在我们可以定义一个简单的aspect,将span生成逻辑放入aspect中,然后使用Bytebuddy植入埋点。
跟踪建议
将跟踪生成逻辑放入切面
public class TraceAdvice { public static Tracer.Span span = null; public static void getCurrentSpan() { if (span == null) { span = Tracer.newTracer().newSpan(); } } /** * @param target 目标类实例 * @param clazz 目标类class * @param method 目标方法 * @param args 目标方法参数 */ @Advice.OnMethodEnter public static void onMethodEnter(@Advice.This(optional = true) Object target, @Advice.Origin Class clazz, @Advice.Origin Method method, @Advice.AllArguments Object[] args) { getCurrentSpan(); span.start(); } /** * @param target 目标类实例 * @param clazz 目标类class * @param method 目标方法 * @param args 目标方法参数 * @param result 返回结果 */ @Advice.OnMethodExit(onThrowable = Throwable.class) public static void onMethodExit(@Advice.This(optional = true) Object target, @Advice.Origin Class clazz, @Advice.Origin Method method, @Advice.AllArguments Object[] args, @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result) { span.end(); span = null; }}
onMethodEnter:方法进入时调用。Bytebuddy 提供了一系列注解,带有@Advice.OnMethodExit 的静态方法,可以插入到方法开始的节点中。我们可以获取方法的详细信息,甚至可以修改传入的参数以跳过目标方法的执行。
OnMethodExit:方法结束时调用。类似于onMethodEnter,但可以捕获方法体抛出的异常并修改返回值。
植入建议
将 Javaagent 获得的 Instrumentation 句柄传递给 AgentBuilder(Bytebuddy 的 API)
public class PreMainTraceAgent { public static void premain(String agentArgs, Instrumentation inst) { // Bytebuddy 的 API 用来修改 AgentBuilder agentBuilder = new AgentBuilder.Default() .with(AgentBuilder.PoolStrategy.Default.EXTENDED) .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(new WeaveListener()) .disableClassFormatChanges(); agentBuilder = agentBuilder // 匹配目标类的全类名 .type(ElementMatchers.named("baidu.bms.debug.Greeting")) .transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) { return builder.visit( // 织入切面 Advice.to(TraceAdvice.class) // 匹配目标类的方法 .on(ElementMatchers.named("sayHello")) ); } }); agentBuilder.installOn(inst); } // 本地启动 public static void main(String[] args) throws Exception { ByteBuddyAgent.install(); Instrumentation inst = ByteBuddyAgent.getInstrumentation(); // 增强 premain(null, inst); // 调用 Class greetingType = Greeting.class. getClassLoader().loadClass(Greeting.class.getName()); Method sayHello = greetingType.getDeclaredMethod("sayHello", String.class); sayHello.invoke(null, "developer"); }
除了制作agent.jar,我们可以在本地调试的时候在main函数中启动,如上所示。本地调试
打印结果
WeaveListener onTransformation : baidu.bms.debug.Greetingstart a spanHi! developerspan finishDisconnected from the target VM, address: '127.0.0.1:61646', transport: 'socket'
如您所见,我们在目标方法之前和之后添加了跟踪生成逻辑。
在实际业务中,我们往往只需要捕获应用程序使用的帧,比如Spring的RestTemplate方法,就可以获取准确的Http方法调用信息。这种依靠这种字节码增强的方式,最大程度地实现了与业务的解耦。
还有什么?
在实际业务中,我们也积累了很多踩坑的经验:
1、有没有好的探针框架可以让我“哼哼哼”地写业务?
2、如何实现无意义的热升级,让用户在产品上轻松设置埋点?
3. ByteBuddy如何使用,切面的注解是什么意思?
4、Javaagent+Istio如何让Dubbo微服务治理框架毫无意义地迁移到ServiceMesh?
解决方案:免费的自媒体原创度检测工具有哪些?快速帮你提高系统推荐
绝大多数自媒体人应该会遇到这种问题,他们的文章没有通过自媒体平台的系统审核,或者被自媒体上的0推荐平台,这是很多人头疼的问题。事实上,有一个解决方案。现在很多人使用自媒体原创度数检测工具进行辅助检测。
学习使用自媒体原创度数检测工具其实可以帮你节省很多修改内容的时间,原创低度数的内容可以在自媒体平台上获取爆文推荐的概率很小。今天给大家介绍一些免费的自媒体原创度数检测工具,帮助大家提高系统的推荐率。
工具一:轻松写作原创度数检测
很多人都用过一转的原创度检测工具,现在自媒体平台的作者也是根据一转的原创检测度来判断文章标准的。一般来说,在使用检测工具时,每种工具对每种工具都有不同的检测程度标准。现在一转对文章检测的标准是75%以上,即使是原创度数比较高的内容,你也可以去看看数据。
工具2:搜索引擎重复
这个方法也很简单。一般来说,百度搜索引擎用于检查搜索引擎上的重复项。这是更多的参考。当你的文章创建完成后,根据段落在百度上搜索。,只要不是大范围的红,或者红字太多都可以,到时候可以做适当的改动。
其实如果想提高文章的推荐率和原创的度数,最好自己写。可以找一些爆文素材,然后整合内容,加入自己的观点,这样的文章原创度数也会很高,而且有自己的热点流量,你可以试试。
今天给大家讲解了几个免费的自媒体原创度数检测工具。欢迎关注我。如果您对自媒体有任何疑问,也可以直接评论我,我会为您解答。