微服務 - 如何解決鏈路追蹤問題


一、鏈路追蹤

​ 微服務架構是將單個應用程序被划分成各種小而連接的服務,每一個服務完成一個單一的業務功能,相互之間保持獨立和解耦,每個服務都可以獨立演進。相對於傳統的單體服務,微服務具有隔離性、技術異構性、可擴展性以及簡化部署等優點。

​ 同樣的,微服務架構在帶來諸多益處的同時,也為系統增加了不少復雜性。它作為一種分布式服務,通常部署於由不同的數據中心、不同的服務器組成的集群上。而且,同一個微服務系統可能是由不同的團隊、不同的語言開發而成。通常一個應用由多個微服務組成,微服務之間的數據交互需要通過遠過程調用的方式完成,所以在一個由眾多微服務構成的系統中,請求需要在各服務之間流轉,調用鏈路錯綜復雜,一旦出現問題,是很難進行問題定位和追查異常的。

​ 鏈路追蹤系統就是為解決上述問題而產生的,它用來追蹤每一個請求的完整調用鏈路,記錄從請求開始到請求結束期間調用的任務名稱、耗時、標簽數據以及日志信息,並通過可視化的界面進行分析和展示,來幫助技術人員准確地定位異常服務、發現性能瓶頸、梳理調用鏈路以及預估系統容量。

​ 鏈路追蹤系統的理論模型幾乎都借鑒了 Google 的一篇論文”Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”,典型產品有Uber jaeger、Twitter zipkin、淘寶鷹眼等。這些產品的實現方式雖然不盡相同,但核心步驟一般都有三個:數據采集、數據存儲和查詢展示

​ 鏈路追蹤系統第一步,也是最基本的工作就是數據采集。在這個過程中,鏈路追蹤系統需要侵入用戶代碼進行埋點,用於收集追蹤數據。但是由於不同的鏈路追蹤系統的API互不兼容,所以埋點代碼寫法各異,導致用戶在切換不同鏈路追蹤產品時需要做很大的改動。為了解決這類問題,於是誕生了OpenTracing規范,旨在統一鏈路追蹤系統的API。

二、OpenTracing規范

​ OpenTracing 是一套分布式追蹤協議,與平台和語言無關,具有統一的接口規范,方便接入不同的分布式追蹤系統。

​ OpenTracing語義規范詳見:https://github.com/opentracing/specification/blob/master/specification.md

2.1 數據模型(Data Model)

​ OpenTracing語義規范中定義的數據模型有 Trace、Sapn以及Reference。

2.1.1 Trace

​ Trace表示一條完整的追蹤鏈路,例如:一個事務或者一個流程的執行過程。一個 Trace 是由一個或者多個 Span 組成的有向無環圖(DAG)。

下圖表示一個由8個Span組成的Trace:

        [Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G `FollowsFrom` Span F)

按照時間軸方式更為直觀地展現該Trace:

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]
2.1.2 Span

​ Span表示一個獨立的工作單元,它是一條追蹤鏈路的基本組成要素。例如:一次RPC調用、一次函數調用或者一次Http請求。

每個Span封裝了如下狀態:

2.1.3 Reference

​ 一個Span可以與一個或者多個Span存在因果關系,這種關系稱為Reference。OpenTracing目前定義了兩種關系:ChildOf(父子)關系 和 FollowsFrom(跟隨)關系。

  • ChildOf關系

    父Span的執行依賴子Span的執行結果,此時子Span對父Span的Reference關系是ChildOf。比如對於一次RPC調用,服務端的Span(子Span)與客戶端調用的Span(父Span)就是ChildOf關系。

  • FollowsFrom關系

    父Span的執行不依賴子Span的執行結果,此時子Span對父Span的Reference關系是FollowFrom。FollowFrom常用於表示異步調用,例如消息隊列中Consumer Span與Producer Span之間的關系。

2.2 應用接口(API)

2.2.1 Tracer

​ Tracer接口用於創建Span、跨進程注入數據和提取數據。通常具有以下功能:

  • Start a new span
    創建並啟動一個新的Span。
  • Inject
    將SpanContext注入載體(Carrier)。
  • Extract
    從載體(Carrier)中提取SpanContext。
