Appdash原始碼閱讀——Recorder與Collector

cdh0805010118發表於2018-07-07

Recorder

在 Appdash 中,Recorder 是可以與 Collector 互動,以及 Span 對外的操作入口或者操作單元。

type Recorder struct {
    SpanID
    annotations []Annotation

    finished bool // 表示該span是否已經結束了生命週期

    collector Collector // Recorder作為Collector的輸入

    errors []error // 在span的生命週期內操作出現錯誤的error列表
    errorsMu sync.Mutex // 併發errors
}

// 當StartSpan後,再指定Collector,用於儲存trace資料
// 這裡有個疑問:為何Collector不是在各個服務啟動時,初始化global tracer時並指定Collector服務,而是把Collector細化到Recorder中,這樣如果服務在執行中,Collector不變,則直接提到Tracer上就可以了,如果經常變,那麼Tracer呼叫鏈肯定是不會完整的,各個storage也不能正確的顯示完整的呼叫鏈。後面再看 ::TODO
func NewRecorder(span SpanID, c Collector) *Recorder {
    ...
    return &Recorder{
        SpanID: span,
        collector: c,
    }
}

// 根據當前Span,建立一個子Span。操作入口是Recorder級別
func (r *Recorder) Child() *Recorder {
    return NewRecorder(NewSpanID(r.SpanID), r.collector)
}

// Recorder操作級別,為Span新增OperationName,並通過Event與Annotations之間的序列化進行資料轉換儲存
func (r *Recorder) Name(name string) {
    r.Event(SpanNameEvent{Name: name})
}

// 同上,Event為msgEvent
func (r *Recorder) Msg(msg string) {
    r.Event(msgEvent{Msg: msg})
}

// 同上,Event為logEvent
func (r *Recorder) Log(msg string) {
    r.Event(Log(msg))
}

// 同上,只是增加帶有時間戳
func (r *Recorder) LogWithTimestamp(msg string, timestamp time.Time) {
    r.Event(LogWithTimestamp(msg, timestamp))
}

// 把Event轉換為Recorder中的Annotations
func (r *Recorder) Event(e Event) {
    ans, err := MarshalEvent(e)
    ... 
    r.annotations = append(r.annotations, ans...)
}

// Recorder操作粒度,Span生命週期結束, 並把Recorder推送到Collector。
// 這裡會有個疑問:如果每個Recorder在結束時立即推送,如果業務量爆發,則這個短連線的高併發推送,會對業務造成明顯抖動。所以最好是長連線池
func (r *Recorder) Finish() {
    if r.finished {
        ... // error
    }
    r.finished = true
    r.Annotation(r.annotations...)
}

func (r *Recorder) Annotation(as ...Annotation) {
    r.failsafeAnnotation(as...)
}

// 推送Span的所有資訊到Collector中,包括自身資訊和其他資訊,這個量在網路傳輸中比較大,沒有限制大小
func (r *Recorder) failsafeAnnotation(as ...Annotation) {
    return r.collector.Collect(r.SpanID, as...)
}

// 還有幾個errors的併發處理,不展開了。

從上面可以瞭解到,Appdash 中 span 的操作粒度為 Recorder。通過 Recorder 來完成 Span 生命週期的所有操作。

Collector

collector server 限制了處理每個請求的資料大小maxMessageSize=1M,Appdash 指定 Collector server 責任是處理 Span Recorder event 資料,但是業務微服務發過來的 Recorder 是包含了 SpanID 和 Annotations 所有資料,而 event 只是 Annotations 中的一部分資料,可能 event 佔比很大。

// Collector可供使用者自定義實現自己的Collector資料處理
type Collector interface {
    Collect(SpanID, ...Annotation) error
}

// Appdash的Collector和Storage設計思路為:
// 每種後端儲存方式都會有對應的Collector資料處理方式。
// Collector是接收和清洗,Storage是儲存。
// 下節我會詳細介紹Appdash自帶的多種記憶體儲存方式
type Store interface {
    Collector
    Trace(ID) (*Trace, error)
}

// 本地儲存,表示Collector服務和trace資料儲存在同一個節點上
func NewLocalCollector(s Store) Collector {
    return s
}

// 這個表示Collector服務和Storage服務是分離的,當Collector服務完成資料接收、清洗後,再通過protobuffer協議傳輸到Storage服務進行資料儲存。
func newCollectPacket(s SpanID, as Annotations) *wire.CollectPacket {
    return &wire.CollectPacket{
        Spanid: s.wire(),
        Annotation: as.wire(),
    }
}

