Golang - Jaeger 實現分佈式鏈路追蹤

什麼是分佈式鏈路追蹤(Distributed Tracing?)?

分佈式追蹤是軟件開發和性能監控中使用的一種技術,用於跟蹤和分析請求在分佈式系統的各個組件中的流動。在現代軟件應用中,不同的服務和微服務通常一起工作來滿足用戶請求。這些服務可能分佈在多臺服務器、容器或地理位置上。

瞭解這些服務之間的交互並識別瓶頸或性能問題可能是具有挑戰性的。不同服務生成的跨度被收集併發送到一個稱爲追蹤系統的集中式存儲庫。這個系統聚合並存儲跨度以供分析。

像 Jaeger、Zipkin 和 OpenTelemetry 等分佈式追蹤工具和平臺有助於在複雜系統中實現和管理分佈式追蹤。

Trace and Span

追蹤上下文:當請求進入系統時,會生成一個唯一的追蹤 ID。隨着請求通過不同的服務傳遞,這個追蹤 ID 也隨之傳遞。這使得服務能夠將單個請求的各個段關聯起來。

Jaeger 是什麼?

Jaeger[1] 是一個開源的、端到端的分佈式追蹤系統,幫助開發人員監控和排查基於複雜微服務的應用程序的性能問題。

Jaeger Client

Clien 是一組開發人員集成到他們應用程序中的 lib 和 SDK。捕獲表示單個工作單元或交互的跟蹤跨度。這些跨度包含有關時間、上下文和其他相關數據的信息。

Jaeger Agent:

Agent 負責通過 UDP 或 gRPC 協議從應用程序接收跟蹤數據。它將這些數據進行批處理,並將其轉發給後端組件進行存儲和分析。

Jaeger Collector:

收集器從 Agent 那裏接收跟蹤數據,進行初始處理,然後將數據傳遞給存儲後端。它能夠執行採樣和聚合以管理跟蹤數據的量,並確保有效的存儲。

Jaeger Query:

查詢服務允許用戶搜索和檢索跟蹤數據。它提供了一個用戶界面,您可以在其中可視化跟蹤、查看跨度的時間信息,並瞭解組件之間的交互。

Jaeger Web UI

與 Golang 集成

我們創建兩個簡單的 golang 服務:

項目結構:

Comment Service 的 main.go

package main

import (
 "comment-service/config"
 "comment-service/domain"
 "fmt"
 "github.com/gin-gonic/gin"
 "github.com/opentracing/opentracing-go"
 "log"
)

func main() {
 tracer, closer, _ := config.InitJaeger()
 defer closer.Close()
 opentracing.SetGlobalTracer(tracer)

 router := gin.Default()
 router.GET("/api/v1/comments", getCommentsByPostId)

 router.Run(":9090")
}

func getCommentsByPostId(c *gin.Context) {
 log.Println("comment-service /getCommentsByPostId method")
 span := config.StartSpanFromRequest(config.Tracer, c.Request, "comment-service handle /getCommentsByPostId")
 defer span.Finish()

 postId := c.Query("postId")
 comment1 := domain.Comment{ID: "4d13441a-3c10-11ee-be56-0242ac120002", Content: "Good performance...", PostID: postId}
 comment2 := domain.Comment{ID: "fd7dbe48-1f88-4eba-880a-8ca8197f8a72", Content: "Good performance...", PostID: postId}

 comments := []domain.Comment{
  comment1,
  comment2,
 }

 span.LogEvent(fmt.Sprintf("Comments fetched by post id. Post ID: %s", postId))

 c.JSON(http.StatusOK, comments)
}

Post Service Main.go

package main

import (
 "context"
 "fmt"
 "github.com/gin-gonic/gin"
 "github.com/opentracing/opentracing-go"
 "net/http"
 "post-service/client"
 "post-service/config"
 "post-service/domain"
)

var POST_ID_LIST = []string{"1-ab-2""2-ab-3""3-ab-4"}

func main() {
 tracer, closer, _ := config.InitJaeger()
 defer closer.Close()
 opentracing.SetGlobalTracer(tracer)

 router := gin.Default()
 router.GET("/api/v1/posts/:id", findPostById)

 router.Run(":8080")
}

func findPostById(c *gin.Context) {
 span := config.StartSpanFromRequest(config.Tracer, c.Request, "post-service handle /findPostById method")
 defer span.Finish()
 ctx := opentracing.ContextWithSpan(context.Background(), span)

 id := c.Param("id")
 if isPostExist := contains(POST_ID_LIST, id); !isPostExist {
  errMsg := fmt.Sprintf("Post not found. Post ID: %s", id)
  span.SetTag("error"true)
  span.LogEvent(errMsg)
  c.JSON(http.StatusNotFound, gin.H{"message": errMsg})
  return
 }

 comments, _ := client.GetCommentsByPostId(ctx, id)
 post := domain.Post{ID: id, Title: "post title", Comments: comments}

 span.LogEvent(fmt.Sprintf("Post fetched by id with comments. Post ID: %s", id))
 c.JSON(http.StatusOK, gin.H{"data": post})
}

