Appdash原始碼閱讀——Store儲存
解析完 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 值
我們要理解層次樹的重建,要搞清楚幾點:
- 重建後的樹也不一定是有序的層次樹;
- 每次重建後,如果新來的 span 尚不能放在正確的位置,對於 MemoryStore 的 trace 儲存,span 就放在臨時根節點的子節點上,距離為 1.也就是 root.Sub 列表中。
- 每次重建後,還不能放在正確位置的 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)
}
}
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Appdash原始碼閱讀——reflectAPP原始碼
- Appdash原始碼閱讀——RecentStore和LimitStoreAPP原始碼MIT
- Appdash原始碼閱讀——部分opentracing支援APP原始碼
- Appdash原始碼閱讀——Annotations與EventAPP原始碼
- Appdash原始碼閱讀——Recorder與CollectorAPP原始碼
- Appdash原始碼閱讀——Tracer&SpanAPP原始碼
- Appdash原始碼閱讀——Tracer&SpanAPP原始碼
- TiFlash 原始碼閱讀(一) TiFlash 儲存層概覽原始碼
- opentracing-go原始碼閱讀——Log儲存(完結篇)Go原始碼
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- OceanBase 原始碼解讀(九):儲存層程式碼解讀之「巨集塊儲存格式」原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- TiFlash 原始碼閱讀(三)TiFlash DeltaTree 儲存引擎設計及實現分析 - Part 1原始碼儲存引擎
- ReactorKit原始碼閱讀React原始碼
- Vollery原始碼閱讀(—)原始碼
- NGINX原始碼閱讀Nginx原始碼
- ThreadLocal原始碼閱讀thread原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- Runtime 原始碼閱讀原始碼
- RunLoop 原始碼閱讀OOP原始碼
- AmplifyImpostors原始碼閱讀原始碼
- stack原始碼閱讀原始碼
- CountDownLatch原始碼閱讀CountDownLatch原始碼
- fuzz原始碼閱讀原始碼
- HashMap 原始碼閱讀HashMap原始碼
- delta原始碼閱讀原始碼
- AQS原始碼閱讀AQS原始碼
- Mux 原始碼閱讀UX原始碼
- ConcurrentHashMap原始碼閱讀HashMap原始碼
- HashMap原始碼閱讀HashMap原始碼
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- JDK原始碼閱讀:String類閱讀筆記JDK原始碼筆記
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- 如何閱讀Java原始碼?Java原始碼
- buffer 原始碼包閱讀原始碼
- 使用OpenGrok閱讀原始碼原始碼