Appdash 的 agent 包含兩類:一類是基於 Recorder 的塊傳輸,一類是基於 Recorder 的單一傳輸。當業務對時延要求比較高時,同時能夠忍受一定的時效性,則可以使用塊傳輸 ChunkedCollector;當業務對時延要求不太高,且希望時效性儘量好時,則使用 RemoteCollector。

Agent——ChunkedCollector

// ChunkedCollector是屬於業務微服務的agent,由它傳輸到Collector server。
// Appdash提供了另一種比較好的agent模式, 場景在於如果微服務數量多,且業務量大,同時如果業務對時延性要求高,所以為了減少對業務造成的負載壓力,我們可以先在業務微服務先暫存下來,在進行塊傳輸。
// 這個類似於日誌檔案分割:希望日誌檔案能夠按照時間和大小兩個維度來分割。
// 1. 當日志檔案流寫入到一定時間時,直接Flush檔案並關閉且重新開啟新的檔案描述符fd;
// 2. 如果沒有到時間,日誌檔案流大小到達指定時,則也做關閉並開啟新的fd;
type ChunkedCollector struct {
    Collector

    MinInterval time.Duration // 時間維度

    // 這個是表示如果寫入超時,則直接丟棄剩下沒有傳輸的資料
    FlushTimeout time.Duration 

    // 預設大小:32M
    // 這個變數代表的意義要仔細理解。
    // 當前ChunkedCollector資料記憶體塊queueSizeBytes沒有超過這個值時,如果下一個到來的Recorder累加,超過這個最大值,則直接拋棄接收到的Recorder,不會丟棄整個記憶體塊。直到最小時間到來時Mininterval,寫入後才能繼續接收Recorder。這樣的問題在於,如果MaxQueueSize設定不合理,同時Mininterval設定又很大的情況下,則會造成某一段時間Recorder捕獲不到。
    // 簡言之:溢位則直接丟棄新來的Recorder.
    MaxQueueSize uint64

    started, stopped bool
    stopChan chan struct{}

    queueSizeBytes uint64 // 當前佇列大小
    pendingBySpanID map[SpanID]Annotations // 收集多個Span

    mu sync.Mutex // 併發操作
}

func NewChunkedCollector(c Collector) *ChunkedCollector {
    return &ChunkedCollector{
        Collector: c,
        MinInterval: 500*time.Millisecond, // 500ms寫一次
        FlushTimeout: 2*time.Second, // 2秒沒寫入成功,直接丟棄
        MaxQueueSize: 32*1024*1024, // 32M
    }
}


// 這個Collector塊大小處理寫得不錯,
// 存在一個問題,溢位時丟棄Span,可能使得某些trace呼叫鏈不完整。
func (cc *ChunkedCollector) Collect(span SpanID, anns ...Annotation) error {
    ... // 併發操作,以及校驗ChunkedCollector是否已停止處理
    if !cc.Started {
        cc.start() // 啟動一個goroutine,用於定時出發pendingBySpanID的Flush Chunked操作。
    }

    // 計算ChunkedCollector的累積塊大小
    // Recorder由兩部分構成:SpanID和Annotations,其中SpanID={SpanID、TraceID和ParentID} 都是uint64 8個位元組,所以SpanID共佔用24個位元組
    // 然後再累計Annotations位元組數
    var collectionSize uint64 = 3*8
    for _, ann:= range anns {
        collectionSize += uint64(len(ann.Key))
        collectorSize += uint64(len(ann.Value))
    }
    cc.queueSizeBytes += collectionSize

    // 溢位則直接丟棄Recorder
    if cc.MaxQueueSize!=0 && cc.queueSizeBytes+collectionSize > cc.MaxQueueSize {
        return ErrQueueDropped
    }

    if p, present := cc.pendingBySpanID[span]; present {
        cc.pendingBySpanID[span] = append(p, anns...)
    } else {
        cc.pendingBySpanID[span] = anns
    }

    return nil
}

// 我覺得Appdash的業務微服務agent處理,會存在併發大記憶體拷貝現象,會很吃記憶體
func (cc *ChunkedCollector) Flush() error {
    start:=time.Now()
    cc.mu.Lock()
    ... // 記憶體 拷貝記憶體塊,釋放鎖,交給agent繼續處理Recorder資料
    cc.mu.Unlock()

    // 省略了一些程式碼
    for spanID, p := range pendingBySpanID {
        cc.Collector.Collect(spanID, p...)

        if cc.FlushTimeout !=0 && time.Since(start) > cc.FlushTimeout {
            break
        }
    }
    ...
    return nil
}

