Appdash原始碼閱讀——Recorder與Collector
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)...)
}
}
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Appdash原始碼閱讀——Annotations與EventAPP原始碼
- Appdash原始碼閱讀——reflectAPP原始碼
- Appdash原始碼閱讀——RecentStore和LimitStoreAPP原始碼MIT
- Appdash原始碼閱讀——部分opentracing支援APP原始碼
- Appdash原始碼閱讀——Store儲存APP原始碼
- Appdash原始碼閱讀——Tracer&SpanAPP原始碼
- Appdash原始碼閱讀——Tracer&SpanAPP原始碼
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- CyberRT_recorder原始碼解讀以及record解析原始碼
- 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原始碼
- basictracer-go原始碼閱讀——SpanRecorder與wireGo原始碼
- JDK原始碼閱讀:String類閱讀筆記JDK原始碼筆記
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- 如何閱讀Java原始碼?Java原始碼
- buffer 原始碼包閱讀原始碼
- 使用OpenGrok閱讀原始碼原始碼
- express 原始碼閱讀(全)Express原始碼
- Kingfisher原始碼閱讀(一)原始碼