SkyWalking源码-- Agent 链路追踪

172 阅读4分钟

本文基于 SkyWalking-Java-agent 8.15.0 版本

image.png

推荐文档

基本概念

Trace

概念:表示一整条链路(跨线程、跨进程的所有 Segment 的集合)
Trace 不是一个具体的数据模型,而是多个 Segment(SkyWalking中定义为 TraceSegment)串起来表示的逻辑对象

TraceSegment

基本概念

分布式链路追踪的节点。一个TraceSegment表示当前线程的一个节点。分布式链路跟踪是跨多个进程、跨多个线程的,导致了一条分布式跟踪的链路就会有多个 TraceSegment 组成

属性

  • String traceSegmentId:链路节点的id,每个 segment 都有一个全局唯一的id
  • TraceSegmentRef ref:segment的链路指针,指向当前segment的parent segment(上一级/父节点)
  • List<AbstractTracingSpan> spans:已完成的具体操作
  • DistributedTraceId relatedGlobalTraceId:当前segment所在Trace的id
  • boolean ignore = false:是否忽略
  • boolean isSizeLimited = false:是否有数量限制
  • final long createTime:创建时间

构造器

/**  
* Create a default/empty trace segment, with current time as start time, and generate a new segment id.  
* 创建默认的trace segment,当前时间作为开始时间,并生成一个新的 traceSegmentId  
*/  
public TraceSegment() {  
    this.traceSegmentId = GlobalIdGenerator.generate();  
    this.spans = new LinkedList<>();  
    this.relatedGlobalTraceId = new NewDistributedTraceId();  
    this.createTime = System.currentTimeMillis();  
}

全局id生产器

GlobalIdGenerator

属性

// 应用实例id  
private static final String PROCESS_ID = UUID.randomUUID().toString().replaceAll("-", "");  
// 线程id序号  
private static final ThreadLocal<IDContext> THREAD_ID_SEQUENCE = ThreadLocal.withInitial(  
() -> new IDContext(System.currentTimeMillis(), (short) 0));

核心方法

/**  
* 一个新id有三部分构成  
* 1、第一部分:应用实例 id  
* 2、第二部分:线程id  
* 3、第三部分:有两个部分构成  
*       1)时间戳  
*       2)当前线程里的序列号(0 - 9999) 
*/  
public static String generate() {  
    return StringUtil.join(  
        '.',  
        PROCESS_ID,  
        String.valueOf(Thread.currentThread().getId()),  
        String.valueOf(THREAD_ID_SEQUENCE.get().nextSeq())  
    );  
}

IDContext:用于线程序号的生成

  • 核心逻辑: timestamp() * 10000 + nextThreadSeq() 算法
  • 注意事项:考虑时间回拨
private static class IDContext {  
    // 上次生产 sequence 的时间戳  
    private long lastTimestamp;  
    // 线程的序列号  
    private short threadSeq;  

    // Just for considering time-shift-back only.  
    // 时间回拨  
    private long lastShiftTimestamp;  
    private int lastShiftValue;  

    private IDContext(long lastTimestamp, short threadSeq) {  
        this.lastTimestamp = lastTimestamp;  
        this.threadSeq = threadSeq;  
    }  

    private long nextSeq() {  
        return timestamp() * 10000 + nextThreadSeq();  
    }  

    private long timestamp() {  
        long currentTimeMillis = System.currentTimeMillis();  

        if (currentTimeMillis < lastTimestamp) { // 发生了时间回拨  
            // Just for considering time-shift-back by Ops or OS. @hanahmily 's suggestion.  
            if (lastShiftTimestamp != currentTimeMillis) {  
                lastShiftValue++;  
                lastShiftTimestamp = currentTimeMillis;  
            }  
            return lastShiftValue;  
        } else { // 时间正常  
            lastTimestamp = currentTimeMillis;  
            return lastTimestamp;  
        }  
    }  

    private short nextThreadSeq() {  
        if (threadSeq == 10000) {  
            threadSeq = 0;  
        }  
        return threadSeq++;  
    }  
}

分布式链路id

DistributedTraceId 表示一条分布式调用链。在一条链路中(Trace),无论请求分布于多少不同的进程中,这个 TraceId 都不会改变

public abstract class DistributedTraceId {  
   @Getter  
   private final String id;  
}

TraceSegmentRef:用于指向当前 Segment 的 Parent Segment

Span

概念:表示具体的一个操作

image.png

AsyncSpan

  • AbstractSpan prepareForAsync():span在当前跟踪上下文结束,但当前span在调用asyncFinish前 仍然是活的 。在活跃期间,可以在任何线程中更改span的标签、日志和属性。这个方法在以下场景下必须被调用
    • 1、在原始线程中(跟踪上下文)
    • 2、当前span处于活跃的
  • AbstractSpan asyncFinish():完成后通知 span

AbstractSpan

AbstractSpan 代表 span 的骨架,包含所有的公共方法。

/**  
* The <code>AbstractSpan</code> represents the span's skeleton, which contains all open methods.  
*/  
public interface AbstractSpan extends AsyncSpan {  
    /**  
    * Set the component id, which defines in {@link ComponentsDefine}  
    * 指定当前 Span 表示的操作发生在哪个插件上  
    *  
    * @return the span for chaining.  
    */  
    AbstractSpan setComponent(Component component);  

    /**  
    * 指定当前 Span 表示的操作所在的插件属于哪一种 skywalking 划分的类型  
    * @param layer  
    * @return  
    */  
    AbstractSpan setLayer(SpanLayer layer);  

