본문 바로가기
자습

Jaeger with Go

by litaro 2021. 7. 20.

 

https://www.jaegertracing.io/

 

Jaeger: open source, end-to-end distributed tracing

Monitor and troubleshoot transactions in complex distributed systems

www.jaegertracing.io

MSA (Microservice Archtecture)에 필요한 것이 바로 분산 추적 시스템이고, Zipkin 이 2012년에 나온뒤에 좀더 현대화된 구조로 만든 분산추적 시스템이 Jaeger (예거)다. 좀더 자세한 히스토리는 아래 그림이 한눈에 잘 보여준다.

zipkin이 single process 구조의 Java 구현체라면, Jaeger는 agent, collector, query가 별도 process로 설계된 Go 구현체이다.

그래서인지 Go 진영에서는 아래와 같이 오래되어 안정된 zipkin보다는 jaeger를 많이 사용하는것 같다.

Jaeger는 그림에서처럼 CNCF OpenTracing API와 연동되고, 기본 구조는 아래와 같다.

Application이 Opentracing API를 활용해서 Instrument 하고 Trace Data를 Agent로 전달하여 Collector에 쌓으면 UI에서는 Query를 통해 MSA서버를 거친 하나의 요청 Trace를 보여주게 된다.

그렇다면 나도 한번 Jaeger를 사용해보자 ^______^

Jaeger 배포 및 설치


docker 를 이용하면 간단하게 demo용 all-in-one image를 설치해볼수 있다. 

http://localhost:16686 로 UI 접근 가능하다.

$ docker run -d --name jaeger -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one:1.22

16686은 UI port 이고, 6831은 client 가 jeager agent 와 통신할 udp port 이다.

Kubernetes 배포


테스트용이 아니라 제대로 사용하려면 Jaeger 사이트에 가이드하고 있는 방법으로 배포해보자. https://www.jaegertracing.io/docs/1.24/operator/

Jaeger는 Kubernetes Operator API를 구현해서 Operator 형태로 제공하고 Custom Resource Definition을 정의해서 Jaeger라는 custom resource로 배포하게 된다.

1. 먼저 Jaeger Operator를 배포한다.

// Jaeger를 별도로 관리하기 위해 observability namespace에서 생성
kubectl create namespace observability #
// kine:Jaeger로 배포가능하도록 custom resource definition을 정의
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml # <2>
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml
// Jaeger 배포시 생성되는 agent, collector, query를 관리하는 operator
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml

// cluster 범위의 권한을 주기위해서 추가로 적용
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role_binding.yaml

2. Jaeger를 배포한다.

데모나 테스트용으로 default strategy 인 AllInOne 배포로 하면 간단하게 아래와 같은 yaml 파일을 갖는다.

apiVersion: jaegertracing.io/v1
kind: Jaeger 👈 Custom Resource
metadata:
  name: jaeger
  namespace: observability

하지만, 이 경우 메모리에 추적 Data를 저장하기 때문에 잠시 Jaeger가 내려오면 그동안 쌓인 추적 기록이 사라진다.

persistent storage, replica, auto scaling과 같은 좀더 세밀한 설정을 위해서는 아래와 같이 stragety가 'production'으로 배포한다.

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: jaeger
  namespace: observability
spec:
  strategy: production
  storage:
    type: elasticsearch
    options:
      es:
        server-urls: https://quickstart-es-http:9200 👈 http elasticsearch service url
        tls:
          ca: /es/certificates/ca.crt
    secretName: jaeger-secret 👈 ID/PW 로 생성한 secret
  volumeMounts:
    - name: certificates
      mountPath: /es/certificates/
      readOnly: true
  volumes:
    - name: certificates
      secret:
        secretName: quickstart-es-http-certs-public

 

Elastic Storage 연동


Jaeger는 ElasticSearch와 Cassandra 두가지 저장소 연동을 지원하고 있는데, 보통 성능면에서 ElasticSearch를 사용한다고 한다. 

