# Opentracing Jaeger-client golang/JAVA applicaion에 적용하기[Golang 편]

2020. 8. 23. 18:21 개발 이야기/Golang

안녕하세요. 해커의 개발일기 입니다.

 

오늘은 마이크로 서비스 아키텍처를 사용하여 구축된 응용 프로그램을 프로파일링하고 모니터링하는데 사용하는

Distributed Tracing 분산 추적 기술에 중!

 

 

오늘은 지난번에 소개해드렸던

https://bourbonkk.tistory.com/84?category=794341

 

# 오픈소스 OpenTracing - Jaeger

안녕하세요. 해커의 개발일기 입니다. 오늘은 마이크로 서비스 아키텍처를 사용하여 구축된 응용 프로그램을 프로파일링하고 모니터링하는데 사용하는 Distributed Tracing 분산 추적 기술에 대해서

bourbonkk.tistory.com

instrumentation 작업을 통해 latency를 측정하고자 하는 곳에 코드를 심어주게 되면 서비스는 그 코드를 심은 부분의 함수가 끝날 때마다 span 데이터 jaeger-agent UDP 형태로 보내게 되는데요. 이때TraceID를 함께 보내 백엔드에서는 동일한 TraceID select를 하게 되면 완전한 trace 정보가 완성되게 되는 원리입니다!

위의 예시 그림을 보게 되면Servica A span 데이터는 총 3개(빨간색 라인)를 받았을 것이고, Service B의 span 데이터는 1개를 받았을 것입니다. jaeger-agent span 데이터를 보내고, 이 jaeger-agent jaeger-collector에 전달을 해서kafka DB에 바로 넣어주게 되는데요. 이때 백엔드에서 같은 TraceID를 이용해 조회를 하게 되면 위와 같은 완전한 trace 정보가 완성되게 됩니다!~ Service A의 끝나는 시간과Service B의 시작시간의 Gap이 바로 Network Latency가 되겠죠?

자 그러면, Jaeger와 관련해서 어떻게 하면 실제 어플리케이션에 instrumentation을 적용해서 서비스 구간구간 소요시간을 알 수 있는지 알아보도록 하겠습니다. 제가 적용해본 언어는 두 가지가 있는데요. 바로 Golang으로 작성된 app JAVA로 작성된 app입니다. 두 가지 예시를 모두 소개해드릴 생각인데요. 

JAVA app의 경우에는 special-agent라는 auto-instrumentation 해주는 프로젝트를 이용해 적용하는 방법을 소개했기 때문에 Golang App의 실제 적용 방법 소개해드리겠습니다.

# Opentracing Jaeger-client golang/JAVA applicaion에 적용하기[JAVA 편]

https://bourbonkk.tistory.com/100

 

# Opentracing Jaeger-client golang/JAVA applicaion에 적용하기[JAVA 편]

안녕하세요. 해커의 개발일기 입니다. 오늘은 마이크로 서비스 아키텍처를 사용하여 구축된 응용 프로그램을 프로파일링하고 모니터링하는데 사용하는 Distributed Tracing 분산 추적 기술에 중! 오

bourbonkk.tistory.com

 

먼저 제가 사용할 jaeger-clientuber에서 만든 jaeger-client-go를 사용할 건데요. 이것만 사용하니 몇 가지 의존성들이 문제가 되는 것들이 있었어서 제가 사용한 모듈들이 나열된 go.mod 파일을 소개해드리겠습니다.

github.com/go-kit/kit v0.10.0
github.com/gorilla/mux v1.7.4
github.com/jaegertracing/jaeger v1.18.1
github.com/opentracing-contrib/go-stdlib v1.0.0
github.com/opentracing/opentracing-go v1.1.0
github.com/prometheus/client_golang v1.4.1
github.com/uber/jaeger-client-go v2.25.0+incompatible
github.com/uber/jaeger-lib v2.2.0+incompatible
go.uber.org/zap v1.15.0
golang.org/x/net v0.0.0-20200202094626-16171245cfb2

의존성 문제가 없으면 위에 go.mod 파일은 참고 안 하셔도 상관없습니다. 자 그럼 시작하겠습니다.

먼저 go get으로 모듈을 설치하겠습니다.

go get -u github.com/uber/jaeger-client-go/

 

간략하게 jaeger 프로젝트에서 발췌한 trace init 함수를 통해 설명하도록 하겠습니다.