    /**  
    * Set a key:value tag on the Span.  
    *  
    * @return this Span instance, for chaining  
    * @deprecated use {@link #tag(AbstractTag, String)} in companion with {@link Tags#ofKey(String)} instead  
    */  
    @Deprecated  
    AbstractSpan tag(String key, String value);  

    AbstractSpan tag(AbstractTag<?> tag, String value);  

    /**  
    * Record an exception event of the current walltime timestamp.  
    * walltime 挂钟时间,本地时间  
    * serverTime 服务器时间  
    *  
    * @param t any subclass of {@link Throwable}, which occurs in this span.  
    * @return the Span, for chaining  
    */  
    AbstractSpan log(Throwable t);  

    AbstractSpan errorOccurred();  

    /**  
    * @return true if the actual span is an entry span.  
    */  
    boolean isEntry();  

    /**  
    * @return true if the actual span is an exit span.  
    */  
    boolean isExit();  

    /**  
    * Record an event at a specific timestamp.  
    *  
    * @param timestamp The explicit timestamp for the log record.  
    * @param event the events  
    * @return the Span, for chaining  
    */  
    AbstractSpan log(long timestamp, Map<String, ?> event);  

    /**  
    * Sets the string name for the logical operation this span represents.  
    * 如果当前 Span 的操作是  
    * 一个 HTTP 请求,那么 operationName 就是请求的 URL  
    * 一个 SQL 请求,那么 operationName 就是 SQL 的类型  
    * 一个 Redis 请求,那么 operationName 就是 Redis 命令  
    *  
    * @return this Span instance, for chaining  
    */  
    AbstractSpan setOperationName(String operationName);  

    /**  
    * Start a span.  
    *  
    * @return this Span instance, for chaining  
    */  
    AbstractSpan start();  

    /**  
    * Get the id of span  
    *  
    * @return id value.  
    */  
    int getSpanId();  

    String getOperationName();  

    /**  
    * Reference other trace segment.  
    *  
    * @param ref segment ref  
    */  
    void ref(TraceSegmentRef ref);  

    AbstractSpan start(long startTime);  

    /**  
    * 什么叫 peer,就是对端地址  
    * 一个请求可能跨多个进程,操作多种中间件,那么每一次 RPC,对端的服务地址就是 remotePeer  
    * 每一次中间件的操作,中间件的地址就是 remotePeer  
    * @param remotePeer  
    * @return  
    */  
    AbstractSpan setPeer(String remotePeer);  

    /**  
    * @return true if the span's owner(tracing context main thread) is been profiled.  
    */  
    boolean isProfiling();  

    /**  
    * Should skip analysis in the backend.  
    */  
    void skipAnalysis();  
}

SpanLayer:span 分层,SkyWalking对不同数据源的分类

  • DB(1):存储
  • RPC_FRAMEWORK(2):rpc框架
  • HTTP(3):http请求
  • MQ(4):消息队列
  • CACHE(5):缓存

OfficialComponent:SkyWalking 支持的官网组件
ComponentsDefine:定义了所有 SkyWalking 支持的 官网组件 清单列表

StackBasedTracingSpan

基于栈的 TracingSpan,

  • int stackDepth:栈深度
  • String peer:对端地址

LocalSpan

用于记录一个本地方法的调用,在一个Segment里,会有多个LocalSpan

public class LocalSpan extends AbstractTracingSpan {  
  
    public LocalSpan(int spanId, int parentSpanId, String operationName, TracingContext owner) {  
        super(spanId, parentSpanId, operationName, owner);  
    }  

    @Override  
    public boolean isEntry() {  
        return false;  
    }  

    @Override  
    public boolean isExit() {  
        return false;  
    }  

    @Override  
    public AbstractSpan setPeer(String remotePeer) {  
        return this;  
    }
}

EntrySpan

  • 在一个 TraceSegment 里面只能存在一个 EntrySpan
  • 后面的插件复用前面插件创建的 EntrySpan 时会覆盖掉前面插件设置的 Span 信息
  • EntrySpan 记录的信息永远是最靠近服务提供侧的信息
public class EntrySpan extends StackBasedTracingSpan {  
  
private int currentMaxDepth; // 最大栈深

...
/**  
* Set the {@link #startTime}, when the first start, which means the first service provided.  
* EntrySpan 只会由第一个插件创建,但是后面的插件复用 EntrySpan 时都要来调用一次 start() 方法  
* 因为每一个插件都以为自己是第一个创建 EntrySpan 的  
*/  
@Override  
public EntrySpan start() {  
    if ((currentMaxDepth = ++stackDepth) == 1) {  
        super.start();  
    }  
    clearWhenRestart();  
    return this;  
}
...
}

ExitSpan

区别就在于 EntrySpan 记录的是更靠近服务这一侧的信息。ExitSpan 记录的是更靠近消费这一侧的信息

  • 所谓 ExitSpan 和 EntrySpan 一样采用复用的机制,前提是在插件嵌套的情况下
  • 多个 ExitSpan 不存在嵌套关系,是平行存在的时候,是运行同时存在多个 ExitSpan
  • 把 ExitSpan 简单理解为离开当前进程/线程的操作
  • TraceSegment 里不一定非要有 ExitSpan

image.png

链路追踪上下文

TracingContext 管理

  • 当前 Segment 和自己前后的 Segment 的引用 TraceSegmentRef
  • 当前 Segment 内的所有 span

image.png