Storage를 연동하려면 Jaeger 배포 전, ElasticSearch 배포와 Secret 생성을 하면 된다.

ElasticSearch 배포 

가이드: https://www.elastic.co/guide/en/cloud-on-k8s/1.6/k8s-deploy-elasticsearch.html

아래의 yaml 파일로 배포한다.

apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
  namespace: observability
spec:
  version: 7.13.3
  nodeSets:
    - name: default
      count: 1
      config:
        node.master: true
        node.data: true
        node.ingest: true
        node.store.allow_mmap: false

Secret 생성

기본적으로 Jaeger와 ElasticSearch 와의 통신은 TLS 통신이라 ID PW와 인증서가 필요하다.

가이드 잘 나와있어 따라하면 되는데... 난 이 부분에서 해맸다. 가이드의 "changeme"가 무슨 의미인지 몰라서 ㅠㅠ

$ kubectl create secret generic jaeger-secret --from-literal=ES_PASSWORD=changeme --from-literal=ES_USERNAME=elastic -n observability

"changeme" 대신 아래 실행시 나오는 암호를 사용하면 된다.

$ PASSWORD=$(kubectl get secret quickstart-es-elastic-user -o go-template='{{.data.elastic | base64decode}}' -n observability)

잘 생성되었는지 한번 확인

$ kubectl get secret jaeger-secret -n observability
NAME            TYPE     DATA   AGE
jaeger-secret   Opaque   2      7d10h

이제 production 설정으로 Jaeger yaml을 배포하면 아래와 같다.

$ kubectl get all -n observability
NAME                                           READY   STATUS      RESTARTS   AGE
pod/jaeger-collector-5bcf85f8f8-6sqxx          1/1     Running     0          7d8h
pod/jaeger-operator-64897945d-ff2mj            1/1     Running     0          14d
pod/jaeger-query-7cffd8978f-nvrlb              2/2     Running     0          7d8h
pod/otel-collector-5676c47f47-gf2vw            1/1     Running     0          13d
pod/quickstart-es-default-0                    1/1     Running     0          8d

NAME                                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                  AGE
service/jaeger-collector            ClusterIP   10.100.165.88    <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP   7d8h
service/jaeger-collector-headless   ClusterIP   None             <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP   7d8h
service/jaeger-operator-metrics     ClusterIP   10.100.149.36    <none>        8383/TCP,8686/TCP                        14d
service/jaeger-query                ClusterIP   10.100.44.80     <none>        16686/TCP                                7d8h
service/quickstart-es-default       ClusterIP   None             <none>        9200/TCP                                 8d
service/quickstart-es-http          ClusterIP   10.100.248.65    <none>        9200/TCP                                 8d
service/quickstart-es-transport     ClusterIP   None             <none>        9300/TCP                                 8d

...

HTTP로 ElasticSearch 연동

처음에 TLS가 잘 안되어서... 찾아보니 http로 연동하는 방법도 있어 기록으로 남긴다. 

ElasticSearch 배포 yaml

apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
  namespace: observability
spec:
  version: 7.13.3
  nodeSets:
    - name: default
      count: 1
      config:
        node.master: true
        node.data: true
        node.ingest: true
        node.store.allow_mmap: false
        xpack.security.authc:
          anonymous:
            username: anonymous
            roles: superuser
            authz_exception: false
  http:
    tls:
      selfSignedCertificate:
        disabled: true

Jaeger 배포 yaml

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: jaeger
  namespace: observability
spec:
  strategy: production
  storage:
    type: elasticsearch
    options:
      es:
        server-urls: http://quickstart-es-http:9200

 

Jaeger UI 접근


kubernetes 에 배포하면 UI 접근하기 위한 추가 설정이 필요하다. 

다행히 사이트에 UI 접근 가이드가 나온다.

