
目录
1、前章
2、开篇
1、前章
阐述我司建设千亿级储能、毫秒级查询耗时全链路追踪系统的全过程。
对Pinpoint、Zipkin、Jaeger 和 SkyWalking 进行选型分析,剖析评估 SkyWalking 的优势及适配场景。
讲解POC阶段的全貌,包括对接发布系统及配置中心、完善插件补全链路、加强自监控系统、在测试环境试炼观测。
如何围绕核心应用域开展接入推广,并对启动耗时、Kafka 积压、存储报错、查询超时等生产性能问题进行优化。
SkyWalking官方中文博客所收录稿件的链接:
https://skywalking.apache.org/zh/2022-08-30-pingan-jiankang
早在远古时期,人类就把月亮当作地球的守护神,直到现在还有许多人坚信月亮会从遥远的地方观测到地球上每个人的言行,保佑地球上的人类。正如千年来人类追求和谐安逸的生活一样,在程序的世界中,各类相关人员也都在追求程序系统的稳定健康;除了在设计实现的时候小心谨慎,还要通过额外的质控机制,在上线前后做几遍严格的检测,即使这样依然不放心,甚至祭拜更神秘的组织(比如关公),希望通过信仰的力量来保障系统健康、不要出问题。
对于系统是否健康以及谁来保障系统健康,好让相关人员能够工作得很安逸,这很有争议。借用《我的团长我的团》中的一句台词来说“死都不怕,就怕不安逸;命都不要了,就要安逸”,龙团长的本意并不是安逸不对,而是在当时那种战乱的环境下还只想着让别人来保障自己的安逸不对。翻译一下就是自己系统是否健康自己难道不知道嘛,难道保障系统健康不是自己的事情嘛。对这个话题我的观点是:不是任何时候都要全民皆兵,尤其是软件产业发展到今天,还要每个系统自己全力去保障自己的健康是不科学的;分工-使用专业的APM系统来保障系统的健康,才更高效。
有同学看到这里会有疑惑:现在的产研流程中,开发人员自测通过之后再交由质控部门检测准出的系统才算是健康的,怎么会是APM系统来保障系统的健康呢?这个质疑可以从《Go for Industrial Programming》中找到答案:“对于我们的分布式系统来说,根本没有高性价比的方法来做全面的集成或烟雾测试。集成环境或测试环境在很大程度上就是一种浪费;再多的环境也不会把事情变得更容易。对于我们的大多数系统来说,良好的可观测性肯定比良好的测试更重要,因为只有良好的可观测性才能帮助敏捷的团队专注于快速部署和回滚,优化平均恢复时间(MTTR)而不是平均故障间隔时间(MTBF)”。因此保障系统的健康,良好的可观测性比良好的测试更重要。
另外,近些年软件领域发展迅速,各类中间件百家争鸣,很多公司把握风口迅速扩张,各个业务线全面开战;一时间技术复杂度、业务复杂度、人员复杂度全部增加,随之而来的问题也越来越多、越来越复杂,期待通过一种简单快捷的方法(比如信仰)来解决掉他们是不现实的,否则监控领域也不会如此具有挑战性。正如知乎上《 复杂度是不灭的,只会转移,难道一切都是徒劳的吗?》 文中所说“只有复杂度才能处理复杂度,这是无法避免的;复杂度可以转移但不会消失,必须存在于某个地方”。作为技术人员, 无论你是否意识到,它始终是人们解决问题时需要注意到的;而作为架构师或者管理者对此一定要有清晰的理解。
把专业化分工的必然、良好的可观测性比测试更重要以及复杂度不消失只转移的规则这三者套用到监控领域来看,就能理解为何专业的非侵入式APM系统被追捧成为主流。类比月亮被奉为地球远方的守护神,SkyWalking Agent 就是应用程序非侵入式的守护神。
小提示:本篇内容的技术探讨只覆盖了 Java 技术栈。
如果我们有一个完美的可观测性数据的收集系统,通过它可以查看我们想知道的所有细节,那么任何问题我们就能快速在系统中查询并得出精准的解释,但显然这样的数据系统是不存在的。因此,需要通过决策取舍后给某些类型的观测注入语义,以便开展观测工作。
Opentelemetry 协议,是 CNCF (Cloud Native Computing Foundation -云原生计算基金会) 定义的最新一代可观测规范,该规范定义了可观测性的三大支柱:Metrics(指标)、Trace(追踪)、Log (日志),它们在当下以及未来一段时间仍会是主流支柱,当然变数依然存在,比如Events现在也很受热捧。

