解决方案:最全面!一文让你看懂无侵入的微服务探针原理!!

优采云 发布时间: 2022-11-14 17:50

  解决方案:最全面!一文让你看懂无侵入的微服务探针原理!!

  前言

  随着微服务架构的兴起,应用行为的复杂性显着增加。为了提高服务的可观测性,分布式监控系统变得非常重要。

  基于谷歌的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的核心就是放置Call​​back函数。

  正是这种机制允许我们向 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?

  解决方案:Kubernetes日志采集Sidecar模式介绍

  作为 CNCF(云原生计算基金会)的核心项目,Kubernetes(K8S)得到了 Google 和 Redhat 强大社区的支持。近两年发展迅速。在成为容器编排领域的领导者的同时,也在向着 PAAS 基地迈进。标准开发。

  记录 采集 方式

  日志作为任何系统都不可缺少的一部分,在K8S的官方文档中也以多种日志采集的形式进行了介绍。总结起来主要有以下三种:native方法、DaemonSet方法和Sidecar方法。

  Native方式:使用kubectl日志直接查看本地保留的日志,或者通过docker引擎的日志驱动将日志重定向到文件、syslog、fluentd等系统。DaemonSet方法:在K8S的每个节点上部署一个日志代理,将所有容器的日志从agent采集发送到服务器。Sidecar 模式:在 POD 中运行 sidecar 的日志代理容器用于 POD 的主容器生成的 采集 日志。

  采集方法对比

  每种采集方法都有一定的优缺点,这里我们做一个简单的比较:

  原生方式

  DaemonSet 方法

  边车方式

  采集日志类型

  标准输出

  标准输出 + 部分文件

  文档

  部署和维护

  低原生支持

  一般需要维护DaemonSet

  更高,每个需要采集日志的POD都需要部署一个sidecar容器

  日志分类存储

  达不到

  一般可以通过容器/路径等方式进行映射。

  每个 POD 都可以单独配置以实现高灵活性

  多租户隔离

  虚弱的

  一般只通过配置之间的隔离

  强,通过容器隔离,资源可单独分配

  支持集群大小

  无限本地存储,如果使用syslog和fluentd,会有单点限制

  中小规模,业务数量最多可支持100级

  无限

  资源占用

  低,由 docker 引擎提供

  较低,每个节点运行一个容器

  更高,每个 POD 运行一个容器

  查询方便

  低的

  高,可进行自定义查询和统计

  高,可根据业务特点定制

  可定制性

  低的

  低的

  

  高,每个 POD 单独配置

  适用场景

  测试、POC等非生产场景

  单功能集群

  大型混合 PAAS 集群

  从上表可以看出:

  native 方法比较弱,一般不建议在生产系统中使用,否则很难完成问题排查、数据统计等任务;DaemonSet 方式每个节点只允许一个日志代理,相对资源消耗要小很多,但可扩展性,租户隔离有限,更适合功能单一或服务数量少的集群;Sidecar方式为每个POD单独部署一个日志代理,占用资源较多,但灵活性强,多租户隔离。该方法用于 K8S 集群或服务多个业务方的集群作为 PAAS 平台。日志服务 K8S采集 方法

  DaemonSet 和 Sidecar 模式各有优缺点,目前还没有可以适用于所有场景的方法。因此,我们的阿里云日志服务同时支持 DaemonSet 和 Sidecar 两种方式,并且对每种方式都做了一些额外的改进,更适合 K8S 下的动态场景。

  两种模式都是基于Logtail实现的。目前,日志服务客户端Logtail已经部署在百万级别,每天有采集数万个应用和PB级数据,并经过多次双11和双12测试。相关技术分享请参考文章:多租户隔离技术+双十一实战效果,日志顺序保存采集轮询+Inotify组合下的解决方案。

  守护进程优采云采集器方法

  在 DaemonSet 模式下,Logtail 做了很多适配工作,包括:

  详细介绍文章可以参考:再次升级!阿里云Kubernetes日志解决方案LC3视角:日志采集,Kubernetes下的存储与处理技术实践

  边车采集方式

  Sidecar模式的配置和使用与虚拟机/物理机采集上的数据相差不大。从Logtail容器的角度来看:Logtail工作在一个“虚拟机”上,需要采集这台机器上的某台机器。个人/一些日志文件。

  但在容器场景下,需要解决两个问题:

  配置:使用编排方式配置代理容器动态:需要适应POD的IP地址和主机名的变化

  目前Logtail的容器支持通过环境变量配置相关参数,支持自定义logo机器组的工作,可以完美解决以上两个问题。Sidecar 配置示例

  Sidecar模式下的日志组件安装配置方法如下:

  第一步:部署Logtail容器部署POD时,将日志路径挂载到本地,并将对应的卷挂载到Logtail容器中。Logtail 容器需要配置 ALIYUN_LOGTAIL_USER_ID 、 ALIYUN_LOGTAIL_CONFIG 、 ALIYUN_LOGTAIL_USER_DEFINED_ID 。参数含义及取值请参见:标准Docker Log采集。

  提示:

  建议为Logtail容器配置健康检查,当运行环境或内核出现异常时可以自动恢复。示例中使用的Logtail镜像访问阿里云杭州公网镜像仓库。您可以根据需要替换成本区域的图片,使用内网方式。

  apiVersion: batch/v1

