Appdash原始碼閱讀——RecentStore和LimitStore

cdh0805010118發表於2018-07-12

Appdash 除了提供 PersistentStore 可持久化檔案儲存外,還提供了基於記憶體的限制儲存兩種,RecentStore 和 LimitStore, 這兩種的 Span Recorder 都有生命週期,到了一定時間或者大小,Appdash 認為這些 Recorder 不會再使用,刪除它們。

當然基於記憶體的 Span Recorder 儲存肯定有很多弊端,比如:報表統計做不到全域性,只能看近期;針對第一種 RecentStore 儲存,它是以時間為維度,進行 Span Recorder 的過期刪除工作。缺點:1. 延展性差;2. 報表具有區域性性等;延展性差是指,當業務量小時,Span Recorder 本身就不多,則生命週期內的 Recorder 就少,不聚集。另一個,當業務量猛增時,生命週期長,則週期內的 Span Recorder 就非常多,容易造成記憶體瓶頸;對於第二種 LimitStore 儲存,它是以記憶體佔用大小為維度,進行 Span Recorder 刪除。它的缺點是當業務量猛增時,Span Recorder 很容易突破設定的記憶體限制,則資料量不全,或者搜尋 traceid 時,無法搜尋到。

因為記憶體儲存的 Span Recorder 都是有限制的,所以肯定會有 Span Recorder 刪除行為,則 Appdash 提供了相關刪除介面

type DeleteStore interface{
    // 因為是基於Store interface操作
    Store

    // 提供了Delete刪除Span Recorder操作
    Delete(...ID) error
}

RecentStore

type RecentStore struct {
    // 基於時間週期的SpanRecorder刪除
    MinEvictAge time.Duration

    DeleteStore

    // 帶有Span Recorder的生命週期管理
    created map[ID]int64

    // 上一次刪除的時間
    lastEvicted time.Time

    mu sync.Mutex
}
// 由上可以看出,RecentStore是利用MinEvictAge和lastEvicted兩個時間,來對Span Recorder進行生命週期的管理。

func (rs *RecentStore) Collect(id SpanID, anns ...Annotation) error) {
    ... // 併發
    // 儲存新來的Span Recorder
    if _, present := rs.created[id.Trace]; !present {
        rs.created[id.Trace] = time.Now().UnixNano()
    }
    // 刪除過期的Span Recorder列表
    if time.Since(rs.lastEvicted) > rs.MinEvictAge {
        rs.evictBefore(time.Now().Add(-1*rs.MinEvictAge))
    }

    return rs.DeleteStore.Collect(id, anns...)
}

// 刪除小於t時間的Span Recorder。
func (rs *RecentStore) evictBefore(t time.Time) {
    evictStart := time.Now()

    rs.lastEvicted = evictStart

    tnano := t.UnixNano()

    var toEvict []ID
    for id, ct := range rs.created{
        if ct < tnano {
            toEvict = append(toEvict, id)
            delete(rs.created, id)
        }
    }

    if len(toEvict) ==0 {
        return
    }

    // 刪除記憶體中的Span Recorder列表
    go func(){
        rs.DeleteStore.Delete(toEvict...)
    }()
    return
}

由 RecentStore 的 Collect 可以看出,雖然刪除和儲存 Span Recorder 是非同步操作。理論上應該是單獨的 goroutine 去對所有收集到的 Span Recorder 的生命週期進行管理,否則,缺點兩個:

  1. 如果沒有新的 Span Recorder 到來,則無法觸發過期的 Span Recorder 刪除。
  2. 如果過期時間 MinEvictAge 設定得很小,則不斷新到來的 Span Recorder 會不斷觸發 goroutine 操作,這個也是不合理的。

LimitStore

type LimitStore struct {
    // 儲存的Span Recorder最大數量
    Max int

    DeleteStore

    mu sync.Mutex

    // 帶Max的Span Recorder生命週期管理
    traces map[ID]struct{}

    // 通過環狀儲存管理
    ring []int64

    // 下一個插入Span Recorder位置
    nextInsertIdx int
}
// 對於LimitStore儲存,它是基於儲存Span Recorder最大數量的維度來維護所有的Span Recorder。它是通過traces、ring和nextInsertIdx兩個儲存來維護的。traces用來表示trace是否存在;ring和nextInsertIdx用來表示環插入trace維護

其實,環traces的維護只需要ring陣列和nextInsertIdx兩個變數來維護,但是在ring陣列中查詢traceid太慢,所以引入了traces map結構儲存,時間複雜度為o(1)

func (ls *LimitStore) Collect(id SpanID, anns ...Annotation) error {
    ... // 併發
    if ls.ring ==nil {
        ls.ring = make([]int64, ls.Max)
        ls.traces = make(map[ID]struct{}, ls.Max)
    }

    // 獲取traceid是否存在, 存在即更新
    if _, ok := ls.traces[id.Trace]; ok {
        return ls.DeleteStore.Collect(id, anns...)
    }

    // 如果ring環的下個插入位置不為0,則表示ring環滿了,需要覆蓋(刪除並插入)
    if nextInsert := ls.ring[ls.nextInsertIdx]; nextInsert !=0 {
        old := ID(ls.ring[ls.nextInsertIdx])
        delete(r.traces, old)
        ls.DeleteStore.Delete(old)
    }

    // 插入
    ls.traces[id.Trace] = struct{}{}
    ls.ring[ls.nextInsertIdx] = int64(id.Trace)
    ls.nextInsertIdx = (ls.nextInsertIdx+1)%ls.Max
    return ls.DeleteStore.Collect(id, anns...)
}

由此可看,RecentStore 和 LimitStore 都是基於 DeleteStore 實現,且 DeleteStore interface 都是在 Store 上新增了 Delete 方法實現。而 MemoryStore 持久化儲存則不僅實現了本地檔案儲存,還實現了 DeleteStore interface。所以 RecentStore 和 LimitStore 都是基於 MemoryStore 實現的。

MemoryStore 的定期本地檔案儲存,策略不夠豐富,目前只支援一種策略:記憶體全域性寫入檔案,因為 MemoryStore 限制有寫入磁碟的有效時間,一旦過期,則會導致有些 trace 無法落地磁碟;而且每次全域性寫入,持久化需要的時間過長。

其中 RecentStore 的設計和實現是存在缺陷的。

更多原創文章乾貨分享,請關注公眾號
  • Appdash原始碼閱讀——RecentStore和LimitStore
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章