本章着重介绍链路数据(TraceSegment)是如何从被监控服务端进行采集,整合,输出这一过程,主要涉及到对探针内部启动的服务ContextManager以及链路数据管理的上下文TracingContext的介绍。
在本章主要内容开篇之前,我们先要明确在可观测系统整体的业务中,java-agent探针主要承担了哪几个功能:
1.采集各个服务的链路数据,包括不限于执行片段的基础信息(如请求地址,服务地址,调用方式,调用时间,调用中间件类型等),异常堆栈信息日志,拓扑关联信息等。
2.获取JVM里面的monitor的部分监控数据并定时上报,主要为该服务JVM的监控数据,以及部分主机的基础监控数据。
3.获取Meter类型的非标准指标数据,通过toolkit的埋点数据,自定义数据等
4.获取被监控服务的日志数据(需要在服务中引入sw的toolkit依赖),并于链路数据通过traceId进行关联。
5.对采集数据的解析,流转,格式转换,序列化,缓存等;收集到的数据上报,远程通信。
6.对于上级系统(例如OAP等)下发配置的接收和更改,主要也是通过gRPC的通道实现的。
对于全链路监控系统而言,主要的核心即在于Trace,Logging,Metrics这三种数据之间的协同作用和分析,对于这部分更深入的介绍大家可以参考可观测性和可观测系统(Observability System)相关的资料和书籍。
本章介绍的ContextManager以及TracingContext等主要负责的是Trace层面的数据处理工作。
agent探针中Trace类型数据采集业务的过程逻辑大致如下图所示:
其中:
plugin:各个中间件,框架,链路等链路数据采集的插件,主要集中定义在apm-sdk-plugin下
ContextManager:链路上下文的管理服务,通过SPI方式启动,启动位置为premain()方法中ServiceManager单例对象的boot()方法
ContextManagerExtendService:链路上下文管理扩展服务,主要用于动态创建链路上下文实例对象。
EntrySpan,LocalSpan,ExitSpan:跨度数据片段,承载一次调用或操作的链路信息,为整条链路数据串联的最基本单位,承载数据的第二小单位(最小的单位为Tag标签,存储一条key-value映射结构的属性值)
TracingContext(或者IgnoredTracerContext):链路数据上下文,承担一个请求在该服务上所有操作的链路数据的采集工作,以及链路片段数据转化的工作。每次有请求发起到服务时会新建一个链路上下文的实例对象。当不需要采集该部分对象时,可以创建一个空上下文对象IgnoredTracerContext,不执行数据采集业务,两者同样继承自AbstractTracerContext的接口实现。
TraceSegment:链路数据片段,当本次请求的链路数据采集结束时,会将TracingContext采集到的数据组装成该格式的对象,并通过监听器触发发送给客户端服务。
TracingContextListener:注册该监听器在TraceSegmentServiceClient客户端中,当Context数据采集完成时调用finish()方法触发监听器事件,将数据加入缓存队列中等待客户端读取。
TraceSegmentServiceClient:链路数据上报的客户端服务,通过grpc协议将链路片段数据TraceSegment转化为protocolv3的格式进行网络传输。
其中还有一些细节的处理流程没有全部标注出来(比如跨线程,跨进程,跨服务的数据,缓存数据等),下面会对这些内容的功能和源代码进行一一介绍。
1.ContextManager源代码解析
ContextManager是链路上下文TracingContext的管理服务,实现代码如下所示:
public class ContextManager implements BootService {
private static final String EMPTY_TRACE_CONTEXT_ID = "N/A";
private static final ILog LOGGER = LogManager.getLogger(ContextManager.class);
private static ThreadLocal<AbstractTracerContext> CONTEXT = new ThreadLocal<AbstractTracerContext>();
private static ThreadLocal<RuntimeContext> RUNTIME_CONTEXT = new ThreadLocal<RuntimeContext>();
private static ContextManagerExtendService EXTEND_SERVICE;
private static AbstractTracerContext getOrCreate(String operationName, boolean forceSampling) {
AbstractTracerContext context = CONTEXT.get();
if (context == null) {
if (StringUtil.isEmpty(operationName)) {
if (LOGGER.isDebugEnable()) {
LOGGER.debug("No operation name, ignore this trace.");
}
context = new IgnoredTracerContext();
} else {
if (EXTEND_SERVICE == null) {
EXTEND_SERVICE = ServiceManager.INSTANCE.findService(ContextManagerExtendService.class);
}
context = EXTEND_SERVICE.createTraceContext(operationName, forceSampling);
}
CONTEXT.set(context);
}
return context;
}
private static AbstractTracerContext get() {
return CONTEXT.get();
}
/**
* @return the first global trace id when tracing. Otherwise, "N/A".
*/
public static String getGlobalTraceId() {
AbstractTracerContext context = CONTEXT.get();
return Objects.nonNull(context) ? context.getReadablePrimaryTraceId() : EMPTY_TRACE_CONTEXT_ID;
}
/**
* @return the current segment id when tracing. Otherwise, "N/A".
*/
public static String getSegmentId() {
AbstractTracerContext context = CONTEXT.get();
return Objects.nonNull(context) ? context.getSegmentId() : EMPTY_TRACE_CONTEXT_ID;
}
/**
* @return the current span id when tracing. Otherwise, the value is -1.
*/
public static int getSpanId() {
AbstractTracerContext context = CONTEXT.get();
return Objects.nonNull(context) ? context.getSpanId() : -1;
}
/**
* @return the current primary endpoint name. Otherwise, the value is null.
*/
public static String getPrimaryEndpointName() {
AbstractTracerContext context = CONTEXT.get();
return Objects.nonNull(context) ? context.getPrimaryEndpointName() : null;
}
public static AbstractSpan createEntrySpan(String operationName, ContextCarrier carrier) {
AbstractSpan span;
AbstractTracerContext context;
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
if (carrier != null && carrier.isValid()) {
SamplingService samplingService = ServiceManager.INSTANCE.findService(SamplingService.class);
samplingService.forceSampled();
context = getOrCreate(operationName, true);
span = context.createEntrySpan(operationName);
context.extract(carrier);
} else {
context = getOrCreate(operationName, false);
span = context.createEntrySpan(operationName);
}
return span;
}
public static AbstractSpan createLocalSpan(String operationName) {
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
AbstractTracerContext context = getOrCreate(operationName, false);
return context.createLocalSpan(operationName);
}
public static AbstractSpan createExitSpan(String operationName, ContextCarrier carrier, String remotePeer) {
if (carrier == null) {
throw new IllegalArgumentException("ContextCarrier can't be null.");
}
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
AbstractTracerContext context = getOrCreate(operationName, false);
AbstractSpan span = context.createExitSpan(operationName, remotePeer);
context.inject(carrier);
return span;
}
public static AbstractSpan createExitSpan(String operationName, String remotePeer) {
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
AbstractTracerContext context = getOrCreate(operationName, false);
return context.createExitSpan(operationName, remotePeer);
}
public static void inject(ContextCarrier carrier) {
get().inject(carrier);
}
public static void extract(ContextCarrier carrier) {
if (carrier == null) {
throw new IllegalArgumentException("ContextCarrier can't be null.");
}
if (carrier.isValid()) {
get().extract(carrier);
}
}
public static ContextSnapshot capture() {
return get().capture();
}
public static void continued(ContextSnapshot snapshot) {
if (snapshot == null) {
throw new IllegalArgumentException("ContextSnapshot can't be null.");
}
if (!snapshot.isFromCurrent()) {
get().continued(snapshot);
}
}
public static AbstractTracerContext awaitFinishAsync(AbstractSpan span) {
final AbstractTracerContext context = get();
AbstractSpan activeSpan = context.activeSpan();
if (span != activeSpan) {
throw new RuntimeException("Span is not the active in current context.");
}
return context.awaitFinishAsync();
}
/**
* Using this method will cause NPE if active span does not exist. If one is not sure whether there is an active span, use
* ContextManager::isActive method to determine whether there has the active span.
*/
public static AbstractSpan activeSpan() {
return get().activeSpan();
}
/**
* Recommend use ContextManager::stopSpan(AbstractSpan span), because in that way, the TracingContext core could
* verify this span is the active one, in order to avoid stop unexpected span. If the current span is hard to get or
* only could get by low-performance way, this stop way is still acceptable.
*/
public static void stopSpan() {
final AbstractTracerContext context = get();
stopSpan(context.activeSpan(), context);
}
public static void stopSpan(AbstractSpan span) {
stopSpan(span, get());
}
private static void stopSpan(AbstractSpan span, final AbstractTracerContext context) {
if (context.stopSpan(span)) {
CONTEXT.remove();
RUNTIME_CONTEXT.remove();
}
}
@Override
public void prepare() {
}
@Override
public void boot() {
}
@Override
public void onComplete() {
}
@Override
public void shutdown() {
}
public static boolean isActive() {
return get() != null;
}
public static RuntimeContext getRuntimeContext() {
RuntimeContext runtimeContext = RUNTIME_CONTEXT.get();
if (runtimeContext == null) {
runtimeContext = new RuntimeContext(RUNTIME_CONTEXT);
RUNTIME_CONTEXT.set(runtimeContext);
}
return runtimeContext;
}
public static CorrelationContext getCorrelationContext() {
final AbstractTracerContext tracerContext = get();
if (tracerContext == null) {
return null;
}
return tracerContext.getCorrelationContext();
}
}
ContextManager继承自启动父类接口BootService,因此对启动服务类的4个通用方法prepare(),boot(),onComplete(),shutdown()都进行了实现。
ContextManager新定义的方法:
getOrCreate() 拿到或者创建新的链路上下文的实例,每个线程仅保留一个静态实例对象
这个静态方法在创建各个span实例的方法中会被调用,判断如果ThreadLocal中不为空,则取到当前线程ThreadLocal中的链路上下文实例。当ThreadLocal中为空时,继续判断:当操作名为空的时候,记录错误日志并创建IgnoredTracerContext上下文实例,跳过本次的链路信息采集;如果操作名参数正常传入不为空时,通过ServiceManager.INSTANCE.findService()方法获取当前运行的ContextManagerExtendService单例服务的对象实例,然后调用它的createTraceContext()方法创建上下文对象。我们来看一下这个方法的内部逻辑:
public AbstractTracerContext createTraceContext(String operationName, boolean forceSampling) {
AbstractTracerContext context;
/*
* Don't trace anything if the backend is not available.
*/
if (!Config.Agent.KEEP_TRACING && GRPCChannelStatus.DISCONNECT.equals(status)) {
return new IgnoredTracerContext();
}
int suffixIdx = operationName.lastIndexOf(".");
if (suffixIdx > -1 && Arrays.stream(ignoreSuffixArray)
.anyMatch(a -> a.equals(operationName.substring(suffixIdx)))) {
context = new IgnoredTracerContext();
} else {
SamplingService samplingService = ServiceManager.INSTANCE.findService(SamplingService.class);
if (forceSampling || samplingService.trySampling(operationName)) {
context = new TracingContext(operationName, spanLimitWatcher);
} else {
context = new IgnoredTracerContext();
}
}
context.getReadablePrimaryTraceId();
return context;
}
这里做了几个判断:
1.!Config.Agent.KEEP_TRACING && GRPCChannelStatus.DISCONNECT.equals(status)这里表示如果探针内部KEEP_TRACING这个配置(用于控制探针连接状态),或者grpc和OAP后管连接状态断开的时候,agent系统判定无法正常上报数据,因此这里会直接创建IgnoredTracerContext实例直接忽略数据采集流程。
2.suffixIdx这个参数的判断是用于skywalking系统内置的链路忽略功能(之后OAP源码解析的时候会讲到),如果当前操作的名称和链路忽略配置的名称列表(存在ignoreSuffixArray对象中)中某项匹配成功的话,该链路也忽略采集
3.这里只要forceSampling和trySampling返回的一个值是true,则创建正常链路上下文对象,否则忽略采集。其中forceSampling赋值为true的情况只有createEntrySpan的部分逻辑里出现,其他均为false;trySampling()方法用来判断采样率保护配置,方法代码如下:
public boolean trySampling(String operationName) {
if (on) {
int factor = samplingFactorHolder.get();
if (factor < samplingRateWatcher.getSamplingRate()) {
return samplingFactorHolder.compareAndSet(factor, factor + 1);
} else {
return false;
}
}
return true;
}
这里事实上用到了观察者模式,在采样服务SamplingService里有一个samplingRateWatcher对象,这个对象在服务初始化时注册到了ConfigurationDiscoveryService服务中,当发现采样配置改变时,会触发modify事件,动态调整samplingRate的值。
这个方法的逻辑是:通过samplingFactorHolder取得当前context中已经进行了采样的计数,如果这个计数小于samplingRate限制值,则factor+1;如果大于等于samplingRate值,则返回false。
当forceSampling和trySampling都返回false时,创建IgnoredTracerContext实例,忽略本次数据采样。
关于skywalking的远程配置同步ConfigurationDiscoveryService(简称CDS)功能,会在之后OAP源码的解析中进行介绍,大家也可以通过阅读官方文档了解。
官方文档链接:CDS - Configuration Discovery Service | Apache SkyWalking
createEntrySpan() 创建新的EntrySpan对象实例
span是链路数据收集中节点的最小单位,可以代表一次操作,或者一个方法,一个过程的执行,
在skywalking中根据的服务请求流动的方向分为EntrySpan、LocalSpan和ExitSpan。三种span都继承自抽象类AbstractTracingSpan。其中EntrySpan表示请求流入端(Server端),用来表示请求在服务(接收端)的入口链路。一般createEntrySpan方法会在服务框架类型的采集插件(例如spring,tomcat,netty等的plugin)中调用;消息队列类型的中间件,Consumer消费端的节点也会用EntrySpan。一个TraceSegment片段中只会有一个EntrySpan。
public static AbstractSpan createEntrySpan(String operationName, ContextCarrier carrier) {
AbstractSpan span;
AbstractTracerContext context;
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
if (carrier != null && carrier.isValid()) {
SamplingService samplingService = ServiceManager.INSTANCE.findService(SamplingService.class);
samplingService.forceSampled();
context = getOrCreate(operationName, true);
span = context.createEntrySpan(operationName);
context.extract(carrier);
} else {
context = getOrCreate(operationName, false);
span = context.createEntrySpan(operationName);
}
return span;
}
createLocalSpan() 创建新的LocalSpan对象实例
LocalSpan用于服务的本地方法执行的信息采集,比如通过toolkit埋点的定制方法采集,定时任务等都是创建这个span
public static AbstractSpan createLocalSpan(String operationName) {
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
AbstractTracerContext context = getOrCreate(operationName, false);
return context.createLocalSpan(operationName);
}
createExitSpan() 创建新的ExitSpan对象实例
ExitSpan用于从该服务出发跨进程到其他服务,中间件,数据库,第三方平台等调用过程的信息采集,这时候植入agent的服务的角色是client,作为请求的发起方,而数据库,中间件,rpc目标等作为被请求的一方(server端)。所以skywalking-ui展示面板中,对缓存性能的监控又叫虚拟缓存,因为他的性能指标是通过采集被监控服务对缓存中间件请求的性能数据(响应时间,成功率等)进行分析得到的结果,而不是直接监控缓存中间件(例如通过在缓存服务器中加入Prometheus)得出的结果。
public static AbstractSpan createExitSpan(String operationName, ContextCarrier carrier, String remotePeer) {
if (carrier == null) {
throw new IllegalArgumentException("ContextCarrier can't be null.");
}
operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD);
AbstractTracerContext context = getOrCreate(operationName, false);
AbstractSpan span = context.createExitSpan(operationName, remotePeer);
context.inject(carrier);
return span;
}
inject() 将当前链路上下文的部分(快照)信息注入到ContextCarrier中,仅在当前ActiveSpan为ExitSpan时执行
public static void inject(ContextCarrier carrier) {
get().inject(carrier);
}
extract()将ContextCarrier中的父节点链路信息装配到链路上下文的TraceSegmentRef对象中,一般用于跨(进程)服务调用(如rpc,feign,kafka等)中的下一级的链路,当前ActiveSpan为EntrySpan时执行
public static void extract(ContextCarrier carrier) {
if (carrier == null) {
throw new IllegalArgumentException("ContextCarrier can't be null.");
}
if (carrier.isValid()) {
get().extract(carrier);
}
}
capture() 创建当前链路上下文的信息快照
continued()父线程的链路信息快照传递到该线程,继续链路采集流程。一般用于异步调用或者跨线程的链路数据处理中
public static void continued(ContextSnapshot snapshot) {
if (snapshot == null) {
throw new IllegalArgumentException("ContextSnapshot can't be null.");
}
if (!snapshot.isFromCurrent()) {
get().continued(snapshot);
}
}
awaitFinishAsync()用于异步,跨线程链路的情况,等待采集流程结束执行finish方法。
public static AbstractTracerContext awaitFinishAsync(AbstractSpan span) {
final AbstractTracerContext context = get();
AbstractSpan activeSpan = context.activeSpan();
if (span != activeSpan) {
throw new RuntimeException("Span is not the active in current context.");
}
return context.awaitFinishAsync();
}
stopSpan()停止span的信息采集,一般用于整个span数据采集完成时
isActive()判断当前链路上下文是否存在(不为空)
以下为获取某一对象&元素的getter方法,由于逻辑较为简单在此不多作介绍:
get()获取当前线程的链路上下文实例对象
getGlobalTraceId()获取traceId
getSegmentId()获取当前TraceSegment片段的id
注意:globalTraceId和segmentId并不是同一个东西,segmentId指的就是这个TraceSegment片段的id,当该实例初始化时会自动生成,globalTraceId是整条Trace链路的全局追迹id,这个值会在初始化的时候生成,但是如果该链路片段是有父节点的情况下,会替换为父节点传递过来的traceId与前方保持一致。
生成方法的代码如下所示:
private static final String PROCESS_ID = UUID.randomUUID().toString().replaceAll("-", "");
private static final ThreadLocal<IDContext> THREAD_ID_SEQUENCE = ThreadLocal.withInitial(
() -> new IDContext(System.currentTimeMillis(), (short) 0));
public static String generate() {
return StringUtil.join(
'.',
PROCESS_ID,
String.valueOf(Thread.currentThread().getId()),
String.valueOf(THREAD_ID_SEQUENCE.get().nextSeq())
);
}
可以看得出来这里追迹id是根据指定的格式拼接生成的。
getSpanId()获取当前ActiveSpan的spanId
getPrimaryEndpointName()获取私有端点名称
activeSpan()获取当前活跃的span实例对象,一般为span存储集合的栈顶元素
getRuntimeContext()获取运行上下文对象
getCorrelationContext()获取CorrelationContext对象实例,该对象内可以自定义加入部分数据,通过sw8系列协议加入请求头进行跨服务传递
2.ContextManagerExtendService源码解析
@DefaultImplementor
public class ContextManagerExtendService implements BootService, GRPCChannelListener {
private volatile String[] ignoreSuffixArray = new String[0];
private volatile GRPCChannelStatus status = GRPCChannelStatus.DISCONNECT;
private IgnoreSuffixPatternsWatcher ignoreSuffixPatternsWatcher;
private SpanLimitWatcher spanLimitWatcher;
@Override
public void prepare() {
ServiceManager.INSTANCE.findService(GRPCChannelManager.class).addChannelListener(this);
}
@Override
public void boot() {
ignoreSuffixArray = Config.Agent.IGNORE_SUFFIX.split(",");
ignoreSuffixPatternsWatcher = new IgnoreSuffixPatternsWatcher("agent.ignore_suffix", this);
spanLimitWatcher = new SpanLimitWatcher("agent.span_limit_per_segment");
ConfigurationDiscoveryService configurationDiscoveryService = ServiceManager.INSTANCE.findService(
ConfigurationDiscoveryService.class);
configurationDiscoveryService.registerAgentConfigChangeWatcher(spanLimitWatcher);
configurationDiscoveryService.registerAgentConfigChangeWatcher(ignoreSuffixPatternsWatcher);
handleIgnoreSuffixPatternsChanged();
}
@Override
public void onComplete() {
}
@Override
public void shutdown() {
}
public AbstractTracerContext createTraceContext(String operationName, boolean forceSampling) {
AbstractTracerContext context;
/*
* Don't trace anything if the backend is not available.
*/
if (!Config.Agent.KEEP_TRACING && GRPCChannelStatus.DISCONNECT.equals(status)) {
return new IgnoredTracerContext();
}
int suffixIdx = operationName.lastIndexOf(".");
if (suffixIdx > -1 && Arrays.stream(ignoreSuffixArray)
.anyMatch(a -> a.equals(operationName.substring(suffixIdx)))) {
context = new IgnoredTracerContext();
} else {
SamplingService samplingService = ServiceManager.INSTANCE.findService(SamplingService.class);
if (forceSampling || samplingService.trySampling(operationName)) {
context = new TracingContext(operationName, spanLimitWatcher);
} else {
context = new IgnoredTracerContext();
}
}
context.getReadablePrimaryTraceId();
return context;
}
@Override
public void statusChanged(final GRPCChannelStatus status) {
this.status = status;
}
public void handleIgnoreSuffixPatternsChanged() {
if (StringUtil.isNotBlank(ignoreSuffixPatternsWatcher.getIgnoreSuffixPatterns())) {
ignoreSuffixArray = ignoreSuffixPatternsWatcher.getIgnoreSuffixPatterns().split(",");
}
}
}
ContextManagerExtendService实现了服务启动接口BootService和grpc通道监听器接口GRPCChannelListener,同时声明了两个观察者对象IgnoreSuffixPatternsWatcher,SpanLimitWatcher(原生系统下)
其中比较重要的方法一个是前面讲过的createTraceContext()用于链路上下文创建
还有就是事件触发状态值变更方法:(spanLimit的值直接通过动态在SpanLimitWatcher内部修改,无需handle方法进一步处理)
statusChanged()(grpc连接状态)
handleIgnoreSuffixPatternsChanged()(链路忽略匹配字符串集)
其实根据上文对链路上下文创建方法的介绍可以发现,这几个配置值其实都是用来对创建哪种上下文对象实例判断使用的,进而控制是否忽略数据采集。个人理解单独耗费资源启动这个服务的目的就是能够动态的更改某些值,从而实现动态控制链路上下文的创建(因为ContextManager里面的create方法为静态的)。
3.TracingContext,IgnoredTracerContext,TraceSegment的数据结构级各部分数据生成的逻辑
4.ContextCarrier及链路跨线程,跨进程传递的逻辑实现(跨进程传播协议)