kind: Job

metadata:

name: nginx-log-sidecar-demo

namespace: kube-system

spec:

template:

metadata:

name: nginx-log-sidecar-demo

spec:

# volumes配置

volumes:

- name: nginx-log

emptyDir: {}

containers:

# 主容器配置

- name: nginx-log-demo

image: registry.cn-hangzhou.aliyuncs.com/log-service/docker-log-test:latest

<p>

command: ["/bin/mock_log"]

args: ["--log-type=nginx", "--stdout=false", "--stderr=true", "--path=/var/log/nginx/access.log", "--total-count=1000000000", "--logs-per-sec=100"]

volumeMounts:

- name: nginx-log

mountPath: /var/log/ngin

# Logtail的Sidecar容器配置

- name: logtail

image: registry.cn-hangzhou.aliyuncs.com/log-service/logtail:latest

env:

# aliuid

- name: "ALIYUN_LOGTAIL_USER_ID"

value: "165421******3050"

# 自定义标识机器组配置

- name: "ALIYUN_LOGTAIL_USER_DEFINED_ID"

value: "nginx-log-sidecar"

# 启动配置(用于选择Logtail所在Region)

- name: "ALIYUN_LOGTAIL_CONFIG"

value: "/etc/ilogtail/conf/cn-hangzhou/ilogtail_config.json"

# 和主容器共享volume

volumeMounts:

- name: nginx-log

mountPath: /var/log/nginx

# 健康检查

livenessProbe:

exec:

command:

- /etc/init.d/ilogtaild

- status

initialDelaySeconds: 30

periodSeconds: 30

</p>

  步骤 2:配置机器组

  如下图,在日志服务控制台创建Logtail机器组,为机器组选择自定义ID,可以动态适应POD ip地址的变化。具体操作步骤如下:

  激活日志服务并创建项目和日志存储。详细步骤请参见准备过程。在日志服务控制台的“机器组列表”页面,单击“创建机器组”。选择User-defined ID,在User-defined ID内容框中填写您在上一步中配置的ALIYUN_LOGTAIL_USER_DEFINED_ID。

  步骤 3:配置 采集 方法

  机器组创建完成后,可以配置对应文件的采集配置。目前支持极简、Nginx访问日志、分隔符日志、JSON日志、常规日志等格式。详细请参考:文本日志配置方法。本例中的配置如下:

  第四步:查询日志

  采集配置完成并应用到机器组后,可以在1分钟内上传采集的日志,进入采集的查询页面可以查询到采集上传的日志对应的日志存储。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线