본문 바로가기
자습

Zipkin Go

by litaro 2021. 7. 17.

https://zipkin.io/

 

OpenZipkin · A distributed tracing system

Zipkin Zipkin is a distributed tracing system. It helps gather timing data needed to troubleshoot latency problems in service architectures. Features include both the collection and lookup of this data. If you have a trace ID in a log file, you can jump di

zipkin.io

요즘 핫한 MSA (Microservice Architecture) 구조로 서버를 개발하면 하나의 호출이 여러 서비스를 거쳐서 이루어 지게 되는데, 이렇게 분산되어있는 서비스간 호출을 한눈에 파악하기란 쉽지 않다. 이를 위해 분산 추적 시스템이 필요하게 되고, 트위터가 아래와 같이 한눈에 여러 서비스간 호출을 보여주는 Zipkin을 2012년 오픈소스로 릴리즈했다. 이후 다른 분산추적 시스템이 나오긴 했지만 원조인 Zipkin을 어떻게 사용하는지 한번 알아보자.

Zipkin 구조

MSA의 각 서비스들은 Zipkin 라이브러리를 통해 다른 서비스로 요청시 Header에 TraceID, SpanID와 같은 정보를 전달하고, 또한 추가로 필요한 여러 추적 정보를 Zipkin Collector에 Async로 전달하게 된다.

기본 동작 

Zipkin 사이트의 아래 Flow를 보면 쉽게 동작을 이해할 수 있다. 

  • 요청시 Header에 TraceId, SpanId를 Zipkin Header 로 다음 서비스로 전달
  • 추가적인 정보 (method, timestamp, duration...) 을 Async 하게 Collector로 전달
  • Collector에서는 Storage에 해당 정보들을 저장하고 통합하여 하나의 Trace로 UI에 표시 

예제 실습

docker 를 통해 쉽게 실행 가능하다.

$ docker run -d -p 9411:9411 openzipkin/zipkin

http://localhost:9411/zipkin/  로 Zipkin시스템에서 제공하는 UI로 한눈에 요청을 볼수 있다.

Main.go

import (
    zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http"
...
)
func main() {
	// Create Tracer
    tracer, err := newTracer()
    ...
    // We add the instrumented transport to the defaultClient
    // that comes with the zipkin-go library
    
    // Create global zipkin traced http client
    client, err := zipkinhttp.NewClient(
        tracer,
        zipkinhttp.ClientTrace(true),
    )
    ...
    r := mux.NewRouter()
    // 8083 port의 your_service 호출
    r.Methods("GET").Path("/some_function").HandlerFunc(someFunc(client, "http://127.0.0.1:8083"))
   
    // Create global zipkin http server middleware
    // 요청이 들어오면 'request' span start
	r.Use(zipkinhttp.NewServerMiddleware(
        tracer,
        zipkinhttp.SpanName("request")),
    )
    log.Fatal(http.ListenAndServe(":8080", r))
}

Tracer 생성

import (
    "github.com/openzipkin/zipkin-go/model"
    "github.com/openzipkin/zipkin-go"
    reporterhttp "github.com/openzipkin/zipkin-go/reporter/http"
...
)

// Zipkin Server url
const endpointURL = "http://localhost:9411/api/v2/spans"                        
 
func newTracer() (*zipkin.Tracer, error) {
    // The reporter sends traces to zipkin server
 	// Zipkin Server url을 endpoint로 설정한 Reporter 생성
    reporter := reporterhttp.NewReporter(endpointURL)                          
 
    // Local endpoint represent the local service information
    // 자기자신의 local endpoint를 ServiceName 'my_service'로 설정
    localEndpoint := &model.Endpoint{ServiceName: "my_service", Port: 8080}  
 
    // Sampler tells you which traces are going to be sampled or not. 
    // trace sampling을 1로 설정하여 모든 trace를 저장
    sampler, err := zipkin.NewCountingSampler(1)                               
...
    // reporter, sampler, localendpoint로 zipkin tracer 생성
    t, err := zipkin.NewTracer(                                                
        reporter,
        zipkin.WithSampler(sampler),
        zipkin.WithLocalEndpoint(localEndpoint),
    )
...
 
    return t, err
}

Context 전달

Client 

import (
    "github.com/opentracing/opentracing-go/ext"
...
)
 
func someFunc(client *zipkinhttp.Client, url string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // middleware를 통해 request에 저장된 context로 부터 'request' span 가져와서 Tag나 Annotate 설정
        span := zipkin.SpanFromContext(r.Context()) 
        // 해당 span에 Tag 설정
        span.Tag("custom_key", "some value")                             
 
        // 해당 span에 시간 설정
        time.Sleep(25 * time.Millisecond)
        span.Annotate(time.Now(), "expensive_calc_done")                       
 
        // your_service 호출할 Request생성
        newRequest, err := http.NewRequest("POST", url+"/other_function", nil) 
...
        // 'request'span으로 context 생성
        ctx := zipkin.NewContext(newRequest.Context(), span)  
        // context를 your_service Request 에 설정
        newRequest = newRequest.WithContext(ctx)    
        // 'other_function' span 이름으로 http로 your_service 호출
        res, err := client.DoWithAppSpan(newRequest, "other_function")
        if err != nil {
            log.Printf("call to other_function returned error: %+v\n", err)
            http.Error(w, err.Error(), 500)
            return
        }
        res.Body.Close()
    }
}

Server 

func main() {
    tracer, err := newTracer()
    if err != nil {
        log.Fatal(err)
    }
 
    r := mux.NewRouter()
    r.Methods("POST").Path("/other_function").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("other_function called with method: %s\n", r.Method)
        time.Sleep(50 * time.Millisecond)
    })
    // 'my-service'로 전달된 context로 'request' span start
    r.Use(zipkinhttp.NewServerMiddleware(                                   
        tracer,
        zipkinhttp.SpanName("request")), // name for request span
    )
    log.Fatal(http.ListenAndServe(":8083", r))
}

 

결과 확인

http://localhost:9411/zipkin/ 

 

'자습' 카테고리의 다른 글

Kong Gateway + Konga  (0) 2021.08.29
Jaeger with Go  (1) 2021.07.20
Localstack 살펴보기  (0) 2021.07.16
React + Gin + Emqx Chat App  (0) 2021.02.27
Go OAuth2.0 서버  (0) 2021.01.31