Appdash原始碼閱讀——Store儲存

cdh0805010118發表於2018-07-07

解析完 Collector 後,本節講解 Appdash 的後端儲存,目前 Appdash 支援的後端儲存全部都是基於記憶體的儲存,也可以自定義對接後端儲存,比如:mysql、redis 等,需要做一些 Collect 方法資料儲存的適配工作{SpanID, Annotations}

Store

為了豐富 Collect 服務本地儲存和網路儲存,提供了可自定義實現的 Store interface;同時也提供了一些基於記憶體的本地儲存 localmemory 等;

// SpanID與Annotations的資料儲存
type Store interface {
    Collector

    Trace(ID) (*Trace, error)
}

// 在做Dashboard時的查詢條件
// 支援基於時間範圍和TraceID列表的查詢操作
type TracesOpts struct {
    Timespan Timespan

    TraceIds []ID
}

// 查詢介面,通過TraceOpts實現Trace條件搜尋, 返回符合條件的Trace列表。
type Queryer interface {
    Traces(opts TracesOpts) ([]*Trace, error)
}

// Trace聚合資料, 資料通過Aggregator interface獲取
type AggregateResult struct {
    RootSpanName string

    Average, Min, Max, StdDev time.Duration

    Samples int64

    Slowest []ID
}

// 聚合操作, 它也是指定時間範圍內進行Trace資料聚合操作,返回聚合資料列表
// 注意一點:這裡的時間範圍是指:過去一段時間範圍
type Aggregator interface{
    // Aggregate(-72*time.Hour, 0)
    Aggregate(start, end time.Duration) ([]*AggregateResult, error)
}

持久化儲存

持久化儲存也就是把資料最終儲存到磁碟上。PersistentStore interface 是表示持久化儲存,它在 Store interface 的基礎上新增了 ReadFrom 和 Write 方法,用於持久化儲存。

Appdash 持久化儲存的實現包括:MemoryStore

type PersistentStore interface{
    Write(io.Writer) error
    ReadFrom(io.Reader) (int64, error)
    Store
}

我們知道,無論是業務微服務的 agent 採集,或者 Collector 服務端,都是已 Recorder 單元進行資料處理,並沒有形成完整的 trace 呼叫鏈。所以 Storage 首先就是要把離散的 Span,聚合成一條條完整的 trace 列表。這樣在 Dashboard 中顯示時才能夠形成呼叫鏈列表。那麼在 Store 處理資料過程中,就會有插入分裂節點操作等

MemoryStore

type MemoryStore struct {

    trace map[ID]*Trace // trace ID -> trace tree

    span map[ID]map[ID]*Trace // trace ID -> span ID -> sub trace tree

    sync.Mutex // 併發操作
}

這個 MemoryStore 比較有意思,Trace 型別我們在《Appdash 原始碼閱讀——Tracer&Span》文章中介紹過,它是一個 Trace 呼叫鏈完整的層次樹。那麼通過 trace 的 map 結構,我們可以通過 TraceID 獲取整顆完整的 trace 樹,這個 map 結構的 trace 儲存的是一顆顆完整的 trace 樹。

span 的 map 結構儲存的資料是通過 TraceID 可以獲取到每個子節點的樹,也就是把上一個 map 結構的 trace 展開儲存,後者只能通過遞迴形式獲取到某個 span 節點,但是這個 map 結構的 span 可以獲取到全域性到任一 span 節點,也就是說查詢到的 TraceID 列表各個 span 的時間複雜度 o(1)。 而前者是 logN。

所以 trace 和 span 擁有的 span 節點是相同的,也就相當於 2 倍 Trace 儲存。

上面這個結構設計,不太合理。

我們再看 MemoryStore 的建立、節點插入和讀取等操作

// 建立一個MemoryStore例項
func NewMemoryStore() *MemoryStore {
    return &MemoryStore{
        trace: map[ID]*trace{},
        span: map[ID]map[ID]*trace{},
    }
}

// 這裡提一個小技巧,如果我們想確認自定義的後端儲存是否實現了Store、或者PersistentStore介面,可以在編譯時確定,做法如下, 這種做法非常常見

var _ interaface {
    Store
    Queryer
} = (*MemoryStore)(nil)