// 啟動一個goroutine,用於agent傳輸記憶體塊到Collector server
func (cc *ChunkedCollector) start() {
    cc.stopChan = make(chan struct{})
    cc.started = true
    go func() {
        for {
            t:= time.After(cc.MinInterval)
            select {
                case <-t:
                    cc.Flush
                case <-cc.stopChan:
                    return // stop
            }
        }
    }()
}

func (cc *ChunkedCollector) Stop() {
    ...
    close(cc.stopChan)
    cc.stopped = true
}

這個 Flush 方法負責處理傳輸記憶體塊到 Collector server 中, 我們看到程式碼中有個變數值 cc. FlushTimeout, 當 agent 傳輸塊記憶體到 Collector server 中超時時,則會直接丟棄沒有傳完的資料。這樣做的目的是,防止 agent 卡死,因為 agent 的接收、處理和傳輸 Span 資料,是存在併發操作,且共用 cc.mu.Lock 鎖,如果傳輸一直卡著,則 agent 接收 Recorder 也一直卡著。

Agent——Remote Collector

// RemoteCollector兩種tcp client連線,帶證書和不帶證書
func NewRemoteCollector(addr string) *RemoteCollector {
    reutrn &RemoteCollector{
        addr: addr,
        dial: func(net.Conn, error) {
            return net.Dial("tcp", addr)
        },
    }
}

func NewTLSRemoteCollector(addr string, tlsConfig *tls.Config) *RemoteCollector {
    return &RemoteCollector{
        addr: addr,
        dial: func() (net.Conn, error){
            return tls.Dial("tcp", addr, tlsConfig)
        },
    }
}

type RemoteCollector struct {
    addr string

    dial func() (net.Conn, error) 

    mu sync.Mutex
    pconn pio.WriteCloser // client connection

    ... 
}

// 傳送Span資料到Collector server, 傳輸資料序列化協議protobuffer
// 並通過rc.collect方法進行pconn.WriteMsg方法進行寫操作
func (rc *RemoteCollector) Collect(span SpanID, anns ...Annotation) error {
    return rc.collectAndRetry(newCollectPacket(span, anns))
}

// pb協議,建立Collector client連線
func (rc *RemoteCollector) connect() error {
    c, err := rc.dial()

    rc.pconn = pio.NewDelimitedWriter(c)
}

Collector Server

Appdash 把 Collector Client 和 Server 都放在了 collector.go 檔案中,感覺還是有些雜,如果能寫為 collector_client.go、collector_server.go 和 collector.go,就非常好了

Collector Server 主要就是啟動一個 collector 服務,監聽並收集來自各個業務微服務中的 agent 傳送過來的 Recorder 資料。

// 建立一個Collector Server。該方法的第二個引數Collector,是帶有指定Remote/Local Storage的client。這個Collector實現了Store interface
// 第一個引數一般都是tcp長連線, 因為SpanID數量太多,如果HTTP請求,則三次握手消耗太大,無法接受
func NewServer(l net.Listener, c Collector) *CollectorServer {
    cs :=&CollectorServer{c: c, l: l}
    return cs
}

type CollectorServer struct {
    c Collector
    l net.Listener
}

// 啟動Collector服務,並監聽獲取和處理來自agent的請求
func (cs *CollectorServer) Start() {
    for {
        conn,err := cs.l.Accept()

        go cs.handleConn(conn)
    }
}


// Collector服務處理方式和http一樣,都是來一個請求,建立一個goroutine。
// 同時利用pb協議,讀取io流,每次讀取一個Recorder大小,並把Recorder傳送給Store interface處理。
func (cs *CollectorServer) handleConn(conn net.Conn) (err error) {
    defer conn.Close()

    rdr := pio.NewDelimitedReader(conn, maxMessageSize)
    defer rdr.Close()
    for {
        p := &wire.CollectPacket{}
        rdr.ReadMsg(p)

        spanID := spanIDFromWire(p.Spanid)

        cs.c.Collect(spanID, annotationsFromWire(p.Annotation)...)
    }
}
更多原創文章乾貨分享,請關注公眾號
  • Appdash原始碼閱讀——Recorder與Collector
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章