Jaeger Operator가 외부 접근을 위해 Ingress route를 생성해놨기때문에 Ingress controller를 설치하면 된다.

docker desktop 인 경우, 

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.47.0/deploy/static/provider/cloud/deploy.yaml

아래와 같이 ingress가 잘 생성된것을 확인할수 있다. 이제 http://localhost:80 으로 접근 가능하다.

$ kubectl get ingress -n observability
NAME           CLASS    HOSTS   ADDRESS     PORTS   AGE
jaeger-query   <none>   *       localhost   80      5h13m

 

Go  애플리케이션 구현


이제 Jaeger는 준비되었으니 Test를 위한 Go Server를 만들자.

https://github.com/yurishkuro/opentracing-tutorial/tree/master/go 에 예제가 잘 나와있어서 참고하면 된다.

아래 구조의 간단한 서비스를 만들고 각 서비스들을 거친 요청이 하나의 Trace로 잘 나오는지 확인한다.

                        -----> formatter (service B)
Starter (service A) ---|
                        -----> publisher (service C)

 

Jaeger Tracer 초기화


모든 서비스에서 해야할 부분은 Jaeger에 data를 전송할 준비를 하는 것이다.

ℹ️ 테스트 환경은 Docker-Desktop 에서 아래와 같이 배포해서 하였다. 

<jaeger.yaml>

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: jaeger
  namespace: observability
spec:
  collector:
    serviceType: NodePort // 로컬 테스트앱에서 접근하기 위해 NodePort로 배포

그렇게 되면 아래의 환경이 kubernetes 환경에 잘 배포된것을 확인할수 있다.

kubectl get all -n observability
NAME                                   READY   STATUS    RESTARTS   AGE
pod/jaeger-76dc4d889f-8zx9q            1/1     Running   0          10m
pod/jaeger-operator-6bb854bb84-prtbl   1/1     Running   5          2d16h

NAME                                TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                                                          AGE
service/jaeger-agent                ClusterIP   None           <none>        5775/UDP,5778/TCP,6831/UDP,6832/UDP                              10m
service/jaeger-collector            NodePort    10.99.96.48    <none>        9411:30803/TCP,14250:31270/TCP,14267:30356/TCP,14268:32344/TCP   10m
service/jaeger-collector-headless   ClusterIP   None           <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP                           10m
service/jaeger-operator-metrics     ClusterIP   10.97.36.47    <none>        8383/TCP,8686/TCP                                                2d16h
service/jaeger-query                ClusterIP   10.105.37.50   <none>        16686/TCP                                                        10m

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/jaeger            1/1     1            1           10m
deployment.apps/jaeger-operator   1/1     1            1           2d16h

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/jaeger-76dc4d889f            1         1         1       10m
replicaset.apps/jaeger-operator-6bb854bb84   1         1         1       2d16h

NodePort로 배포된 Jaeger Collector와 연동하는 Tracer를 생성하면 된다. 이때, Port는 Http port인 14268 의 node port 32344 를 사용한다.

// initJaeger returns an instance of Jaeger Tracer that samples 100% of traces and logs all spans to stdout.
func initJaeger(service string) (opentracing.Tracer, io.Closer) {
	cfg := &config.Configuration{
		ServiceName: service, // 해당 서비스 이름
		Sampler: &config.SamplerConfig{ 
			Type:  "const",
			Param: 1, // sampler type 을 const=1 로 설정하면 모든 span을 다 sampling하겠다는 설정
		},
		Reporter: &config.ReporterConfig{
			LogSpans:          true,
			CollectorEndpoint: "http://localhost:32344/api/traces", // Collector URL
		},
	}
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	return tracer, closer
}

 

Client Side 


Starter 의 main()

import (
	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"
	"github.com/opentracing/opentracing-go/log"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	xhttp "github.com/yurishkuro/opentracing-tutorial/go/lib/http"
...
)

