在 APM 和全链路监控领域,Skywalking 是非常有名的项目,我司使用的就是该方案来进行应用性能监控和分布式链路跟踪。而我本人最近的工作和 Skywalking 也高度相关,因此,lz想以本文来作为这段时间,对关于 Skywalking 的知识点进行总结和分享,包括插件机制的原理,核心领域模型的分析,异步 trace 可能存在的问题,编写复杂插件时如何避免采坑,如何基于 Skywalking 打造全链路压测等等。如果不当,还请指出,不吝赐教。另外,本文只关注 Skywalking Java Agent,关于 Skywalking 其他的组件,不在本文探讨之列。
Skywalking Java Agen 使用 Java premain 作为 Agent 的技术方案,关于 Java Agent,其实有 2 种,一种是以 premain 作为挂载方式(启动时挂载),另外一种是以 agentmain 作为挂载方式,在程序运行期间随时挂载,例如著名的 arthas 就是使用的该方案;agentmain 会更加灵活,但局限会比 premain 多,例如不能增减父类,不能增加接口,新增的方法只能是 private static/final 的,不能修改字段,类访问符不能变化。而 premian 则没有这些限制。
另外,agentmain 的挂载方式,对性能是有影响的,他的工作原理是启动一个新的进程,触发ClassFileLoadHook 事件,然后修改正在运行的字节码,那如果这个类正在运行怎么办呢?JVM 会在安全点暂停所有线程,然后触发我们编写的 Agent 钩子,并重新转换字节码。而在暂停所有现场的过程中,程序就会产生可能不可控的延迟。
另外说一个题外话,关于 Redefine 和 Retransform 的区别,前者会覆盖掉被修改的内容,后者会保留被修改的内容。Redefine 是 Java 1.5 引入的,Retransform 是 Java 1.6 引入的。Redefine 有很多缺陷,例如 Redefine 后的类不能恢复,不能修改删除 field 和 method,包括方法参数,名称和返回值。Jdk 1.6 的 Retransform 则解决了这些问题。关于 Retransform 和 Redefine,可以参考 arthas 作者的一些文章介绍。
回到 Skywalking 上面,Skywalking 是在 premian 方法中类加载时修改字节码的。使用 ByteBuddy 类库(基于 ASM)实现字节码插桩修改。入口类 SkyWalkingAgent#premain 。
Skywalking Agent 整体结构基于微内核的方式,即插件化,apm-agent-core 是核心代码,负责启动,加载配置,加载插件,修改字节码,记录调用数据,发送到后端等等。而 apm-sdk-plugin 模块则是各个中间件的插装插件,比如 Jedis,Dubbo,RocketMQ,Kafka 等各种客户端。
如果想要实现一个中间件的监控,只需要遵守 Skywalking 的插件规范,编写一个 Maven 模块就可以。Skywalking 内核会自动化的加载插件,并插桩字节码。
Skywalking 的作者曾说:不管是 Linux,Istio 还是 SkyWalking ,都有一个很大的特点:当项目被「高度模块化」之后,贡献者就会开始急剧的提高。
而模块化,插件化,也是一个软件不容易腐烂的重要特性。Skywalking 的就是遵循这个理念设计。
Skywalking 如何加载插件的呢? Skywalking 的插件在 maven 打包完成后,会自动放在 plugins 目录下,Skywalking 在启动时,会使用自定义的 AgentClassLoader 进行插件加载,该 ClassLoader 重写了findclass 方法(并没有破坏双亲委派模型)。启动时,Skywalking 就会查找所有的 skywalking-plugin.def 文件,并使用默认的 AgentClassLoader 加载这些文件里定义的插件元数据类,来映射目标 class 和拦截 class 的关系(代码位置 PluginBootstrap#loadPlugins )。此时真正的拦截插件并不会加载,这些映射规则,则是插件开发者自己定义的。
在 Skywalking 中,每个业务 classLoader 实例,都会对应一个新的 AgentClassLoader。哪些是业务ClassLoader呢?比如 sun.misc.Launcher$AppClassLoader,org.springframework.boot.loader.LaunchedURLClassLoader,sun.misc.Launcher$ExtClassLoader, sun.reflect.DelegatingClassLoader,业务自己创建的 ClassLoader 等等。
而 AgentClassLoader 的路径则是 plugins 和 activations 目录,AgentClassLoader 可以在这 2个路径下查找 Class。
当加载一个类时,比如 Jedis,那么就会触发 javaAgent 的 Instrumentation 钩子,Instrumentation 内部则实现了一整套逻辑。Skywalking 会检查是否有 Jedis 的插件(这个规则是Jedis 插件里的 skywalking-plugin.def 定义,此文件在启动时就加载了),如果有,就使用一个新的 AgentClassLoader (parent 是目标类加载器)来加载拦截器,并将拦截器插入到调用方法的前面和后面(代码位置 ClassEnhancePluginDefine#enhance)。
为什么要用一个新的 AgentClassLoader 呢?假设不用 AgentClassLoader,用默认的 AgentClassLoader,这个 AgentClassLoader 的 parent 是 JDK AppClassLoader,而如果 Jedis 是 一个自定义类加载器加载的,且插件里又访问 Jedis 这个类,因为 AgentClassLoader 是无法访问到 Jedis 这个类文件的,因此只能向上查找,向上查找到 AppClassLoader,肯定是查不到的,因为 Jedis 是自定义类加载器加载的。
如下图:

而如果我们使用一个新的 AgentClassLoader,并将其 parent 设置为 Jedis 的 ClassLoader,则可以解决这个问题,如下图:

插件分为 3 种:构造器插件,静态方法插件,实例方法插件。分别是 InstanceConstructorInterceptor 接口,StaticMethodsAroundInterceptor 接口, InstanceMethodsAroundInterceptor 接口。
我们随便点开一个插件,例如 HttpClient 插件:

该插件在拦截器代码里访问 apache http 的类。我们可以在其执行execute方法时,拦截到请求参数,并进行解析。根据 Skywalking 的规范,设置各种标签和 Span。关于 Span ,下面会单独详解。
Skywalking 是全链路追踪和 APM 插件,我们这里先讨论全链路跟踪,暂时不讨论 APM。自从 google 2010 发布 Dapper 论文以来,各种全链路跟踪插件如雨后春笋般的出现。Skywalking是其中优秀的代表。
全链路跟踪一般有几个概念 Trace,Span。Trace 代表了一次调用所产生的链路,并且会有一个全局唯一的 ID,在 google的论文中,他是一组 span 的集合,Span 表示一个组件的调用信息,是整个 Trace 中的一个节点,他的 ID 在 trace 中是唯一的。
一般 Span 的结构是这样的 (伪代码):
class Span {
int id; // 自身 Span 的 ID
int parentId; // 父 Span 的 ID
String name; // Span 的名称
String traceId; // 全局 traceID
Date startTime; // span 的启动时间
Date endTime; // span 的执行结束时间
}
Segment 是 Skywalking 代码里的独有概念,他表示的是一个 JVM 里一个线程里的一次调用链路,通常会有多个 Span。SKywalking Agent 代码中是没有 Trace 实体的,Trace 其实就是多个 Segment 连接成的一个东西。
一个 Segment 由多个 Span 组成,当一个线程一次调用运行结束了,那么这个 Segment 就结束了(非异步场景),SKywalking 就会把这个调用信息返回到后端统计服务 OAP 中,此时,就可以通过 web 页面进行搜索查看了。
我们来看下代码是怎么写的,首先看 Segment,该类全称是 TraceSegment
public class TraceSegment {
private String traceSegmentId;
private List<TraceSegmentRef> refs;
private List<AbstractTracingSpan> spans;
private DistributedTraceIds relatedGlobalTraces;
private final long createTime;
public TraceSegment() {
this.traceSegmentId = GlobalIdGenerator.generate();
this.spans = new LinkedList<>();
this.relatedGlobalTraces = new DistributedTraceIds();
this.relatedGlobalTraces.append(new NewDistributedTraceId());
this.createTime = System.currentTimeMillis();
}
}
traceSegmentId: 表示自身作为 Segment 的全局唯一 ID;
refs:每次有新的流量进入 JVM,都会创建一个新的 Segment,如果他的前面还是有一个 JVM 的话,那么就将前面这个 JVM 的 Segment 保存到 refs 链表中(新版本已经不是链表了,只是一个单对象,链表可能会导致内存泄漏),这样就将 Segment 串联起来了。
spans:在 JVM 中运行 Span 节点,都会保存到 spans 中。
relatedGlobalTraces:第一个节点生成的唯一 ID,也就是 TraceID;注意,虽然构造方法这里赋值了,但是后面会调用其 Set 方法,将其覆盖。
Span 结构是怎么样的呢?Span 种类比较多,分为入口 Span(例如 Tomcat 入口,SpringMVC 入口),出口 Span(DB 客户端,Jedis 客户端,Http 客户端),本地方法 Span(本地函数);
SKywalking 抽象的 Span 代码如下:
public abstract class AbstractTracingSpan implements AbstractSpan {
protected int spanId; // 自身 ID,从0开始
protected int parentSpanId; // 父 span ID
protected List<TagValuePair> tags; // 执行过程中,记录的数据
protected String operationName; // 名字
protected volatile boolean isInAsyncMode = false; // 是否为异步模式
private volatile boolean isAsyncStopped = false; // 异步是否停止
protected final TracingContext owner; // 持有该 Span 的上下文
protected long startTime; // 开始时间
protected long endTime; // 结束时间
protected boolean errorOccurred = false; // 是否发生了错误
protected int componentId = 0; // Span 组件 ID
protected List<LogDataEntity> logs; // 日志
protected List<TraceSegmentRef> refs; // 父 Segment
}
可以看到,SKywalking 的 Span 设计和大部分设计是差不多的。我们注意到有个 TracingContext,这是一个关键对象,用来维护一次调用过程中,所有 Span 的生命周期。
TracingContext 属性:
public class TracingContext implements AbstractTracerContext {
private TraceSegment segment; // 当前调用的 Segment
// 当前调用的所有 Span,使用链表维护,模拟栈的进出
private LinkedList<AbstractSpan> activeSpanStack = new LinkedList<>();
private int spanIdGenerator; // id 生成器
private volatile int asyncSpanCounter; 异步计数器
private volatile boolean isRunningInAsyncMode; 是否为异步模式
private volatile ReentrantLock asyncFinishLock; 异步执行锁
private volatile boolean running; 是否结束
private final long createTime; 创建时间
}
此类的关键就是 activeSpanStack,其使用链表模拟了栈的进出,为什么使用栈的结构呢?使用栈结构能够更方便的管理 Span 的生命周期。在 SKywalking 中,一个 Span 创建成功,就是入栈操作,该 Span 执行结束,则是出栈操作。当这个栈空了,表示这个 Segment 执行结束了。
具体如下图所示:

上图中,显示了 SKywalking 中如何管理 Span 的生命周期:当第一个 Span 创建时,例如 Tomcat Span,则会放到栈底,当 Jedis Span 对外访问时(例如执行 get 命令),则放在栈顶。当 Jedis 操作执行结束时,则会出栈,当 ThreadSpan 执行 Run 方法结束时,也会出栈,当访问 Tomcat 的请求执行结束时,则也会出栈,直至栈为空。当栈为空,则会将这些 Span 发送到后端 OAP server 进行保存。
然后我们总结下 trace Segment span 的关系:

大体上,就是这样的一个关系。
前面我们了解了 Span 和 Segment 的原理,其实还有一点,SKywalking Agent 用来存储 Span 的容器是 ThreadLocal,便于在单个线程中,随时取出 Span 对象。当栈为空时,则会删除 ThreadLocal 对象,防止内存泄漏。
那如果是异步 Trace,该怎么办呢?SKywalking 提供了 capture 和 continued(snapshot),前者表示将当前栈顶的 Span 复制并返回一个快照,continued 表示将快照恢复为当前栈顶 Span 的父 Span,以此来完成 Span 和 Span 之间的链接。
例如,当我们使用异步线程执行任务时,SKywalking 在默认情况下,是无法链接当前线程的 Span 和异步线程的 Span 的,除非我们在 Runnable 实现类使用 TraceCrossThread 类似的注解,表示这个 Runnable 需要跨线程追踪,那么,SKywalking 就会做出 capture 和 continued(snapshot) 操作,将主线程的 Span+Segment 复制到 Runnable 中,并将这 2 个 Span 进行链接。如下图

上图中,主线程复制当前线程 Segment 和 Span 的基本信息,包括 Segment ID,Span ID,Name 等信息。然后在子线程中,进行回放,回放的操作,就是将这个 快照 的信息,保存到 Span 的父 Span 中,标记子线程的父 Span 就是这个 Span。
还有一种场景的异步 Span,比如在 A 线程开启,在 B 线程关闭,我们需要记录这个 Span 的耗时。比方说,异步 HttpClient,我们在主线程开启了访问,在异步线程得到结果,就复合刚刚我们说的场景。
SKywalking 为我们提供了 prepareForAsync 和 asyncFinish 这两个方法,当我们在 A 线程创建了一个 Span,我们可以执行 span.prepareForAsync 方法,表示这个 span 开始了访问,即将进入异步线程。当在 B 线程得到结果后,执行 span.asyncFinish 则表示,这个 span 执行结束了,那么, A 线程就可以将整个 Segment标记结束,并返回到 OAP server 中进行统计。那么如何在 B 线程里得到这个 Span 的实例,然后调用 asyncFinish 方法呢?实际上,是需要插件开发者自己想办法传递的,比如在被拦截对象的参数里、构造函数里传递。
那么这 2 种异步模式的区别是什么呢?说实话,我在刚刚看到这两个的时候,脑子也有点迷糊,经过总结,发现两者虽然看起来相似,当谁也代替不了谁。
简单来说,prepareForAsync 和 asyncFinish 只是为了统计一个 Span 跨越 2 个线程的场景,例如上面的提到 HttpAsyncClient 场景。在 A 线程创建,在 B 线程结束,我们需要在 B 线程拿到返回值和耗时。
而 capture 和 continued(snapshot) 的使用场景是为了连接 2 个线程的不同 Span。我们将主线程的最后一个 Span 和子线程的第一个 Span 相连接。
而两者也是可以结合使用。如下图:

以上,表示了一次 HttpAsyncClient 请求中,如何将 Span 进行跨线程连接,并记录返回值。

最终的效果如上。
在使用 SKywalking 的过程中,我也写过一些公司内部的插件,如果是同步调用的话,就比较简单,例如,在 before 方法中创建一个 span(就是向栈中推入一个 Span),在 after 方法中,执行 stop span(就是从栈中弹出一个 Span)。
当编写异步插件时,需要考虑的情况就比较复杂。有几个点需要注意:
当我们执行 capture 和 continued 时,栈顶一定要有 Span。这样才能将这两个 Span 进行链接。
当我们执行 prepareForAsync 异步时,一定要在其他线程执行 asyncFinish,否则这个 Segment 就会断开,因为如果不执行 asyncFinish,这个 Segment 就不会 finish,也就不会发送到后端 OAP。另外,对一个 Span 执行完 prepareForAsync 后,一定不要忘记执行这个 span 的 stop 方法。
一定要正确的调用 ContextManager.stopSpan(),否则,一定会出现内存泄漏。假设,Tomcat Span 是入口,在 Tomcat 插件的 after 方法里,执行了 stopSpan,但是栈却没有清空,那么 ThreadLocal里的对象就不会清除,当下次在这个线程里调用 continued时,continued 会将其他线程的对象继续添加到这个线程里的 Segment 列表里。导致内存无限增大(新版本限制了链表的大小,但没有从根本解决问题)。
SKywalking 是基于 java agent 技术打造的,而 java agent 又非常的适合开发全链路压测产品,那么,是否可以借助 SKywalking 的现有能力开发出全链路压测呢?答案是可以的。
全链路压测的核心问题是压测的过程中不能有脏数据,当影子流量进入容器,这些流量不能进入正式的数据库。通常的做法是,例如在执行 SQL 的时候,判断是否是影子流量,如果是,则更换 SQL 数据源,即不能在正式库中执行影子 SQL。
基于 SKywalking 的目前的实现,我们只需要对一个类实现多个插件即可,并将这些插件进行包装,基于过滤器模式进行串联,实现对一个类的 压测增强 和 全链路Trace增强。
以上,就是本人这段时间,对 SKywalking(8.1.0) 学习和使用的总结。SKywalking 版本升级的很快,现在已经是 8.9.0 版本了,又有了很多功能的更新,大家可以参考的看看。
参考:
opentracing 规范 https://github.com/opentracing/specification/blob/master/specification.md#the-opentracing-data-model
Instrumentation API https://www.matools.com/api/java8
Arthas源码分析--jad反编译原理 https://hengyun.tech/arthas-jad/
Skywalking原理分析 http://www.bewindoweb.com/306.html
JVM 源码分析之 javaagent 原理完全解读 https://www.infoq.cn/article/javaagent-illustrated/
SkyWalking源码分析https://www.processon.com/view/link/611fc4c85653bb6788db4039#map
SKywalking Java Agent 源码地址 https://github.com/apache/skywalking-java
最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总
Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图
关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭3年前。Improvethisquestion我正处于学习Ruby的阶段,我想查看一些小型库的源代码以了解它们是如何构建的。我不知道什么是小型图书馆,但希望SO能推荐一些易于理解的图书馆来学习。因此,如果有人知道一两个非常小的库,这是新手Rubyists学习的好例子,请推荐!我想使用Manveru'sInnatelib,因为它试图保持在2000LOC以下,但我还不熟悉其中经常使用的Ruby速记。也许大约100-5
由于匿名block和散列block看起来大致相同。我正在玩它。我做了一些严肃的观察,如下所示:{}.class#=>Hash好的,这很酷。空block被视为Hash。print{}.class#=>NilClassputs{}.class#=>NilClass为什么上面的代码和NilClass一样,下面的代码又显示了Hash?puts({}.class)#Hash#=>nilprint({}.class)#Hash=>nil谁能帮我理解上面发生了什么?我完全不同意@Lindydancer的观点你如何解释下面几行:print{}.class#NilClassprint[].class#A
我很难理解Ruby中sender和receiver的实际含义。它们一般是什么意思?到目前为止,我只是将它们理解为方法调用和获取其返回值的调用。但是,我知道我的理解还远远不够。谁能给我一个Ruby中发送者和接收者的具体解释? 最佳答案 面向对象中的一个核心概念是消息传递和早期概念化,这在很大程度上借鉴了计算的Actor模型。艾伦·凯(AlanKay)创造了面向对象一词并发明了最早的OO语言之一SmallTalk,他拥有voicedregretatusingatermwhichputthefocusonobjectsinsteadofo
rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:
我在某些代码中遇到了三元组,但我无法理解条件:str.split(/',\s*'/).mapdo|match|match[0]==?,?match:"somestring"end.join我确实理解我是在某些点上拆分字符串并将总结果转换为数组,然后依次处理数组的每个元素。除此之外,我不知道发生了什么。 最佳答案 一种(稍微)不那么令人困惑的写法是:str.split(/',\s*'/).mapdo|match|ifmatch[0]==?,matchelse"somestring"endend.join我认为多行三元语句很糟糕,尤其是
有没有人成功地将S3存储桶读取为子文件夹?文件夹1--子文件夹2----文件3----文件4--文件1--文件2文件夹2--子文件夹3--文件5--文件6我的任务是读取文件夹1。我希望看到子文件夹2、文件1和文件2,但看不到文件3或文件4。现在,因为我将存储桶键限制为prefix=>'folder1/',你仍然会得到file3和4,因为它们在技术上具有folder1前缀。似乎真正做到这一点的唯一方法是吸收folder1下的所有键,然后使用字符串搜索从结果数组中实际排除file3和file4。有没有人有过这方面的经验?我知道像Transmit和Cyberduck这样的FTP风格的S3
关于yolov5训练时参数workers和batch-size的理解yolov5训练命令workers和batch-size参数的理解两个参数的调优总结yolov5训练命令python.\train.py--datamy.yaml--workers8--batch-size32--epochs100yolov5的训练很简单,下载好仓库,装好依赖后,只需自定义一下data目录中的yaml文件就可以了。这里我使用自定义的my.yaml文件,里面就是定义数据集位置和训练种类数和名字。workers和batch-size参数的理解一般训练主要需要调整的参数是这两个:workers指数据装载时cpu所使
我在理解GrapeAPI时遇到很多困难,特别是route_param以及它如何仅使用params。考虑这段代码:desc"Returnastatus."paramsdorequires:id,type:Integer,desc:"Statusid."endroute_param:iddogetdoStatus.find(param[:id])endend这个街区产生什么路线?我知道这是一个get请求,但为什么它被包裹在route_paramblock中?为什么它不能在paramsblock中? 最佳答案 你的block产生这条路线: