Go從入門到精通(25)
一個簡單web項目-實現鏈路跟蹤
文章目錄
- Go從入門到精通(25)
- 前言
- 為什么需要分布式鏈路跟蹤?
- go實現鏈路跟蹤
- 搭建zipkin 服務
- 安裝依賴
- 添加tracing包,OpenTelemetry 和Zipkin
- 在 Gin 中集成 OpenTelemetry 中間件
- log包添加獲取traceId方法
- 日志打印加入traceId
- sql 操作加入鏈路跟蹤
- 客戶端請求傳遞追蹤上下文
- 關鍵點總結
前言
分布式鏈路跟蹤技術(Distributed Tracing)是分布式系統(尤其是微服務架構)中用于追蹤請求全鏈路流轉的核心技術。它通過記錄請求在多個服務間的傳播路徑、執行耗時、狀態等信息,幫助開發者定位跨服務調用的性能瓶頸、故障點和依賴關系,是保障分布式系統可觀測性(Observability)的三大支柱之一(另外兩個是日志和指標)。
為什么需要分布式鏈路跟蹤?
在單體應用中,一個請求的處理流程在單一進程內完成,通過日志即可定位問題;但在分布式系統(如微服務)中,一個用戶請求可能經歷以下流程:
客戶端 → API網關 → 認證服務 → 訂單服務 → 庫存服務 → 數據庫 → 緩存
這種場景下,傳統調試方式面臨三大挑戰:
- 鏈路斷裂:無法確定請求經過了哪些服務、調用順序如何;
- 責任模糊:某個請求超時 / 失敗時,無法判斷是哪個服務導致(如訂單服務本身慢,還是依賴的庫存服務響應延遲);
- 性能盲區:無法量化各服務的耗時占比(如 90% 的耗時在數據庫,還是在網絡傳輸)。
分布式鏈路跟蹤技術通過全鏈路數據采集與可視化,解決了這些問題。
go實現鏈路跟蹤
這里以 OpenTelemetry+Zipkin為例實現鏈路跟蹤
搭建zipkin 服務
一般公司會統一搭建zipkin服務,自行搭建參考官網
安裝依賴
go get “go.opentelemetry.io/contrib/propagators/b3”
go get “go.opentelemetry.io/otel”
go get “go.opentelemetry.io/otel/exporters/zipkin”
go get “go.opentelemetry.io/otel/propagation”
go get “go.opentelemetry.io/otel/sdk/resource”
go get “go.opentelemetry.io/otel/sdk/trace”
go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin
添加tracing包,OpenTelemetry 和Zipkin
//tracing/tracing.go
package tracingimport ("go-web-demo/logger""go.opentelemetry.io/contrib/propagators/b3""go.opentelemetry.io/otel""go.opentelemetry.io/otel/exporters/zipkin""go.opentelemetry.io/otel/propagation""go.opentelemetry.io/otel/sdk/resource""go.opentelemetry.io/otel/sdk/trace"semconv "go.opentelemetry.io/otel/semconv/v1.30.0"oteltrace "go.opentelemetry.io/otel/trace""go.uber.org/zap""os"
)var (tracer oteltrace.Tracer
)func InitTracer() error {// 初始化鏈路跟蹤zipkinEndpoint := os.Getenv("ZIPKIN_ENDPOINT")if zipkinEndpoint == "" {logger.Sugar.Warn("ZIPKIN_ENDPOINT 未設置,鏈路跟蹤將被禁用")return nil}zipkinExporter, err := zipkin.New(zipkinEndpoint)if err != nil {zap.L().Error("Failed to create Zipkin exporter", zap.Error(err))return err}tp := trace.NewTracerProvider(trace.WithSampler(trace.AlwaysSample()),trace.WithBatcher(zipkinExporter),trace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName("go-web-demo"))),)otel.SetTracerProvider(tp)// 創建專門的W3C propagatorw3cPropagator := propagation.TraceContext{}// 創建B3 propagator,同時支持單頭和多頭格式b3Propagator := b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader | b3.B3SingleHeader),)otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(w3cPropagator, b3Propagator, propagation.TraceContext{}, propagation.Baggage{}))tracer = otel.Tracer("go-web-demo")logger.Sugar.Infow("Zipkin 鏈路跟蹤初始化成功,服務名: %s,端點: %s", "go-web-demo", zipkinEndpoint)return nil
}
- zipkinEndpoint 來源于你搭建的服務地址
- trace.WithSampler(trace.AlwaysSample()) 這里使用100%采樣,如果生成環境可以做適當的調整,比如0.1
sampler := sdktrace.TraceIDRatioBased(0.1) // 采樣率10%
在 Gin 中集成 OpenTelemetry 中間件
import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"// 創建默認引擎,包含日志和恢復中間件router := gin.Default()// 添加Zipkin中間件router.Use(otelgin.Middleware("go-web-demo")) //其他之前的邏輯。。
log包添加獲取traceId方法
func WithTraceCtx(ctx context.Context) *zap.Logger {span := trace.SpanFromContext(ctx)if !span.SpanContext().IsValid() {return Logger}traceID := span.SpanContext().TraceID().String()spanID := span.SpanContext().SpanID().String()return Logger.With(zap.String("traceId", traceID),zap.String("spanId", spanID),)
}
日志打印加入traceId
func HealthHandler(c *gin.Context) {ctx := c.Request.Context()logger.WithTraceCtx(ctx).Info("health check")c.JSON(http.StatusOK, gin.H{"message": "success"})
}
日志打印效果
{"service": "user-api", "traceId": "579ec5e6c0a21967e628266103499008", "spanId": "9053af570d799499"}
當然我們也可以在zipkin服務器查看,把我們日志的traceId輸入并且搜索就可以得到類型下圖的效果。大家也可以多搭建一個服務調用效果更佳
sql 操作加入鏈路跟蹤
//repository/database_connection.go
package repositoryimport gormTracing "gorm.io/plugin/opentelemetry/tracing"
func InitGormDB() error {//。。。 之前的的數據庫連接邏輯//添加鏈路跟蹤err = DB.Use(gormTracing.NewPlugin(//這里使用實際的數據類型,比如mysqlgormTracing.WithDBSystem(dbDriver),gormTracing.WithoutServerAddress(),))
}
客戶端請求傳遞追蹤上下文
// 調用其他服務并傳遞追蹤上下文
func callAnotherService(ctx context.Context, url string) (*http.Response, error) {// 從上下文中獲取當前spanspan := trace.SpanFromContext(ctx)// 創建HTTP請求req, err := http.NewRequestWithContext(ctx, "GET", url, nil)if err != nil {span.RecordError(err)span.SetStatus(codes.Error, err.Error())return nil, err}// 使用OpenTelemetry傳播器將上下文注入到請求頭otel.GetTextMapPropagator().Inject(ctx,propagation.HeaderCarrier(req.Header),)// 發送請求client := &http.Client{}resp, err := client.Do(req)if err != nil {span.RecordError(err)span.SetStatus(codes.Error, err.Error())return nil, err}// 記錄響應狀態span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode))if resp.StatusCode >= 400 {span.SetStatus(codes.Error, resp.Status)}return resp, nil
}
關鍵點總結
- 使用 OpenTelemetry 標準:
通過 go.opentelemetry.io/otel 實現與 Zipkin 的集成,遵循 CNCF 標準。 - 中間件集成:
使用 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin 自動處理 Gin 請求的追蹤。