Request-ID需求
- 客戶端訪問web服務時如何將該請求與服務器端日誌關聯
- 微服務架構系統的訪問日誌如何查詢
- 不同項目交互異常,如何做日誌關聯
沒有Request-ID的請求,只能根據調用函數記錄日誌關鍵字定位,在根據用戶的輸入的參數和時間來確定相關的日誌。
如果項目是以分佈式微服務架構來實現的,代碼多次封裝後無法通過記錄日誌關鍵字與用戶請求關聯;微服務架構的用戶請求邏輯層會拆分爲多個子任務給下層服務處理,下層服務無法與用戶請求關聯;不同項目交互在併發和錯誤重試參數相同的情況下,也很難通過日誌關鍵字和時間來定位問題。
一般來說,在一個完整的請求中(對外暴露的是一個接口,對內的話可能經過 N 多個子服務),每個子服務共用一個相同的、全局唯一的 Request ID,這樣當出現問題時根據 Request ID 就可以檢索到請求當時的各個子服務的日誌。
應用 Request ID 需要有一套健全的日誌系統。
Request-ID也可以配合mysql的唯一索引實現接口調用的冪等。
Golang Web日誌記錄Request-ID
採用中間件形式,以gin框架爲例
- RequestId 中間件, 主要的作用是生成 requestId
func RequestIdMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context){
requestId := ctx.Request.Header.Get("X-Request-Id")
if requestId == "" {
requestId = uuid.NewV4().String()
ctx.Request.Header.Set("X-Request-Id", requestId)
}
// 寫入響應
ctx.Header("X-Request-Id", requestId)
ctx.Next()
}
}
- 日誌中間件
func LogMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context){
requestId := ctx.Request.Header.Get("X-Request-Id")
ctx.Next()
logrus.WithField("X-Request-Id", requestId).Info("xxxx")
}
}
- 使用方式
engine.Use(RequestIdMiddleware(), LogMiddleware())
以go-gin-api爲例 https://github.com/xinliangnote/go-gin-api
trace_id 鏈路ID,String,例如:4b4f81f015a4f2a01b00 在中間件中進行設置,示例代碼:
// 如果存在 traceId 就使用原來的,如果不存在就重新生成。
if traceId := context.GetHeader(trace.Header); traceId != "" {
context.setTrace(trace.New(traceId))
} else {
context.setTrace(trace.New(""))
}
go-zero鏈路追蹤 https://go-zero.dev/cn/docs/deployment/trace/
微服務架構中調用鏈可能很漫長,從 http 到 rpc ,又從 rpc 到 http 。而開發者想了解每個環節的調用情況及效率,最佳方案就是 全鏈路跟蹤。
追蹤的方法就是在一個請求開始時生成一個自己的 spanID ,隨着整個請求鏈路傳下去。我們則通過這個 spanID 查看整個鏈路的情況和性能問題。
HTTP
func TracingHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// **1**
carrier, err := trace.Extract(trace.HttpFormat, r.Header)
// ErrInvalidCarrier means no trace id was set in http header
if err != nil && err != trace.ErrInvalidCarrier {
logx.Error(err)
}
// **2**
ctx, span := trace.StartServerSpan(r.Context(), carrier, sysx.Hostname(), r.RequestURI)
defer span.Finish()
// **5**
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func StartServerSpan(ctx context.Context, carrier Carrier, serviceName, operationName string) (
context.Context, tracespec.Trace) {
span := newServerSpan(carrier, serviceName, operationName)
// **4**
return context.WithValue(ctx, tracespec.TracingKey, span), span
}
func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec.Trace {
// **3**
traceId := stringx.TakeWithPriority(func() string {
if carrier != nil {
return carrier.Get(traceIdKey)
}
return ""
}, func() string {
return stringx.RandId()
})
spanId := stringx.TakeWithPriority(func() string {
if carrier != nil {
return carrier.Get(spanIdKey)
}
return ""
}, func() string {
return initSpanId
})
return &Span{
ctx: spanContext{
traceId: traceId,
spanId: spanId,
},
serviceName: serviceName,
operationName: operationName,
startTime: timex.Time(),
// 標記爲server
flag: serverFlag,
}
}
- 將 header -> carrier,獲取 header 中的traceId等信息
- 開啓一個新的 span,並把「traceId,spanId」封裝在context中
- 從上述的 carrier「也就是header」獲取traceId,spanId
- 看header中是否設置
- 如果沒有設置,則隨機生成返回
- 從 request 中產生新的ctx,並將相應的信息封裝在 ctx 中,返回
- 從上述的 context,拷貝一份到當前的 request
RPC
在 rpc 中存在 client, server ,所以從 tracing 上也有 clientTracing, serverTracing 。
func TracingInterceptor(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// open clientSpan
ctx, span := trace.StartClientSpan(ctx, cc.Target(), method)
defer span.Finish()
var pairs []string
span.Visit(func(key, val string) bool {
pairs = append(pairs, key, val)
return true
})
// **3** 將 pair 中的data以map的形式加入 ctx
ctx = metadata.AppendToOutgoingContext(ctx, pairs...)
return invoker(ctx, method, req, reply, cc, opts...)
}
func StartClientSpan(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) {
// **1**
if span, ok := ctx.Value(tracespec.TracingKey).(*Span); ok {
// **2**
return span.Fork(ctx, serviceName, operationName)
}
return ctx, emptyNoopSpan
}
- 獲取上游帶下來的 span 上下文信息
- 從獲取的 span 中創建新的 ctx,span「繼承父span的traceId」
- 將生成 span 的data加入ctx,傳遞到下一個中間件,流至下游
go-zero 通過攔截請求獲取鏈路traceID,然後在中間件函數入口會分配一個根Span,然後在後續操作中會分裂出子Span,每個span都有自己的具體的標識,Finsh之後就會彙集在鏈路追蹤系統中。開發者可以通過 ELK 工具追蹤 traceID ,看到整個調用鏈。
同時 go-zero 並沒有提供整套 trace 鏈路方案,開發者可以封裝 go-zero 已有的 span 結構,做自己的上報系統,接入 jaeger, zipkin 等鏈路追蹤工具。