// 儲存Recorder
func (ms *MemoryStore) Collect(id SpanID, anns ...Annotation) error {
    ms.Lock()
    defer ms.Unlock()
    return ms.collectNoLock(id, anns...)
}

// recorder資料轉換儲存到MemoryStore中
func (ms *MemoryStore) collectNoLock(id SpanID, anns ...Annotation) error {
    // 增加span節點
    if _, present := ms.span[id.Trace]; !present {
        ms.span[id.Trace] = map[ID]*Trace{}
    }

    // insert || update span
    s, present := ms.span[id.Trace][id.Span]
    if !present {
        s = &Trace{
            Span: Span{
                ID: id,
                Annotations: anns,
            },
        }
        ms.span[id.Trace][id.Span] = s
    } else {
        s.Annotations = append(s.Annotations, anns...)
        return nil
    }

    // insert || update trace tree
    root, present := ms.trace[id.Trace]
    if !present {
        ms.trace[id.Trace] = s
        root = s
    }

    // 後面一段程式碼做了整個層次樹的重建。
    // 做法:
    // 1. 如果當前trace的TraceID已存在,不管目前儲存的trace tree根節點是否為真正的root節點; 分三種情況:
    //    (1). 新來的span是真正的根節點
    //    (2). 當前根節點的父親是新來的span
    //    (3). 當前根節點的父親不是新來的span
    // 前兩者歸為一類, 都把新來的span作為根節點處理
    // 對於(1), (2)表明已存在Trace,且新來的span是根節點的父親,則做樹的重建, 新的root為span
    if ... {
        // 改變根節點為新的span
        ms.trace[id.Trace] = root
        // 調整原來合適但不正確的depth=2的子節點,到新的root下。
        ms.reattachChildren(root, oldRoot)
        // 調整MemoryStore下的span
        ms.insert(root, oldRoot)


    }
    // 2. 當前trace不存在和(3)點歸為一類:
    //    不做處理

    // 對於這個演算法,後面有詳細介紹,便於大家理解
    ...

    // 儲存span到MemoryStore的span合適位置上
    if !id.IsRoot() && s != root {
        ms.insert(root, s)
    }

    // 調整原來root下depth=2的子節點
    if s != root {
        ms.rettachChildren(s, root)
    }
    return
}

// 插入span到ms的span儲存中
func (ms *MemoryStore) insert(root, t *Trace) {
    p, present := ms.span[t.ID.Trace][t.ID.Parent]
    if present {
        // 如果能夠找到父親,則直接新增在父親的子節點列表中
        p.Sub = append(p.Sub, t)
    } else {
        // 如果不能找到父親,則直接新增到root的子節點中,暫存。
        root.Sub = append(root.Sub, t)
    }
}

// 調整原來root的depth=2相關子節點
// 由於dst和src已在前面建立好了關係,所以這裡只是對depth=2的子節點進行適當調整
// dst與src的鏈條不會變化
func (ms *MemoryStore) rettachChildren(dst, src *Trace) {
    var sub2 []*Trace

    for _, sub := range src.Sub {
        if sub.Span.ID.Parent == dst.Span.ID.Span {
            // 該節點和dst是父子關係,直接提上去
            dst.Sub = append(dst.Sub, sub)
        } else {
            sub2 = append(sub2, sub)
        }
    }
    // 因為src下面子節點已發生改變,所以直接賦予新的子節點列表即可
    src.Sub = sub2
}

針對 collectNoLock 方法,有個疑問。我們看到首先對 span 進行儲存操作,當發現 span 節點已存在時,在追加 Annotations 後就直接返回了,但是 trace 的 span 子節點並沒有追加 Annotations。所以會導致 trace 和 span 的子節點資料不一致。

在 collectNoLock 後面程式碼對 trace 層次樹進行重建的原因在於:

並不是建立第一個 trace id 時,它就是根節點,它也可能在網路中滯留了,導致 span 子節點先到,而 root 節點後到,如果不重建,則會導致呼叫鏈的亂序,或者無序。這顆樹的重建依賴於各個 span 的 parentspanid 值