// Init creates a new instance of Jaeger tracer.
func Init(serviceName string, logger log2.Logger) opentracing.Tracer {
	cfg, err := config.FromEnv()
	if err != nil {
		logger.Log("cannot parse Jaeger env vars", zap.Error(err))
	}
	cfg.ServiceName = serviceName

	// TODO(ys) a quick hack to ensure random generators get different seeds, which are based on current time.
	time.Sleep(100 * time.Millisecond)
	metricsFactory := prometheus.New().Namespace(metrics.NSOptions{Name: serviceName, Tags: nil}) // metrics.NSOptions{Name: serviceName, Tags: nil}
	tracer, _, err := cfg.NewTracer(
		config.Metrics(metricsFactory),
		config.Observer(rpcmetrics.NewObserver(metricsFactory, rpcmetrics.DefaultNameNormalizer)),
	)
	if err != nil {
		logger.Log("cannot initialize Jaeger Tracer", zap.Error(err))
	}
	return tracer
}

logger는 사용하고 계신 log로 변경해주시면 될 것 같습니다.

config.FromEnv() 메서드를 이용해 JAEGER 설정에 필요한 값들을 가져오겠습니다. 이 메서드는 jaeger-client-go/config/config.go에 있는 메서드인데 리턴값인 Configuration 구조체를 보면 Jaeger 설정값을 파싱 하는 것을 알 수 있는데요.  대략적으로 아래와 같은 구조체인 것을 볼 수 있습니다.

MetricFactory를 만들어 config.Metrics()에 넣어 tracer를 생성합니다. 이 tracer를 리턴해 tracing이 필요한 부분에 사용합니다.

다음은 main.go인데요 main에서 tracer를 생성해 http 통신을 하는 곳에 span 데이터를 뽑아낼 수 있도록 코드를 넣어줄 예정인데요 

	tracer = tracing.Init(ServiceName, logger)
	opentracing.InitGlobalTracer(tracer)

main에서 tracer를 생성하고 GlobalTracer에 넣어줍니다. 생성된 tracer를 이용해 trace 하고자 하는 곳이 코드를 심어주면 됩니다. HTTPHandler를 만드는 예시를 보겠습니다.

##main.go

	// Endpoint domain.
	endpoints := api.MakeEndpoints(service, tracer)

	// HTTP router
	router := api.MakeHTTPHandler(endpoints, logger, tracer)

	httpMiddleware := []commonMiddleware.Interface{
		commonMiddleware.Instrument{
			Duration:     HTTPLatency,
			RouteMatcher: router,
		},
	}

 tracerMakeEndpoints 메서드에 전달해줍니다.

# endpoints.go

// MakeEndpoints returns an Endpoints structure, where each endpoint is
// backed by the given service.
func MakeEndpoints(s Service, tracer stdopentracing.Tracer) Endpoints {
	return Endpoints{
		LoginEndpoint:       opentracing.TraceServer(tracer, "GET /login")(MakeLoginEndpoint(s)),
		RegisterEndpoint:    opentracing.TraceServer(tracer, "POST /register")(MakeRegisterEndpoint(s)),
		HealthEndpoint:      opentracing.TraceServer(tracer, "GET /health")(MakeHealthEndpoint(s)),
		UserGetEndpoint:     opentracing.TraceServer(tracer, "GET /customers")(MakeUserGetEndpoint(s)),
		UserPostEndpoint:    opentracing.TraceServer(tracer, "POST /customers")(MakeUserPostEndpoint(s)),
		AddressGetEndpoint:  opentracing.TraceServer(tracer, "GET /addresses")(MakeAddressGetEndpoint(s)),
		AddressPostEndpoint: opentracing.TraceServer(tracer, "POST /addresses")(MakeAddressPostEndpoint(s)),
		CardGetEndpoint:     opentracing.TraceServer(tracer, "GET /cards")(MakeCardGetEndpoint(s)),
		DeleteEndpoint:      opentracing.TraceServer(tracer, "DELETE /")(MakeDeleteEndpoint(s)),
		CardPostEndpoint:    opentracing.TraceServer(tracer, "POST /cards")(MakeCardPostEndpoint(s)),
	}
}

 부분은 micro-socks라는 예제 프로그램의 코드를 발췌한 것입니다. opentracing.TraceServerOperationName이라는 span에서 next 엔드포인트를 래핑 하는 미들웨어를 반환합니다. OperationName은 여기서 "GET /health" 이런 것들이 되겠습니다. 즉 http 메서드 요청이 와서 해당 부분이 실행될 때 실행될때 "GET /health" operationName을 가진 root span으로 rapping이 되고 MakeHealthEndPoint에서 정의한 하위 spanchild span이 되도록 정의해줍니다. 잘 이해가 안 갈 수 있기 때문에 그림을 참고하겠습니다. 실제로 온 요청은 "GET /health"로 요청이 오는데요 최 상위 rootspan이 아래 그림처럼 GET /health이 되도록 최상위 rapping을 해주는 것입니다. 만약 traceServer를 안 만들어주었다면 그 밑에 health check operation이 가장 최상위 span이 되겠죠? 이해가 되셨길 바라겠습니다..

코드를 한번 확인해보겠습니다. 요청이 왔을 때 contextspan이 없다면 operationName을 설정해줍니다.

// TraceServer returns a Middleware that wraps the `next` Endpoint in an
// OpenTracing Span called `operationName`.
//
// If `ctx` already has a Span, it is re-used and the operation name is
// overwritten. If `ctx` does not yet have a Span, one is created here.
func TraceServer(tracer opentracing.Tracer, operationName string) endpoint.Middleware {
	return func(next endpoint.Endpoint) endpoint.Endpoint {
		return func(ctx context.Context, request interface{}) (interface{}, error) {
			serverSpan := opentracing.SpanFromContext(ctx)
			if serverSpan == nil {
				// All we can do is create a new root span.
				serverSpan = tracer.StartSpan(operationName)
			} else {
				serverSpan.SetOperationName(operationName)
			}
			defer serverSpan.Finish()
			otext.SpanKindRPCServer.Set(serverSpan)
			ctx = opentracing.ContextWithSpan(ctx, serverSpan)
			return next(ctx, request)
		}
	}
}

 

자 이제 실제로 Span을 생성하는 부분을 보겠습니다. 여러 가지의 하나의 요청에서 여러 span을 보내는 것을 예제로 보겠습니다. MakeUserGetEndPoint 부분인데요 보시면 var span을 만들고 이것을 핸들링하는 것을 볼 수 있는데요. 차례대로 설명해보겠습니다. span에 태그를 붙여주고 이 함수가 끝날 때 span이 종료되도록 defer span.Finish를 실행해줍니다.  

// MakeUserGetEndpoint returns an endpoint via the given service.
func MakeUserGetEndpoint(s Service) endpoint.Endpoint {
	return func(ctx context.Context, request interface{}) (response interface{}, err error) {
		var span stdopentracing.Span
		span, ctx = stdopentracing.StartSpanFromContext(ctx, "get users")
		span.SetTag("service", "user")
		defer span.Finish()

그리곤 userspan 변수를 만들고 StartSpan("OperationName",부모 span정보)를 넣어주고 s.GetUsers(req.ID)를 실행하고 바로 userspan.Finish()를 실행해 span을 닫아줍니다. 그러면 s.GetUsers(req.ID)Latency를 측정해 Jaeger-agent에 전달하겠죠? 그러고 나서 다음 로직인 DB 관련된 곳의 Latency를 측정하는 것을 보겠습니다.

		req := request.(GetRequest)

		userspan := stdopentracing.StartSpan("users from db", stdopentracing.ChildOf(span.Context()))
		usrs, err := s.GetUsers(req.ID)
		userspan.Finish()
		if req.ID == "" {
			return EmbedStruct{usersResponse{Users: usrs}}, err
		}
		if len(usrs) == 0 {
			if req.Attr == "addresses" {
				return EmbedStruct{addressesResponse{Addresses: make([]users.Address, 0)}}, err
			}
			if req.Attr == "cards" {
				return EmbedStruct{cardsResponse{Cards: make([]users.Card, 0)}}, err
			}
			return users.User{}, err
		}

여기서는 attrspan을 만들어 db.GetUserAttributes(&user)이 메서드의 span데이터를 측정하는 것인데요 아래와 같이 설정을 해주면 

		user := usrs[0]
		attrspan := stdopentracing.StartSpan("attributes from db", stdopentracing.ChildOf(span.Context()))
		db.GetUserAttributes(&user)
		attrspan.Finish()
		if req.Attr == "addresses" {
			return EmbedStruct{addressesResponse{Addresses: user.Addresses}}, err
		}
		if req.Attr == "cards" {
			return EmbedStruct{cardsResponse{Cards: user.Cards}}, err
		}
		return user, err
	}
}

"GET /customers" HTTP 요청이 실행되면 parent Span과 더불어 두 개의 child Span이 생성되게 되는 것입니다! 그러면 Parent Span은 어떻게 되는 걸까요? Parent Span은 가장 최초에 생성된 span 변수이고 이 모든 함수가 끝날 때 span.Finish 되도록 defer span.Finish()를 해두었습니다.