func main() {
	if len(os.Args) != 2 {
		panic("ERROR: Expecting one argument")
	}

	tracer, closer := initJaeger("starter")
	defer closer.Close()
	opentracing.SetGlobalTracer(tracer)

	helloTo := os.Args[1]

	span := tracer.StartSpan("say-hello") // root span 시작 ✅
	ctx := context.Background()
	ctx = opentracing.ContextWithSpan(ctx, span) // context 에 span 정보 전달
	span.SetTag("hello-to", helloTo) // 옵션: root span 에 tag 달기
	helloStr := formatString(ctx, helloTo) // context와 함께 formatter 호출
	printHello(ctx, helloStr) // context와 함께 publisher 호출

	span.Finish()
}

Formatter 호출


func formatString(ctx context.Context, helloTo string) string {
	// 전달 받은 context 에 Span 추가 ✅
	span, _ := opentracing.StartSpanFromContext(ctx, "formatString")
	defer span.Finish()

	// formatter 호출을 위한 http request 준비 
	v := url.Values{}
	v.Set("helloTo", helloTo)
	url := "http://localhost:8081/format?" + v.Encode()
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		panic(err.Error())
	}
	// formatter를 http로 호출할테니 RPCClient 로 span 설정
	ext.SpanKindRPCClient.Set(span)
	ext.HTTPUrl.Set(span, url)
	ext.HTTPMethod.Set(span, "GET")

	// Inject로 http header를 통해 span을 전달 ✅
	span.Tracer().Inject(
		span.Context(),
		opentracing.HTTPHeaders,
		opentracing.HTTPHeadersCarrier(req.Header),
	)

	resp, err := xhttp.Do(req)
	if err != nil {
		ext.LogError(span, err)
		panic(err.Error())
	}

	helloStr := string(resp)

	span.LogFields(
		log.String("event", "string-format"),
		log.String("value", helloStr),
	)

	return helloStr
}

 

Server Side


Fomatter 의 main()

func main() {
	tracer, closer := initJaeger("formatter")
	defer closer.Close()

	http.HandleFunc("/format", func(w http.ResponseWriter, r *http.Request) {
    	// Http Header를 통해 들어온 정보를 추출 ✅
		spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
        // span 추가
		span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx))
		defer span.Finish()

		helloTo := r.FormValue("helloTo")
		helloStr := fmt.Sprintf("Hello, %s!", helloTo)
		span.LogFields(
			otlog.String("event", "string-format"),
			otlog.String("value", helloStr),
		)
		w.Write([]byte(helloStr))
	})

	log.Fatal(http.ListenAndServe(":8081", nil))
}

 

정리


  • Jaeger는 2017년 Uber에서 발표했고, Zipkin보다는 최근에 개발되어 현대화된 구조를 적용하여 Agent, Collector, Query가 별도의 프로세서로 이루어진 Go 언어 구현체 이다. 
  • Jaeger는 Kubernetes 로 배포하고 Kubernetes Operator를 구현한 Jaeger Operator 형태로 배포하여 Jaeger를 custom resource 로 배포하게 된다.
  • Jaeger 배포시 다양한 설정이 가능하고, 그 중 하나가 persistent storage 이다. elastic search와 cassandra가 가능한데, 주로 elastic search를 많이 사용한다.
  • OpenTracing Client Go로 Jager와 연동 가능하다. Tutorial 이 잘되어 있어 예제를 따라하면 이해하기 편하다.
  • Root span 생성은 StartSpan, Child span 생성은 StartSpanFromContext, 서비스간 추적 데이터 전달은 주입은 Inject, 추출은 Extract 로 직관적인 API 설계로 쉽게 사용할수 있다.

 

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

go gRPC Server & gRPC Gateway  (0) 2021.09.20
Kong Gateway + Konga  (0) 2021.08.29
Zipkin Go  (0) 2021.07.17
Localstack 살펴보기  (0) 2021.07.16
React + Gin + Emqx Chat App  (0) 2021.02.27