我們要理解層次樹的重建,要搞清楚幾點:

  1. 重建後的樹也不一定是有序的層次樹;
  2. 每次重建後,如果新來的 span 尚不能放在正確的位置,對於 MemoryStore 的 trace 儲存,span 就放在臨時根節點的子節點上,距離為 1.也就是 root.Sub 列表中。
  3. 每次重建後,還不能放在正確位置的 span,對於 MemoryStore 的 span 儲存,span 就存放在根節點的 Sub 列表中

所以對於已存在的 trace,如果新到來的 span 還不能存放在正確的位置,那就存放在 depth=2 的節點上。

再加上一些對於 MemoryStore 的方法列表,包括查詢、刪除等操作

// 根據TraceID,獲取MemoryStore中trace對應ID的呼叫鏈
func (ms *MemoryStore) Trace(id ID) (*Trace, error) {
    ms.Lock()
    defer ms.Unlock()
    return ms.traceNoLock(id)
}

func (ms *MemoryStore) traceNoLock(id ID) (*Trace, error) {
    t, present:= ms.trace[id]
    ...
}

// MemoryStore在開頭說到編譯時也校驗,它實現了Queryer介面
// 這裡雖然實現了Traces查詢功能,但是並沒有使用輸入引數。這個是全域性輸出所有的呼叫鏈
func (ms *MemoryStore) Traces(opts TraceOpts) ([]*Trace, error) {
    ms.Lock()
    defer ms.Unlock()

    for id := range ms.trace {
        t, err := ms.traceNoLock(id)
        ts = append(ts, t)
    }
    ...
}

// 刪除MemoryStore中的trace和span
func (ms *MemoryStore) Delete(traces ...ID) error {
    ms.Lock()
    defer ms.Unlock()
    return ms.deleteNoLock(traces...)
}

func (ms *MemoryStore) deleteNoLock(traces ...ID) error {
    for _, id := range traces {
        delete(ms.trace, id)
        delete(ms.span, id)
    }
    return nil
}

// 刪除MemoryStore中的span,以及對應trace中的span
// 這裡存在一個問題,為何有這種刪除完整呼叫鏈的需求?只刪除Annotations,我還可以理解。
// 如果刪除Span,則會導致這個Span的所有子節點全部失聯的情況。
func (ms *MemoryStore) deleteSubNoLock(s SpanID, annotationsOnly bool) bool {
    ... 
}

// 因為MemoryStore實現了持久化儲存PersistentStore, 所以存在io.Writer和io.Reader操作
// 使用gob進行序列化寫入
func (ms *MemoryStore) Write(w io.Writer) error {
    ... //併發

    data :=memoryStoreData{m.trace, m.span}
    return gob.NewEncoder(w).Encode(data)
}

// 從io流讀取資料到記憶體MemoryStore中
func (ms *MemoryStore) ReadFrom(r io.Reader) (int64, error) {
    ... // 併發

    var data memoryStoreData
    gob.NewDecoder(r).Decode(&data)
    ms.trace = data.Trace
    ms.span = data.Span
    return int64(len(ms.trace)), nil
}

從 Appdash 中,我們可以看到對於併發操作,都會存在兩個方法一個是 XXX(), 另一個則是 XXXNoLock() ,如果存在併發操作,則使用前者,如果非併發,則使用後者。

另一個場景則是,對於 mysql 事務處理,在業務中也可以提供兩類操作,一類是帶事務,一類是不帶事務操作。

PersistentStore

持久化儲存操作,前面提到如果要記憶體資料持久化到本地,則需要實現 PersistetStore 介面,記憶體本地化操作如下:

// 通過實現PersistentStore介面的Storage,通過對應的介面把io流定時寫入到檔案中。
// 這個是對全域性呼叫鏈資訊的持久化,定期全域性覆蓋備份, 在沒有完成備份前,會寫入到臨時檔案中
func PersistentEvery(s PersistentStore, interval time.Duration, file string) {
    for {
        time.Sleep(interval)

        f, err := ioutil.TempFile("", "appdash")

        s.Write(f)
        s.Close()
        os.Rename(f.Name(), file)
    }
}
更多原創文章乾貨分享,請關注公眾號
  • Appdash原始碼閱讀——Store儲存
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章