func contains([]string, str string) bool {
 for _, v := range s {
  if v == str {
   return true
  }
 }
 return false
}

Post Service comment_client.go

我們將使用名爲 “GetCommentsByPostId” 的函數向評論服務發送 HTTP 請求。

package client

import (
 "context"
 "encoding/json"
 "fmt"
 "net/http"
 "post-service/config"
 "post-service/model"
 "post-service/request"

 "github.com/opentracing/opentracing-go"
)

func GetCommentsByPostId(ctx context.Context, postId string) ([]model.Comment, error) {
 span, _ := opentracing.StartSpanFromContext(ctx, "post-service client /GetComments func")
 defer span.Finish()

 span.LogEvent(fmt.Sprintf("GetCommentsByPostId method. Post ID: %s", postId))

 url := fmt.Sprintf("http://localhost:9090/api/v1/comments?postId=%s", postId)
 req, err := http.NewRequest("GET", url, nil)
 if err != nil {
  return nil, err
 }

 if err := config.Inject(span, req); err != nil {
  return nil, err
 }

 var comments []model.Comment
 data, _ := request.Do(req)
 json.Unmarshal(data, &comments)

 return comments, nil
}

Jaeger Config Files

Post Service Jaeger Config:
package config

import (
 "io"
 "net/http"

 "github.com/opentracing/opentracing-go"
 "github.com/opentracing/opentracing-go/ext"
 "github.com/uber/jaeger-client-go"
 "github.com/uber/jaeger-client-go/config"
)

var Tracer opentracing.Tracer

func InitJaeger() (opentracing.Tracer, io.Closer, error) {
 cfg := config.Configuration{
  Sampler: &config.SamplerConfig{
   Type:  jaeger.SamplerTypeRateLimiting,
   Param: 100, // 100 traces per second
  },
  Reporter: &config.ReporterConfig{
   LocalAgentHostPort: "localhost:6831",
  },
 }

 var err error
 var closer io.Closer
 Tracer, closer, err = cfg.New("post-service")
 return Tracer, closer, err
}

func StartSpanFromRequest(tracer opentracing.Tracer, r *http.Request, funcDesc string) opentracing.Span {
 spanCtx, _ := Extract(tracer, r)
 return tracer.StartSpan(funcDesc, ext.RPCServerOption(spanCtx))
}

func Inject(span opentracing.Span, request *http.Request) error {
 return span.Tracer().Inject(
  span.Context(),
  opentracing.HTTPHeaders,
  opentracing.HTTPHeadersCarrier(request.Header))
}

func Extract(tracer opentracing.Tracer, r *http.Request) (opentracing.SpanContext, error) {
 return tracer.Extract(
  opentracing.HTTPHeaders,
  opentracing.HTTPHeadersCarrier(r.Header))
}
Comment Service Jaeger Config:
package config

import (
 "io"
 "net/http"

 "github.com/opentracing/opentracing-go"
 "github.com/opentracing/opentracing-go/ext"
 "github.com/uber/jaeger-client-go"
 "github.com/uber/jaeger-client-go/config"
)

var Tracer opentracing.Tracer

func InitJaeger() (opentracing.Tracer, io.Closer, error) {
 cfg := config.Configuration{
  Sampler: &config.SamplerConfig{
   Type:  jaeger.SamplerTypeRateLimiting,
   Param: 100, // 100 traces per second
  },
  Reporter: &config.ReporterConfig{
   LocalAgentHostPort: "localhost:6831",
  },
 }

 var err error
 var closer io.Closer
 Tracer, closer, err = cfg.New("comment-service")
 return Tracer, closer, err
}

func StartSpanFromRequest(tracer opentracing.Tracer, r *http.Request, funcDesc string) opentracing.Span {
 spanCtx, _ := Extract(tracer, r)
 return tracer.StartSpan(funcDesc, ext.RPCServerOption(spanCtx))
}

func Extract(tracer opentracing.Tracer, r *http.Request) (opentracing.SpanContext, error) {
 return tracer.Extract(
  opentracing.HTTPHeaders,
  opentracing.HTTPHeadersCarrier(r.Header))
}

Jaeger on Docker

version: '3.8'
services:

  jaeger:
    container_name: jaeger
    image: jaegertracing/all-in-one:1.39
    restart: always
    ports:
      - 6831:6831/udp
      - 6832:6832/udp
      - 16686:16686
      - 14268:14268

使用 “docker-compose up -d” 命令運行 Jaeger 容器。運行後訪問 localhost:16686。

可以看到以下 Jaeger Web UI。

運行服務

運行服務併發送 HTTP 請求, 就可以在 Jaeger 中看到 tracing log:

參考資料

[1]

jaeger github: https://github.com/jaegertracing/jaeger

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/yWAJ1PUm17V7PgQfYodntg