Metrics 是 Counter(计数器)、Gauge(仪表)和 Histogram(直方图),它们被快速、实时地分析聚合后,用于仪表板显示和告警。
系统层面的 Metrics:CPU、内存、I/O 等信息,一般由运维人员关注
应用层面的 Metrics:调用量、出错率、请求延迟等,一般由开发人员关注
业务层 Metrics :转化率、下单率,一般由产品与运营人员关注
Log 是离散的事件,用于事后的分析、报告和调试;高质量的日志一定是结构化的,可以在创建之后被灵活的编排操控。
Trace 是请求链路中的全部性能数据,尤其是在分布式系统中,Trace 链路必须是全的,正确、完整地实施跟踪的成本非常高,并且只有在分布式系统规模很大时才有意义。
Log :我们使用到的组件以及业务逻辑中总会被其实现者主动的记录好各种日志,不需要由APM系统来负责日志数据的创建,其上报、解析、存储可由APM系统来完成。
Metrics :
容器层面:如 CPU、内存、硬盘、网络等这些数据,通常是由Prometheus exporter来采集
JVM 层面:APM 系统可以调用 JDK 提供的接口来捕获这些数据,但为了使得数据具有更好的观测效果,通常 APM 系统会用额外的逻辑来对原始数据进行编排
组件层面:很多组件并没有提供完备的指标数据,通常需要额外的逻辑对组件的核心环节进行拦截,计算处理后创建出多维度的指标。
业务层面:近来流行通过 Micrometer 来构建业务维度的各种指标
Trace : 具有复杂、严格的领域模型,涉及的环节众多,并且 Trace 协议的标准从存量市场来看还是没有完全统一的,综合这些原因,正确构建 Trace 的成本是非常高的。
可观测性的三大支柱各有不同的优劣势,可从以下几个维度将他们拆解来看,方便做进一步的了解:
构建成本:开始检测、收集数据的初始成本
运维成本:基础设施支撑其运行的持续成本
响应能力:系统在事件检测和警报方面的表现
排障能力:系统可以在多大程度上帮助分类、调试事件和问题
| 维度 | Metrics | Log | Trace |
|---|---|---|---|
| 构建成本 | 中 | 低 | 高 |
| 运维成本 | 低 | 高 | 中 |
| 响应能力 | 高 | 中 | 低 |
| 排障能力 | 低 | 中 | 高 |
单从功效视角看,可总结为:
Metrics 用于提供统一的仪表板和警报,通常能检测并解决一大类的问题。
Trace 用于定位分布式长链路中什么服务的什么逻辑有问题。
Log 用于事件分类和复杂问题的细节定位、调试。

