Appdash原始碼閱讀——Tracer&Span

cdh0805010118發表於2018-07-06

Appdash 是基於 basictracer-go 擴充套件實現的,在此基礎上豐富了的 Collector 和 Storage。以及增加了 dashboard UI 一些簡單的查詢統計功能。

Appdash 中的 Trace 和 Span struct 與 basictracer-go 中的 traceImpl 和 spanImpl struct 的關係,後面再看。 ::TODO

trace

basictracer-go 的擴充套件實現中,雖然是形成了完整的呼叫鏈,但是如果想要通過 dashboard 去進行繪圖這條呼叫鏈, 則只能通過 SpanRecorder 的 [] RawSpan,去通過演算法找到一條完整的鏈路,可能這個操作還非常耗時。

所以 Appdash 針對 Dashboard 的業務場景,定義了基於 Trace 的顯式鏈路結構:

// Appdash的Trace結構設計:採用了廣度遍歷思想,層次樹儲存。
// ChildOf和FollowsFrom理解:Sub列表為FollowsFrom關係,Trace和Sub列表為ChildOf關係
type Trace struct {
    Span // 根部span
    Sub []*Trace // child node
}

type Span struct {
    ID SpanID // SpanID= {TraceID, SpanID, ParentID}
    // Annotations攜帶了Span的所有資訊;
    // 包括Tags,Baggage,Logs,OperationName等;

    // 其中span.Log進行分類,Appdash自身已定義了:五種Log
    // SpanNameEvent, logEvent, msgEvent, timespanEvent和Timespan
    // 型別分別是:name, log, msg, timespan和TimeSpan
    // 其中後兩者的不同後面再理解 ::TODO
    // 對於Event也可以進行自定義實現, 但是Annotations不是直接的分類,需要對Annotations與Event進行資料轉換儲存才行
    Annotations // []{key:values}
}

// 對於span.Logs的型別定義,在Appdash中可以擴充套件,只需要實現Event interface, 並通過RegisterEvent方法註冊自定義span logs事件
// Schema方法的返回值,作為Log的key型別值
// 需要說明的一點是:內部的五種型別Log,具有一定的標識字首:
// SchemaPrefix = "_schema:"
type Event interface {
    Schema() string
}

func (t *Trace) String() string {
    ... // 序列化Trace資料
}

func (t *Trace) TreeString() string {
    ... // 列印樹資料,以樹結構的形式輸出。
}

// 從根節點開始遍歷遞迴查詢SpanID,返回Trace
// 可能大家有個疑問:為何返回Trace,而不是Span?
// 因為Trace型別是層次樹結構,所以每個節點都可以看成樹節點,等同看的話,這就是Trace。實際上是Span節點,但是我們通過任意的子節點,可以獲取到TraceID等呼叫鏈的全域性資訊。
func (t *Trace) FindSpan(spanID ID) *Trace {
    ... 
}

func (t *Trace) TimespanEvent() (TimespanEvent, error) {
    ... // 它是在當前呼叫鏈的span節點上,找到span logs中五類的TimespanEvent事件並返回
    // 這個過程在下節會詳細介紹
}

在 Appdash span logs 的分類與 Span 的 Annotations 的資料轉換與儲存比較複雜,我會單獨一節講解 span log 內部五類,並對資料轉換與儲存進行詳細介紹。

span

span 作為呼叫鏈樹中的子節點,它的儲存結構如下(Span 攜帶的 annotations 下節再詳細介紹):

type Span struct {
    // SpanID在序列化時String,變成了"TraceID/SpanID/ParentID"
    ID SpanID

    Annotations
}

// 對span進行序列化,資訊包括:TraceID/SpanID/ParentID,以及span的所有攜帶資訊:Tags、Baggages、Sampled、OperationName和Logs等
func (s *Span) String() string {
    ... // 通過json序列化
}

// 返回Span的OperationName,它是通過Annotations的key為"Name"獲取。
func (s *Span) Name() string {
    ...
}

// SpanID管轄Span本身的資訊,與basictracer-go的設計雷同。表現在:
//   basictracer-go中RawSpan包括了SpanContext,它並沒有把SpanID,TraceID、Sampled資訊放在Baggage中,而是單獨拎出來。
// 這裡的Appdash Span設計把TraceID、SpanID和ParentSpanID沒有放在Annotations資訊中獲取,也是單獨拎出來,更加清晰直觀,這個資訊是必須的,非攜帶資訊,而是Span自身資訊。
type SpanID struct {
    Trace ID
    Span ID
    Parent ID
}

func (id SpanID) string {
    ... // 序列化SpanID為"TraceID/SpanID/ParentSpanID"
    // 後面提到的tracesByIDSpan排序,則可以做到全域性Span排序,且這個排序結果是有層次的
    // 做到TraceID有序,SpanID有序,且都是連續的,例如:
    // TraceID_01/SpanID_01/0
    // TraceID_01/SpanID_02/SpanID_01
    // TraceID_01/SpanID_03/SpanID_02
    // TraceID_02/SpanID_01/0
    // TraceID_02/SpanID_02/SpanID_01
    // ...
    // 我們可以看到前面三個為一個整體Trace呼叫鏈,後面兩個為一個整體呼叫鏈,且全域性有序,trace中的Span有序,這個設計很棒!
}

// 這個猜測是用來序列化span資訊的,包括攜帶資訊Annotations
for (id SpanID) Format(s string, args ...interface{}) string{
    args = append([]interface{}{id.String()}, args...)
    return fmt.Sprintf(s, args...)
}

func (id SpanID) IsRoot() bool {
    return id.Parent == 0 // 校驗該span是否為根節點
}

// 封裝SpanID,通過protobuffer協議網路傳輸資料流
func (id SpanID) wire() *wire.CollectPacket_SpanID {
    return &wire.CollectPacket_SpanID {
        Trace: (*uint64)(&id.Trace),
        Span: (*uint64)(&id.Span),
        Parent: (*uint64)(&id.Parent),
    }
}

// 把protobuffer協議網路流轉換成SpanID資料
func spanIDFromWire(w *wire.CollectPacket_SpanID) SpanID {
    return SpanID{
        Trace: ID(*w.Trace),
        Span: ID(*w.Span),
        Parent: ID(*w.Parent),
    }
}

// 生成根節點的SpanID資訊
func NewRootSpanID() SpanID {
    return SpanID{
        Trace: generateID(),
        Span: generateID(),
    }
}

// 生成子節點的SpanID資訊
func NewSpanID(parent SpanID) SpanID {
    return SpanID {
        Trace: parent.Trace,
        Span:  generateID(),
        Parent: parent.Span,
    }
}

// 如果這個樹型別結構是這樣的,大家怎麼看呢?
// ::TODO 後面再理解
type Span struct {
    ParentID SpanID
    SpanID SpanID
    TraceID TraceID
    Annotations []{key-values}
    Sub []*Span
}

在 trace 程式碼中有個 sort.Interface 的介面實現型別, 內部是快排演算法。

// 排序演算法:只需要滿足三點,即可實現
// 1. 比較;2. 交換;3. 參與排序的元素數量。
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

type tracesByIDSpan []*Trace
// 這個型別是對Span列表進行ID排序,從小到大排列。這個意義後面再看,因為Trace樹層次本身就是按時間排序的,看看這個ID uint64型別值的生成演算法. 
// ::TODO
更多原創文章乾貨分享,請關注公眾號
  • Appdash原始碼閱讀——Tracer&Span
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章