자, 그런데 이 서비스로 요청이 오긴 했는데 다른 jaeger instrumentation이 적용된 서비스에서 왔다면 그 서비스가 root span이 될 것인데요 어떻게 알 수 있을까요? 방법은 HTTPHandler를 만들 때 option으로 이 requestroot span이 있는지 알 수 있는 처리를 해주어야 합니다. 예제는 아래와 같습니다.

func MakeHTTPHandler(e Endpoints, logger log.Logger, tracer stdopentracing.Tracer) *mux.Router {
	r := mux.NewRouter().StrictSlash(false)
	options := []httptransport.ServerOption{
		httptransport.ServerErrorLogger(logger),
		httptransport.ServerErrorEncoder(encodeError),
	}


	r.Methods("GET").Path("/login").Handler(httptransport.NewServer(
		e.LoginEndpoint,
		decodeLoginRequest,
		encodeResponse,
		append(options, httptransport.ServerBefore(opentracing.HTTPToContext(tracer, "GET /login", logger)))...,
	))

여기서 보시면 opentracing.HTTPToContext(tracer, OPERATIONNAME, logger) 이 부분인데요 위의 코드를 보시면 GET/login 에 요청이 왔을 때 HTTP header root span정보가 없다면 이 HTTP 요청이 root span이 되도록 해주는 메서드입니다.

코드를 한번 보겠습니다.

// HTTPToContext returns an http RequestFunc that tries to join with an
// OpenTracing trace found in `req` and starts a new Span called
// `operationName` accordingly. If no trace could be found in `req`, the Span
// will be a trace root. The Span is incorporated in the returned Context and
// can be retrieved with opentracing.SpanFromContext(ctx).
func HTTPToContext(tracer opentracing.Tracer, operationName string, logger log.Logger) kithttp.RequestFunc {
	return func(ctx context.Context, req *http.Request) context.Context {
		// Try to join to a trace propagated in `req`.
		var span opentracing.Span
		wireContext, err := tracer.Extract(
			opentracing.HTTPHeaders,
			opentracing.HTTPHeadersCarrier(req.Header),
		)
		if err != nil && err != opentracing.ErrSpanContextNotFound {
			logger.Log("err", err)
		}

		span = tracer.StartSpan(operationName, ext.RPCServerOption(wireContext))
		ext.HTTPMethod.Set(span, req.Method)
		ext.HTTPUrl.Set(span, req.URL.String())
		return opentracing.ContextWithSpan(ctx, span)
	}
}

이렇게 하면 모든 trace 정보를 얻기 위한 준비가 완료됩니다. 설명을 하다 보니 상당히 복잡해 보이는데요.. 간략하게 정리를 해보겠습니다.

 

1. tracer 생성

2. HTTPToContext를 통해 request에 상위의 span이 있는지.. 즉 상위 trace 정보가 있는지 확인

3. TraceServer를 통해 요청되는 http 요청의 최상위 정의

4. Latency를 얻고자 하는 부분에 span을 만들고 특정 부분의 함수가 끝나는 부분에 span.finish() 실행

 

그리고 jaeger-client로 instrumentation 작업이 완료한 뒤 실제로 jaeger-agent로 span 데이터를 보내기 위해서는 jaeger-agent의 정보를 넣어줘야겠죠? 몇 가지 환경 변수 설정이 필요합니다.

환경변수명
JAEGER_SERVICE_NAME The service name.
JAEGER_AGENT_HOST The hostname for communicating with agent via UDP (default localhost).
JAEGER_AGENT_PORT The port for communicating with agent via UDP (default 6831).
JAEGER_SAMPLER_TYPE
The sampler type: remote, const, probabilistic, ratelimiting (default remote). See also https://www.jaegertracing.io/docs/latest/sampling/.
JAEGER_SAMPLER_PARAM The sampler parameter (number).
JAEGER_SAMPLER_MANAGER_HOST_PORT (deprecated) The HTTP endpoint when using the remote sampler.
JAEGER_SAMPLING_ENDPOINT
The URL for the sampling configuration server when using sampler type remote (default http://127.0.0.1:5778/sampling).
JAEGER_TAGS A comma separated list of name=value tracer-level tags, which get added to all reported spans. The value can also refer to an environment variable using the format ${envVarName:defaultValue}.

이렇게 설정하면 끝!

 

이상입니다. 혹시 궁금하신 부분이나 이해가 안 가시는 부분이 있으시면 댓글을 한번 달아주세요. 여유가 있다면 답글을 달도록 하겠습니다. 저도 포스팅을 작성하면서 다시 한번 정리할 수 있는 기회였습니다.

 

MSA구조를 tracing 하고 서비스 운용에 많은 도움이 되길 바라겠습니다.