SkyWaling Agent 早已经支持可观察性的三大支柱,即日志、指标和跟踪,其提供的数据采集处理能力,在开源产品中还是比较优秀的。
JVM 相关的 Metrics ,是通过 JDK 中的 MxBean 来获取,SkyWalking Agent 中对原始的数据再处理后,上报给服务端的收集器。
ManagementFactory.getMemoryMXBean();//内存
ManagementFactory.getMemoryPoolMXBeans();//内存
ManagementFactory.getThreadMXBean();//线程
ManagementFactory.getOperatingSystemMXBean()//cpu
ManagementFactory.getGarbageCollectorMXBeans//gc
用户自定义的 Metrics,可以通过 Micrometer 来实现 Counter、Gauge、Histogram 这三种类型的自定义指标。
服务端还可把 Metrics 导出给其他系统使用。
SkyWalking Agent 对日志的处理已覆盖以下方面:
SkyWalking Agent 对 Trace 的构建能力是优秀的,使用者可以通过多种方式来构建 Trace 数据:
在源码中能看到,Java 技术体系的 Agent 端已经提供了 HTTP、MySQL、Redis、Dubbo 等几十个常见组件的插件,他们可构建对应组件的 Trace 信息;如果所在公司的技术栈都属于开源体系,那么社区提供的插件就足以勾勒出完整的链路了。
如果公司有自研的组件,那么从官网或其他途径查阅相关资料也能快速开发出对应的插件。
通过插件构建的 Trace 信息虽然在绝大部分场景下已足够使用了,但倘若用户需要组件之外逻辑的 Trace 信息,可通过在代码里给待追踪的方法上标注 @Trace 注解,即可完成方法级 Trace 信息的自助式构建。
生产系统会经历许多其他可能影响系统性能的事件,例如升级、重启、混沌测试等,SkyWalking 提供了一种更原生的方式来收集这些事件。另外 SkyWalking Agent 端目前还可以收集 profile 用于性能分析,感兴趣的同学可以去官网了解详情,这里先不展开。
SkyWalking 提供的这许多功能,其实现代码不需要被我们引入到项目工程中,即在项目 Pom 中并不引入其实现逻辑的 Jar 包。个别特殊场景下要的 Jar 包,其内部也仅是接口,并无实现逻辑,这其中的小奥秘会在后续章节解密。
通过上述的介绍我们不难发现,可观测性数据是多源的、异构的,Log 和 Metrics 的采集看起来还算简单,可是 Trace 的情况却很复杂,涉及面最广、难度也最高,必须想办法拦截执行链路中的关键方法,织入额外的逻辑;做好它的采集并非易事,AOP 可能是最好的选择。
在 AOP 的世界观中,任何一个 Java 方法的调用都可以分解为BEFORE、RETURN和THROWS三个环节,由此在三个环节上引申出对应环节的事件探测,甚至是流程控制。
// BEFORE
try {
/*
* other code ...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
基于BEFORE、RETURN和THROWS三个环节的事件,可以完成很多种AOP的操作。
1. 可以感知和改变方法调用的入参
2. 可以感知和改变方法调用返回值和抛出的异常
3. 可以改变方法执行的流程
在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回
混动工程【ChaosBlade】在Java端的故障制造就基于以上能力来实现,上段文字也摘自【JVM-Sandbox】的 github 主页。对于监控领域来说,并不需要改变方法执行的轨迹,只需在方法执行的前后增加逻辑,构建出需要观测的数据即可。
倘若你是技术发烧友,通常有研磨源码的嗜好,一眼不合就想修改-按照自己的审美随意动刀、各种改装,那么增加自己偏爱的观测逻辑和观测内容都是小小case。倘若只有少量组件还好,如果是有几十个组件,且每个组件都会有跟随社区变更而升级的需求,盲猜老板每天在巡场时最期待的肯定你,直到看到你后心中才会默语:“你若在,我便安好!”。
Spring 的 AOP 是很好的动态织入机制,使用字节码增强的方式,SpringBoot 2.x 版本默认改成使用 CGLIB 组件,通过重命名原有方法,再新建一个同签名的方法来做代理的工作模式来完成 AOP 的功能,这种能力感官上看,能轻松应对许多方法逻辑的观测,但需注意,这种方式存在硬伤:
1. 对被代理的目标必须是托管于 Spring 容器中的 Bean ,这个限制对于 Trace 数据的构建并不友好。
2. 一些复杂的上下文逻辑中还需要传递链路信息,就表现得力不从心。
3. 监控的逻辑虽然从组件、业务代码中剥离了,但是依然是伴随着宿主应用一起发布的,倘若仅仅观测功能自身变更需要升级,还要驱动用户去修改代码重新发版,这显然并不友好。这类强耦合的缺陷很多,也算是 Mesh 被推崇的一类原因吧。
通过 Spring 的 AOP 实现观测逻辑的织入,按照复杂度转移的理论,已经是迈出了转移的一小步,只是这一步并不彻底,藕不断丝还连的状态,对开发人员不够友好,其制约了新观测能力的迭代和落地。
那转移的脚步能不能再大一点,既能从管理、部署上相互独立,又能方便快速的合体来实现观测能力的集成和卸载呢?答案是肯定的,将 AOP 实现转移到Java Agent 中,Java Agent 中也可以通过字节码增强的技术来实现 AOP 能力,特别是 Java Agent 的方式对字节码魔改的空间更大。
Java Agent 也是一个Jar包,只是启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附于一个宿主 Java 应用程序运行。
Java Agent 技术可以在加载类文件的环节做拦截,对字节码做修改;也可以在运行期对已加载类的字节码做变更,但是这种情况下会有一些局限,后面会详细说。
从功能层面来说,我们可以使用 Java Agent 技术构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换宿主 JVM 上的程序逻辑,使用它可以实现 JVM 层的 AOP 功能。平时用的很多工具,都是基于 Java Agent 实现的,比如本篇的主角 SkyWalking Agent 、阿里开源的【Arthas】、【JVM-Sandbox】等。
Java Agent 提供了Instrumentation,通常被翻译成插桩(不妨按照植物嫁接来理解它);通过Instrumentation#addTransformer 我们添加自定义的ClassFileTransformer(字节码转换器),在其transform方法中修改类的字节码。运用这种字节码修改的能力,我们能做的事情很多。
JDK1.5 提供的Java Agent 机制是启动时的挂载,JVM 首次加载一个类文件时,在使用这类之前,调用ClassFileTransformer#transform修改字节码,JVM使用修改后的字节码来创建对象。
注意:通过这种方式,宿主应用里所有类的字节码在使用前就都可被增强改造。
JDK1.6 提供了当应用启动后,在运行期动态挂载 Java Agent 的机制,有以下特点:
ClassFileTransformer#transform,我们可在此方法中增强这个类。java.lang.instrument.Instrumentation#retransformClasses方法,让已加载的类重新加载,类被重新加载时即会被传入java.lang.instrument.ClassFileTransformer#transform,我们可在此方法中增强目标类。另外下边这些内容非常重要,在 JDK 的规范中,在运行期重定义一个类必须遵循以下原则:
不允许新增、修改和删除成员变量
不允许新增和删除方法
不允许修改方法签名
6.4、为何不用运行期挂载
SkyWalking Agent 为何不能使用运行期挂载?前边有提到,构建 Trace 数据时,在一些复杂的上下文逻辑中,需要传递 Trace 相关信息,有些情况可以通过 ThreadLocal 来完成,但对于有些跨线程的情况 ThreadLocal 则无法实现,Trace 相关的信息需要由能跨越线程的那些对象来搭载,比如 线程A 调用 线程B 的场景:
线程A
1. 调用ContextManager.capture()将 Trace 的上下文信息保存到一个ContextSnapshot的实例并返回。
2. ContextSnapshot则被附加到任务对象的特定属性中,那么当线程B接触到任务对象时,便能感知到ContextSnapshot。
线程B
1. 线程B中,在任务对象的任务方法被执行前,从任务对象的特定属性中获取ContextSnapshot对象,并将其作为入参调用ContextManager.continued(contextSnapshot)。
2. ContextManager.continued(contextSnapshot)方法中解析出 Trace 的信息后,存储到线程B的线程上下文中。
SkyWalking Agent 会给被增强的类中扩展一个专用的属性的机制是这样的:这个类会被修改,实现了接口EnhancedInstance,此接口中提供了2个方法来读写这个扩展属性
public interface EnhancedInstance {
Object getSkyWalkingDynamicField();
void setSkyWalkingDynamicField(Object value);
}
这个扩展属性就是一个普通的 Object ,在宿主应用这边感知不到它的存在,因为它不是在宿主应用中定义的;但是在 Agent 的上下文中可将其作为数据载体,在如下这些场景使用:
setSkyWalkingDynamicField赋值给这个专用属性这种通过在对象中扩展专用属性来在上下文中传递一些信息的方式,我个人的使用感受是比 ThreadLocal 要更舒服,既能实现信息传递,也能解决跨线程的问题。而这个给类扩展专用属性的能力,在 Java Agent 运行期挂载的模式下是不具备的。因为如上节所述,JDK 的规范中,在运行期重定义一个类不允许新增、修改和删除成员变量。
插件化架构也叫作微内核架构,是一种面向功能进行拆分的可扩展性架构,通常用于存在多版本、多分支的客户端应用;由两个部分组成:核心系统(Core system)和插件模块(plug-in modules)
如下图所示:

SkyWalking Agent 中每个组件的埋点实现就是一个插件,对有多版本差异的组件可能有多个实现,分别对应不同的版本。SkyWalking Agent 对这些插件加载管理的核心流程如下:
初始化配置,并明确插件所在的路径。
在指定的路径下查找并解析 skywalking-plugin.def 插件文件。
通过独立的类加载器AgentClassLoader 加载插件中的类。
通过PluginFinder对插件进行管理分类。
使用 Byte Buddy 创建 AgentBuilder ,根据已加载插件的要求增强目标类,织入埋点的逻辑。
使用 JDK 的 SPI 加载并启动 BootService 服务。BootService可理解为我们通常编写的Service服务。
添加一个 JVM 钩子,在 JVM 退出时关闭所有 BootService 服务。
ClassLoader 是个抽象类,其子类 UrlClassLoader 引入 URL 资源概念,以此方式来约束自己只加载 URL 覆盖范围内的类文件;ExtClassLoader、AppClassLoader 都是 UrlClassLoader 的子类,各自分管的资源路径不同,即给定的 URL 不同则管辖范围不同,并通过委派执行类加载来保障这种分治能力,进而达到了类资源的隔离性。
上图是标准的委派机制,总结为2个方面:
下图描述了 SkyWalking Agent 通过独立的类加载器AgentClassLoader 加载插件中的类,不会对宿主应用中的类产生污染。
遇到过两种找不到 skywalking-agent.jar 中类文件的情况,解决他们依赖对ClassLoader 有较流畅的认知。
一个是 SpringBoot 版本(1.3.x)太低,升级版本解决了,排查定位的过程并不容易,因为这个版本已几乎消失殆尽,此处就不多说了。
另一个是应用的 Jar 被指定由 ExtClassLoader 加载,出现 skywalking-agent.jar 中的类如InstanceConstructorInterceptor找不到,下边这部分的内容若无需求可以跳过,请按需阅读;这里用正常情况和异常情况两方面来展示:
前置知识:
通过语法java -javaagent指定了启动时挂载 skywalking-agent.jar,这个jar中的类是被 AppClassloader 加载。
类加载具有传递性:如当需要加载一个类时,JVM默认会使用(当前需要使用此类的)调用者 Class 对象的 ClassLoader 来加载此类;举例:类A中 import 类B,那么类A的 ClassLoader 会去加载类B。
正常情况:
异常情况:

众多字节码修改框架中,Byte Buddy 灵活且强大,通过编写简单的 Java 代码即可创建自定义的运行时类;此外,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。代码生成库在生成高效代码和高效生成代码之间面临着折衷,Byte Buddy 侧重生成最少、最高效的运行时代码。对生成代码在运行期的高性能要求,应该也是 SkyWalking Agent 选择 Byte Buddy 的重要因素。下表是 Byte Buddy 官网给出的数据,显示了代码生成库的基本性能,以纳秒为单位,标准偏差在括号内附加:

做各个中间件的埋点插件,需要通过对源码梳理,找到关键拦截点,如什么类的什么方法,之后通过其参数、返回值、对象属性等获取构建 Span 的数据信息。在 Byte Buddy 中,编程人员可通过 ElementMatchers 识别一个或多个需要增强的类和方法,ElementMatchers 中提供了功能丰富的预定义拦截器(interceptor),在 SkyWalking Agent 中增强处理操作的代码中可以看到许多 xxxMatch ,分别是用于匹配类的ClassMatch和用于匹配方法的ElementMatcher,方便了插件的开发。
上文有提到 SkyWalking Agent 是启动时挂载,通过 Byte Buddy 中的一些匹配逻辑来判断新加载的类是否需要增强,如果需要则通过增强逻辑修改字节码,之后JVM则使用修改后的字节码创建对象。
通常,在 Java Agent 中做类型创建或操作不会对应用程序的长期运行产生重大影响;但类加载和类增强是运行此类代码最耗时且不可避免的步骤。近期跟一些SkyWalking的道友有过几次探讨,在接入 SkyWalking Agent 后,应用的启动耗时增加,这种时间损耗对实例数较多的应用在发布时很不友好,严重时会影响到正常的发布工作;这里有几个技术点可以作为优化切入:
如果在 Agent 端使用了 Kafka 通道来上报采集数据,那么 Kafka 的引入会带来较多的启动损耗,其中关于 topic 的检查部分可以考虑移除,毕竟 Kafka 中 topic 的准备应该是一次性的预处理工作。

不需要增强的类,不要进入 Byte Buddy 的增强匹配环节,在匹配插件前做过滤,这部分代码在SkyWalkingAgent#premain方法中。
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(allSkyWalkingAgentExcludeToolkit())
.or(ElementMatchers.isSynthetic()));
这段代码是实现过滤的,但是源码中列举的内容是有限的,从 SkyWalking Agent 中的插件情况我们可以发现,所提供插件的总数不过几十个,那么插件中需要增强处理的类其实也可能就几十个,如果你所在公司有 package 命名的规范,那么除了你公司规范所允许的比如 com.company.xxx + 组件插件中需要增强处理的几个类,其他的就都可以忽略:
MonitorFilter的类org.apache开头的类很多,只增强名字是xxx的几个类SkyWalking 作为一款优秀的国产APM系统,包括对 Cloud Native 架构下分布式系统的监控、追踪、诊断能力;本篇是介绍 Java 端的 SkyWalking Agent ,用一篇有限的文字是很难将其优点穷尽,一方面是个人的认知理解是有限的,另一方面其强大社区的背后,是无数专家仍在持续的为其注入前沿的、强大的新能力。作为 SkyWalking 的用户,从能力和信仰的视角来看, SkyWalking Agent 已然是应用程序非侵入式的守护者,接入它、用好它就能拥抱安逸。
如果这篇文章对您有所帮助,或者有所启发的话,帮忙点击公众号名片,关注公众号:「架构染色」,进行交流和学习。您的支持是我坚持写作最大的动力。
求一键三连:点赞、收藏、再看。
原文作者:架构染色;如有侵权,请联系管理员删除。