2.2.2 Span
  • Retrieve a SpanContext
    返回Span對應的SpanContext。

  • Overwrite the operation name
    更新操作名稱。

  • Set a span tag
    設置Span標簽數據。

  • Log structured data
    記錄結構化數據。

  • Set a baggage item
    baggage item是字符串型的鍵值對,它對應於某個 Span,隨Trace一起傳播。由於每個鍵值都會被拷貝到每一個本地及遠程的子Span,這可能導致巨大的網絡和CPU開銷。

  • Get a baggage item
    獲取baggage item的值。

  • Finish
    結束一個Span。

2.2.3 Span Context

​ 用於攜帶跨越服務邊界的數據,包括trace ID、Span ID以及需要傳播到下游Span的baggage數據。在OpenTracing中,強制要求SpanContext實例不可變,以避免在Span完成和引用時出現復雜的生命周期問題。

2.2.4 NoopTracer

​ 所有對OpenTracing API的實現,必須提供某種形式的NoopTracer,用於標記控制OpenTracing或注入對測試無害的東西。

三、Jaeger

​ Jaeger是Uber開源的分布式追蹤系統,它的應用接口完全遵循OpenTracing規范。jaeger本身采用go語言編寫,具有跨平台跨語言的特性,提供了各種語言的客戶端調用接口,例如c++、java、go、python、ruby、php、nodejs等。項目地址:https://github.com/jaegertracing

