basictracer-go原始碼閱讀二——Span

cdh0805010118發表於2018-07-04

Span

span interface擴充套件了opentracing-go最小子集,新增了兩個方法

type Span interface{
    opentracing.Span

    // 獲取operation name
    Operation() string

    // 建立span的時間
    Start() time.Time
}

Span Impl

spanImpl實現了Span interface, span的建立在《basictracer原始碼閱讀——TracerImpl》最後一說明,通過sync.Pool臨時物件池建立小物件;

大家如果想對sync.Pool有更深入的理解,可以看看《真有趣達達寫的slab》, 非常有意思,特別是無鎖的小物件池。

type spanImpl struct {
    tracer *tracerImpl
    event func(SpanEvent)
    // span是否存在併發,取決於執行單元的粗細粒度,如果跨goroutine使用,則會存在併發
    sync.Mutex
    // 記錄了OpenTracing標準中Span最小子集的所有資訊
    raw RawSpan
    // 這個span因為在tracerImpl中設定了MaxLogsPerSpan,儲存被丟棄的日誌記錄數
    numDroppedLogs int
}

type RawSpan struct {
    // Baggage資訊,並且還攜帶了TraceID、SpanID和Sampled
    Context SpanContext

    // 父SpanID,如果當前Span為tracer的頭結點,則ParentSpanID=0
    ParentSpanID uint64

    // Span的Operation name
    Operation string

    // 建立Span的時間
    Start time.Time

    // Span執行單元的時間長度
    Duration time.Duration

    // 本身Tags資訊
    Tags opentracing.Tags

    // 日誌事件記錄列表
    Logs []opentracing.LogRecord
}

// 設定span的操作名稱
func (s *spanImpl) SetOperationName(operationName string) opentracing.Span {
    ... // 注意:併發上鎖
}

// 當span不需要被記錄時,判定是否要對span的tags進行本地儲存
func (s *spanImpl) trim() bool{
    return !s.row.Context.Sampled && s.tracer.options.TrimUnsampledSpans
}

// 為span設定tags
// 注意:當為span儲存sampling.priority時,這個值是儲存在spanImpl的屬性rawspan中的sampled值,所以它在basictracer實現中並沒有寫入tags;
// 另外對於basictracer的實現,值得說的一點是:
// 1. 在tracerImpl中的Options中已經通過ShouldSample函式指定該tracer是否被跟蹤;
// 2. 同時, 無論是否有tracer前置條件,span本身都可以選擇是否記錄並儲存
func (s *spanImpl) SetTag(key string, value interface{}) opentracing.Span{
    defer s.onTag(key, value)
    ... // 注意:併發上鎖
}

// 通過key-value鍵值對列表轉換為[]log.Field, 並儲存到spanImpl Logs中
func (s *spanImpl) LogKV(keyValues ...interface{}) {
    ...
    // 當發生錯誤時,記錄錯誤日誌的錯誤資訊和錯誤位置
    s.LogFields(log.Error(err), log.String("function", "LogKV")
}

// Span當發生日誌記錄時,格式如下:
log:
time: 2006-01-02 15:04:05  log.Fields: 
[ 
    {key: function, value: LogKV}   
    {key: error, value: "non-even keyValues len: 3"}
]
time: 2006-01-02 15:04:08 log.Fields:
[
    {key: update, value: record not found}
]

// 當span.raw.Logs列表長度大於等於tracer.Options.MaxLogsPerSpan(非零)時,這需要丟棄日誌
// 這裡丟棄日誌的做法第一次見到,感覺還不錯:
// 它是把一個列表中上半部分的日誌反覆覆蓋寫,這樣的話,有個問題日誌出現開頭部分連續和結尾部分連續,中間斷層。你可以通過問題發生的地方和結尾地方開始追蹤,並在分散式日誌管理系統中,根據trace的相關開始和結束日誌,定位時間區域,和問題開始和結束的位置和原因等, 中間斷層的日誌在具體的日誌系統中查詢定位。
func (s *spanImpl) appendLog(lr opentracing.LogRecord) {
    ...

    numOld:=(maxLogs -1 ) /2
    numNew = maxLogs - numOld
    s.raw.Logs[numOld+(s.numDroppedLogs%numNew)]=lr
    s.numDroppedLogs++
}

// 該方法記錄一個錯誤發生時,相關的呼叫棧具體日誌錯誤位置和錯誤資訊
func (s *spanImpl) LogFields(fields ...log.Field) {
    ...
}

func (s *spanImpl) Finish() {
    s.FinishWithOptions(opentracing.FinishOptions{}
}

func (s *spanImpl)FinishWithOptions(opts opentracing.FinishOptions) {
    ...
    // 這裡涉及到一個列表旋轉的演算法, 本節下面有旋轉介紹和理解
    rotateLogBuffer(s.raw.Logs[numOld:], s.numDroppedLogs%numNew)
    // 做完之後,還要把span釋放到sync.Pool臨時物件池中複用 spanPool.Put(s)
}

乍一看,臥槽,這個旋轉有毛線用,再仔細理解理解appendLog方法;會再來一句,這想法牛逼;

為啥牛逼呢?

因為appendLog方法在span.RawSpan.Logs溢位後,以後都會產生日誌丟棄行為,而且是從後半段丟棄和重寫,這樣後半段反覆覆蓋重寫,注意一點,我們在LogRecord儲存時,需要保證日誌輸出的時間有序性,但是後半段反覆覆蓋寫,則會導致一種情況:

當(s.numDroppedLogs)/numNew>1時,則會出現後半段的前N個日誌時間戳大於後面的日誌時間戳,這個分界線的位置則是:s.numDroppedLogs%numNew,所以需要這個左邊位置向左旋轉N。

這樣的話,後半段日誌記錄時間戳就是有序遞增的,那麼輸出也就是有序的

spanImpl中的LogEvent, LogEventWithPayload, Log需要廢棄,因為標準中opentracing-go的LogData已廢棄,只保留LogKVs和LogFields方法。

對於陣列/列表旋轉演算法,可以採用C++中的官方演算法庫rotate left《STL 原始碼分析》有助於這個演算法的理解

這個演算法是採用前向移動, 且每次移動[first, pos]大小固定資料單元

一維旋轉,向(左|右)旋轉N位的理解:

把一維陣列分為兩部分,則它當前由三部分構成:左半部分和右半部分;

記住一點:左半部分是整體、右半部分也是整體。

例如:字串abcdefg ,向(左|右)旋轉3位:
1. 向左旋轉3位:
    整體可以看成:A'C'。其中:A'表示abc, C'表示defg, 則左旋轉結果為:C'A', 展開結果為:efgdabc
2. 向右旋轉3位:
    整體可以看成:A'C'。其中:A'表示abcd,C'表示efg,則右旋轉結果為:C'A', 展開結果為:efgabcd

參考資料

basictracer-go

相關文章