经过前面的介绍,我们知道了Skywalking上报的数据其实是Span,那么根据这些Span就可以将整条链路组装完成。接下来就看一下Skywalking是怎么组装的吧。
在组装之前,我们先看看每个零部件(Span)的结构吧。
public abstract class AbstractTracingSpan implements AbstractSpan {
protected int spanId; // 每个Span在Segment中的序号
protected int parentSpanId; // 父级Span的序号
protected List tags;
protected String operationName; // 这个Span的标示,比如一个Http请求入口,这个就是url
protected int operationId;
protected SpanLayer layer;
protected boolean isInAsyncMode = false;
protected volatile AbstractTracerContext context;
protected long startTime; // Span的开始时间
protected long endTime; // Span的结束时间
protected boolean errorOccurred = false;
protected int componentId = 0;
protected String componentName;
protected List logs;
protected List refs; // 上一个Segment的引用
}
这个是 Skywalking 中AbstractTracingSpan
的定义,这里对接下来我们需要的几个属性进行了注释。
通过上面的定义我们知道了几点:
- 在Segment中每个Span都有一个SpanId,这些SpanId通过parentSpanId可以在Segment范围内形成一个具有层级的树结构
- 不同Segemnt通过
List
串联起来。refs
所以只有EntrySpan
中才会有refs
。
按照上面所说的,每个EntrySpan
除了链路最开始的那个,应该只有一个TraceSegmentRef
,为什么AbstractTracingSpan
中却定义的却是一个List呢?也就是为啥会有多个爸爸呢?
是的,对于大部分的请求(比如一次RPC)EntrySpan
只有一个爸爸,但是不排除一些EntrySpan
搞事情,竟然出去认了干爹。
好了,不扯淡了,那么什么情况下会出现一个EntrySpan
会有多个TraceSegmentRef
呢?
在一些批处理或使用了消息队列的场景下就会出现这种情况,我们就拿消息队列来说明吧。
消费者批量消费消息,而这些消息分别来自不同的生产者,这种情况下,消费者这个EntrySpan
就会是多个TraceSegmentRef
。
那么在有消息队列这种场景下,怎么将链路串联呢?可以看到,这里其实已经是条链路了,所以如果Consumer上报数据,能够将多条Traces数据存储下来的,那么每条Trace的数据都能完成的保存。
但是,就我看Skywalking oap-server(agent会将数据上报给oap-server )的源代码,在SegmentParseV2
这个类中处理上报数据时
SegmentParseV2
public boolean parse(BufferData bufferData, SegmentSource source) {
createSpanListeners();
UpstreamSegment upstreamSegment = bufferData.getMessageType();
UpstreamSegment upstreamSegment = bufferData.getMessageType();
List traceIds = upstreamSegment.getGlobalTraceIdsList();
if (bufferData.getV2Segment() == null) {
bufferData.setV2Segment(parseBinarySegment(upstreamSegment));
}
SegmentObject segmentObject = bufferData.getV2Segment();
SegmentDecorator segmentDecorator = new SegmentDecorator(segmentObject);
if (!preBuild(traceIds, segmentDecorator)) {
...
}
}
private boolean preBuild(List traceIds, SegmentDecorator segmentDecorator) {
...
for (UniqueId uniqueId : traceIds) {
notifyGlobalsListener(uniqueId);
}
}
private void notifyGlobalsListener(UniqueId uniqueId) {
spanListeners.forEach(listener -> {
if (listener.containsPoint(SpanListener.Point.TraceIds)) {
((GlobalTraceIdsListener)listener).parseGlobalTraceId(uniqueId, segmentCoreInfo);
}
});
}
SegmentSpanListener
public void parseGlobalTraceId(UniqueId uniqueId, SegmentCoreInfo segmentCoreInfo) {
...
segment.setTraceId(traceIdBuilder.toString());
}
可以看到对于每一个上报的UpstreamSegment
,都会创建一个SegmentSpanListener
,但是在SegmentSpanListener
的segment
属性却只有一个,所以Agent上报了多条TraceId
,但是到了SegmentSpanListener
只保存了一条TraceId
。
所以在这种情况,消费者每次上报的Segment
涉及到的多条Trace
只有一条是完整的,其他的Trace
到生产者这条链路就断掉了。
可能对于消息队列这种场景我的理解还不到位,如有不对的,请各位指正。
上面说的只是跨进程的情况,还有一种跨线程的情况需要说明一下。
由于Skywalking Agent 使用的是ThreadLocal保存的Segment
数据,所以跨线程执行时,其实就是一个新的Segment
了,只不过这个跨线程的Segement
的入口不是EntrySpan
而是LocalSpan
。