3.1 Jaeger組件

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-miLIEWHv-1604561903414)(https://i.loli.net/2020/04/13/bvTxdUkBRuawY1F.png)]

  • jaeger-client

    jaeger的客戶端代碼庫,它實現了OpenTracing協議。當我們的應用程序將其裝配后,負責收集數據,並發送到jaeger-agent。這是我們唯一需要編寫代碼的地方

  • jaeger-agent

    負責接收從jaeger-client發來的Trace/Span信息,並批量上傳到jaeger-collector。

  • jaeger-collector

    負責接收從jaeger-agent發來的Trace/Span信息,並經過校驗、索引等處理,然后寫入到后端存儲。

  • data store

    負責數據存儲。Jaeger的數據存儲是一個可插拔的組件,目前支持Cassandra、ElasticSearch和Kafka。

  • jaeger-query & ui

    負責數據查詢,並通過前端界面展示查詢結果。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ogkrm3Hb-1604561903417)(https://i.loli.net/2020/04/13/UMoHYtlX1ydsx5Q.jpg)]

3.2 快速入門

​ Jaeger官方提供了all-in-one鏡像,方便快速進行測試:

# 拉取鏡像
$docker pull jaegertracing/all-in-one:latest

# 運行鏡像
$docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 14268:14268 \
  -p 9411:9411 \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

​ 通過all-in-one鏡像啟動,我們發現Jaeger占據了很多端口。以下是端口使用說明:

端口 協議 所屬模塊 功能
5775 UDP agent 接收壓縮格式的Zipkin thrift數據
6831 UDP agent 接收壓縮格式的Jaeger thrift數據
6832 UDP agent 接收二進制格式的Jaeger thrift數據
5778 HTTP agent 服務配置、采樣策略端口
14268 HTTP collector 接收由客戶端直接發送的Jaeger thrift數據
9411 HTTP collector 接收Zipkin發送的json或者thrift數據
16686 HTTP query 瀏覽器展示端口

​ 啟動后,我們可以訪問 http://localhost:16686 ,在瀏覽器中查看和查詢收集的數據。

​ 由於通過all-in-one鏡像方式收集的數據都存儲在docker中,無法持久保存,所以只能用於開發或者測試環境,無法用於生產環境。生產環境中需要依據實際情況,分別部署各個組件。

四、Jaeger在業務代碼中的應用

​ 系統中使用Jaeger非常簡單,只需要在原有程序中插入少量代碼。以下代碼模擬了一個查詢用戶賬戶余額,執行扣款的業務場景:

4.1 初始化jaeger函數

​ 主要是按照實際需要配置有關參數,例如服務名稱、采樣模式、采樣比例等等。

func initJaeger() (tracer opentracing.Tracer, closer io.Closer, err error) {
	// 構造配置信息
	cfg := &config.Configuration{
		// 設置服務名稱
		ServiceName: "ServiceAmount",
		// 設置采樣參數
		Sampler: &config.SamplerConfig{
			Type:  "const", // 全采樣模式
			Param: 1,       // 開啟狀態
		},
	}
	
	// 生成一條新tracer
	tracer, closer, err = cfg.NewTracer()
	if err == nil {
		// 設置tracer為全局單例對象
		opentracing.SetGlobalTracer(tracer)
	}
	return
}

4.2 檢測用戶余額函數

​ 用於檢測用戶余額,模擬一個子任務Span。

func CheckBalance(request string, ctx context.Context) {
	// 創建子span
	span, _ := opentracing.StartSpanFromContext(ctx, "CheckBalance")

	// 模擬系統進行一系列的操作,耗時1/3秒
	time.Sleep(time.Second / 3)

	// 示例:將需要追蹤的信息放入tag
	span.SetTag("request", request)
	span.SetTag("reply", "CheckBalance reply")

	// 結束當前span
	span.Finish()

	log.Println("CheckBalance is done")
}

4.3 從用戶賬戶扣款函數

​ 從用戶賬戶扣款,模擬一個子任務span。

func Reduction(request string, ctx context.Context) {
	// 創建子span
	span, _ := opentracing.StartSpanFromContext(ctx, "Reduction")

	// 模擬系統進行一系列的操作,耗時1/2秒
	time.Sleep(time.Second / 2)

	// 示例:將需要追蹤的信息放入tag
	span.SetTag("request", request)
	span.SetTag("reply", "Reduction reply")

	// 結束當前span
	span.Finish()

	log.Println("Reduction is done")
}

4.4 主函數

​ 初始化jaeger環境,生成tracer,創建父span,以及調用查詢余額和扣款兩個子任務span。

package main

import (
	"context"
	"fmt"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go/config"
	"io"
	"log"
	"time"
)

func main() {
	// 初始化jaeger,創建一條新tracer
	tracer, closer, err := initJaeger()
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	defer closer.Close()

	// 創建一個新span,作為父span,開始計費過程
	span := tracer.StartSpan("CalculateFee")
	
	// 生成父span的context
	ctx := opentracing.ContextWithSpan(context.Background(), span)

	// 示例:設置一個span標簽信息
	span.SetTag("db.instance", "customers")
	// 示例:輸出一條span日志信息
	span.LogKV("event", "timed out")

	// 將父span的context作為參數,調用檢測用戶余額函數
	CheckBalance("CheckBalance request", ctx)

	// 將父span的context作為參數,調用扣款函數
	Reduction("Reduction request", ctx)

	// 結束父span
	span.Finish()
}

五、Jaeger在gRPC微服務中的應用

​ 我們依然模擬了一個查詢用戶賬戶余額,執行扣款的業務場景,並把查詢用戶賬戶余額和執行扣款功能改造為gRPC微服務:

5.1 gRPC Server端代碼

main.go:

​ 代碼使用了第三方依賴庫github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing,該依賴庫將OpenTracing封裝為通用的gRPC中間件,並通過gRPC攔截器無縫嵌入gRPC服務中。

package main

import (
	"fmt"
	"github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go/config"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"grpc-jaeger-server/account"
	"io"
	"log"
	"net"
)

// 初始化jaeger
func initJaeger() (tracer opentracing.Tracer, closer io.Closer, err error) {
	// 構造配置信息
	cfg := &config.Configuration{
		// 設置服務名稱
		ServiceName: "ServiceAmount",

		// 設置采樣參數
		Sampler: &config.SamplerConfig{
			Type:  "const", // 全采樣模式
			Param: 1,       // 開啟全采樣模式
		},
	}

	// 生成一條新tracer
	tracer, closer, err = cfg.NewTracer()
	if err == nil {
		// 設置tracer為全局單例對象
		opentracing.SetGlobalTracer(tracer)
	}
	return
}

func main() {
	// 初始化jaeger,創建一條新tracer
	tracer, closer, err := initJaeger()
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	defer closer.Close()
	log.Println("succeed to init jaeger")

	// 注冊gRPC account服務
	server := grpc.NewServer(grpc.UnaryInterceptor(grpc_opentracing.UnaryServerInterceptor(grpc_opentracing.WithTracer(tracer))))
	account.RegisterAccountServer(server, &AccountServer{})
	reflection.Register(server)
	log.Println("succeed to register account service")

	// 監聽gRPC account服務端口
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Println(err)
		return
	}
	log.Println("starting register account service")

	// 開啟gRpc account服務
	if err := server.Serve(listener); err != nil {
		log.Println(err)
		return
	}
}

