Appdash原始碼閱讀——Annotations與Event

cdh0805010118發表於2018-07-06

annotations

Annotations 用於儲存 Span 攜帶資訊,除了 Span 自身部分資訊={TraceID、SpanID 和 ParentID},通過 Span 儲存 Span 自身資訊和 Annotations 攜帶資訊。

這裡要說明的一點,我覺得 Appdash 並沒有把 Span 自身資訊和其他資訊區分好,但是 OpenTracing 官方的 basictracer-go 庫的 spanImpl 區分比較好;因為前者 Span 的 OperationName 應該是屬於 Span 本身的,但是在 Appdash 中被儲存到 Annotation["Name"] 中;

type Annotations []Annotation

type Annotation struct {
    Key string
    Value []byte
}

// 用於校驗Annotation的Key值是否在已註冊事件中是關鍵資訊
func (a Annotation) Important() bool {
    ... // 遍歷獲取所有已註冊事件列表,校驗每個事件是否為重要事件,並對獲取到的重要事件
    // 通過event.Important方法獲取關鍵字列表keys
    // 遍歷keys並與a.Key值比較,如果相同,返回true;否則返回false
}

func (as Annotations) String() string{
    ... // 序列化Annotations的鍵值對列表
}

func (as Annotations) schemas() []string {
    ... // Annotations的所有key,並返回
    // 注意一點:Appdash內部的五種Event資料key都是帶有字首"_schema:"。內部標識
    // 其他都是外部自定義註冊事件值
}

func (as Annotations) get(key string) []byte {
    ... // 在Annotations中找到鍵為key的value值,並返回
}

func (as Annotations) StringMap() map[string]string {
    ... // 把Annotations列表轉換成map結構資料後,返回
}
// 這裡有個疑問,為何Annotations不直接採用map[string][]byte結構儲存,而是使用slice列表結構儲存?
// 答案能想到的就是,map的相同key是會做覆蓋處理,而slice結構是做append追加處理。
// 但是前面所看到的通過key,獲取value,並沒有考慮重複,所以還是有疑問。

func (as Annotations) wire() (w []*wire.CollectPacket_Annotation) {
    ... // 遍歷Annotations列表,把攜帶資訊轉換為protobuffer協議傳輸的資料資訊
    // 注意一點:這裡是使用的Annotations的快照儲存,防止資料動態修改。這裡是否有必要使用快照,後面再看.
}

func annotationFromWire(as []*wire.CollectPacket_Annotations) Annotations {
    ... // 從protobuffer協議資料流中把網路資料轉換為Annotations結構資料儲存。
}

event

// Span攜帶資訊資料Annotations中,有一些是事件資訊。
// Appdash內部自身定義了五種Event
// Schema方法用於返回事件的Key值。這個Key值代表事件名稱
type Event interface{
    Schema() string
}

// 在事件列表中,哪些事件是值得關注的事件或者metrics
// Important方法返回一個列表值,這個後面再看
type ImportantEvent interface {
    Important() []string
}

// 由於Event事件資料是儲存在Span的Annotations資料中,而Annotations並沒有顯式指明哪些是Event列表資料,所以就存在一對解析方法。類似於encoding/decoding
type EventMarshaler interface {
    MarshalEvent() (Annotations, error)
}

type EventUnmarshaler interface{
    UnmarshalEvent(Annotations) (Event, error)
}

// 標準化使用Marshal方法,把Event資料轉換成Annotations
// 如果傳入的引數不支援MarshalEvent,則使用預設的方法去Marshal event資料
// 簡單的Event可以不用MarshalEvent,直接使用預設的
// 複雜的Event資料可以自定義實現MarshalEvent和UnmarshalEvent方法序列化和反序列化資料為指定的資料型別Annotations
// Appdash內部的五類Event,型別簡單,使用預設的序列化就行了
func MarshalEvent(e Event) (Annotations, error) {
    // 先嚐試校驗Event是否實現了MarshalEventer介面,如果是的話,直接序列化
    if v, ok := e.(EventMarshaler); ok {
        as, err := v.MarshalEvent()
        ...
        as = append(as, Annotation{Key: SchemaPrefix + e.Schema()}
        return as, nil
    }

    // 否則,使用預設的序列化方法
    var as Annotations
    flattenValue("", reflect.ValueOf(e), func(k, v string) {
        as = append(as, Annotation{Key: k, Value: []byte(v)})
    })
    as = append(as, Annotation{Key: SchemaPrefix+e.Schema()})
    return as, nil
}

MarshalEvent 方法對於 Annotations 的 Event 儲存理解很重要;它是解答為何 Span 的 Annotations 是 Slice 儲存 key-value,而不是使用 Map[string][] byte 結構儲存?的關鍵。

針對這個問題,我還提了一個 issue,最後自己解答了, issue:206 。 主要原因是作者希望 Annotations 儲存 Event 事件時,希望與該事件相關的資訊儲存在一起連續,這樣取資料時,可以以_schema:xxx為邊界,獲取與該事件相關的所有資訊。這也就是為何對於事件要加一個字首的原因。類似於分隔符。如果事件內部的連續資訊使用了_schema:xxx字首,則會導致事件資訊的分裂。

在 MarshalEvent 方法中,都是先進行事件其他資訊的序列化,然後再在這個單元結尾處新增一個_schema:xxx, 表示這個事件型別名稱和結束符,且這個事件的 value 是空值。

另外一點,預設序列化方法是使用的 flattenValue,它會採用遞迴演算法對傳入的 event 值進行操作,把這個事件的所有屬性和子屬性全部新增到 Annotations 中。

把 Annotations 中的所關注的事件,反序列化給傳入的 event 值。

func UnmarshalEvent(as Annotations, e Event) error {
    aSchemas := as.schemas()
    // 獲取Annotations所有的事件key列表, 並通過遍歷與傳入的Event值比較,如果找不到,返回錯誤
    // 和MarshalEvent類似
    // 先嚐試校驗Event是否支援反序列化,如果不支援,則使用預設的反序列化方法
    unflattenValue("", reflect.ValueOf(&e), reflect.TypeOf(&e), mapToKVs(as.StringMap()))
    return nil
}

對於 unflattenValue 方法是耗時操作,因為它是全域性 Annotations 檢索。我覺得最理想的方法是繼續對 Annotations 的儲存進行規範化,例如:把 Annotations 儲存資料進行分類,分為 Event、Log 等型別。

內部 Event

Appdash 內部已存在五種 Event,分別是 SpanNameEvent,logEvent,msgEvent,timespanEvent 和 Timespan。它們都是先 Event 介面:Schema 方法

// _schema:name
type SpanNameEvent struct {Name string }

func (SpanNameEvent) Schema() string { return "name"}

// 由此可以看到Span的OperationName儲存在Event中,且最終通過MarshalEvent方法序列化儲存在Annotations中
func SpanName(name string) Event {
    return SpanNameEvent{Name: name}
}

// _schema:msg
type msgEvent struct { Msg string}

func (msgEvent) Schema() string { return "msg" }

func Msg(msg string) Event {
    return msgEvent{Msg: msg}
}

// _schema:timespan
type timespanEvent struct {
    S, E time.Time
} 
func (timespanEvent) Schema() string {
    return "timespan"
}

func (ev timespanEvent) Start() time.Time {
    return ev.S
}
func (ev timespanEvent) End() time.Time {
    return ev.E
}

// _schema:Timespan
type Timespan struct {
    S time.Time `trace: "Span.Start"`
    E time.Time `trace: "Span.End"`
}

func (Timespan) Schema() string { return "Timespan"}

func (s Timespan) Start() time.Time {return s.S}
func (s Timespan) End() time.Time {return s.E}

上面的timespanEvent與Timespan兩種事件的不同,我目前還不理解,::TODO

// _schema:log
type logEvent struct {
    Msg string
    Time time.Time
}

func Log(msg string) Event {
    return logEvent{Msg: msg, Time: time.Now()}
}

func LogWithTimestamp(msg string, timestamp time.Time) Event {
    return logEvent{
        Msg: msg,
        Time: timestamp,
    }
}

func (logEvent) Schema() string {
    return "log"
}

func (e *logEvent) Timestamp() time.Time {
    return e.Time
}

本章小結:

Appdash 是早於 OpenTracing 標準出現的,所以它並沒有遵循 OpenTracing 標準,後面也逐漸支援,但是也只是部分支援。雖然該 trace 的 log 部分使用了 basictracer-go,但是其實可以取代不要它,它只是一個擴充套件。見 issue 207

對於 Span 的 Annotations,儲存資料包含了所有的 Event 列表,Event 與 Annotations 的資料轉換 (序列化和反序列化), 通過 MarshalEvent 和 UnmarshalEvent 方法實現。

文中也解釋了 Annotations 為何不使用 Map[string][] byte 儲存,而要用 slice 結構儲存資料。

這部分 Annotations 與 Event 的序列化和反序列化涉及到大量的 reflect,在後面會有相應的分析。

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

相關文章