計費微服務 accountsever.go:

package main

import (
	"github.com/opentracing/opentracing-go"
	"golang.org/x/net/context"
	"grpc-jaeger-server/account"
	"time"
)

// 計費服務
type AccountServer struct{}

// 檢測用戶余額微服務,模擬子span任務
func (s *AccountServer) CheckBalance(ctx context.Context, request *account.CheckBalanceRequest) (response *account.CheckBalanceResponse, err error) {
	response = &account.CheckBalanceResponse{
		Reply: "CheckBalance Reply", // 處理結果
	}

	// 創建子span
	span, _ := opentracing.StartSpanFromContext(ctx, "CheckBalance")

	// 模擬系統進行一系列的操作,耗時1/3秒
	time.Sleep(time.Second / 3)

	// 將需要追蹤的信息放入tag
	span.SetTag("request", request)
	span.SetTag("reply", response)

	// 結束當前span
	span.Finish()

	return response, err
}

// 從用戶賬戶扣款微服務,模擬子span任務
func (s *AccountServer) Reduction(ctx context.Context, request *account.ReductionRequest) (response *account.ReductionResponse, err error) {
	response = &account.ReductionResponse{
		Reply: "Reduction Reply", // 處理結果
	}

	// 創建子span
	span, _ := opentracing.StartSpanFromContext(ctx, "Reduction")

	// 模擬系統進行一系列的操作,耗時1/3秒
	time.Sleep(time.Second / 3)

	// 將需要追蹤的信息放入tag
	span.SetTag("request", request)
	span.SetTag("reply", response)

	// 結束當前span
	span.Finish()
	return response, err
}

5.2 gRPC Client端代碼main.go:

package main

import (
	"context"
	"fmt"
	"github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go/config"
	"google.golang.org/grpc"
	"grpc-jaeger-client/account"
	"io"
	"log"
)

// 初始化jaeger
func initJaeger() (tracer opentracing.Tracer, closer io.Closer, err error) {
	// 構造配置信息
	cfg := &config.Configuration{
		// 設置服務名稱
		ServiceName: "ServiceAmount",

		// 設置采樣參數
		Sampler: &config.SamplerConfig{
			Type:  "const", // 全采樣模式
			Param: 1,       // 開啟全采樣模式
		},
	}

	// 生成一條新tracer
	tracer, closer, err = cfg.NewTracer()
	if err == nil {
		// 設置tracer為全局單例對象
		opentracing.SetGlobalTracer(tracer)
	}
	return
}

func main() {
	// 初始化jaeger,創建一條新tracer
	tracer, closer, err := initJaeger()
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	defer closer.Close()
	log.Println("succeed to init jaeger")

	// 創建一個新span,作為父span
	span := tracer.StartSpan("CalculateFee")

	// 函數返回時關閉span
	defer span.Finish()

	// 生成span的context
	ctx := opentracing.ContextWithSpan(context.Background(), span)

	// 連接gRPC server
	conn, err := grpc.Dial("localhost:8080",
		grpc.WithInsecure(),
		grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(grpc_opentracing.WithTracer(tracer),
		)))
	if err != nil {
		log.Println(err)
		return
	}

	// 創建gRPC計費服務客戶端
	client := account.NewAccountClient(conn)

	// 將父span的context作為參數,調用檢測用戶余額的gRPC微服務
	checkBalanceResponse, err := client.CheckBalance(ctx,
		&account.CheckBalanceRequest{
			Account: "user account",
		})
	if err != nil {
		log.Println(err)
		return
	}
	log.Println(checkBalanceResponse)

	// 將父span的context作為參數,調用扣款的gRPC微服務
	reductionResponse, err := client.Reduction(ctx,
		&account.ReductionRequest{
			Account: "user account",
			Amount: 1,
		})
	if err != nil {
		log.Println(err)
		return
	}
	log.Println(reductionResponse)
}

注:
本文全部源代碼位於:https://github.com/wangshizebin/micro-service
本文時候用的開發工具為:goland 來自於嗖嗖下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM