[Zipkin] 사용법 가이드 - MSA 요청추적 (springboot/ sleuth/ grpc/ http)

tracing이 필요한 이유

Monolithic 시스템의 경우 클라이언트의 요청을 받으면 하나의 스레드에서 모든 요청을 실행하므로 로그를 확인하기 쉽다는 장점이 있습니다.
그에 반해 MSA의 경우에는 각 서비스의 복잡도가 낮아지고 역할 분담이 용이하지만 클라이언트의 요청을 받았을 때 여러 개의 마이크로 서비스 간에 통신이 발생해 로그를 확인하기 어려운 문제가 있습니다.
이를 해결하기 위한 어플리케이션 간 분산 추적을 위한 표준이 OpenTracing이며, 대표적은 구현체는 zipkin과 jaeger가 있습니다.

클라이언트가 서버로 호출한 하나의 호출을 Trace라고 했을 때, 서비스 컴포넌트간의 호출을 Span이라고 한다.각 서비스 컴포넌트들은 하나의 클라이언트 호출을 추적하기 위해서 같은 Trace Id를 사용하고,
각 서비스간의 호출은 각각 다른 SpanId를 사용합니다. 이렇게 함으로써 전체 트렌젝션 시간을 Trace로 추적이 가능하고, 각 서비스별 구간 시간은 Span으로 추적할 수 있습니다.

개발환경

  • spring-boot 2.5.4
  • spring-cloud-sleuth
  • spring-cloud-sleuth-zipkin
  • grpc, http

예제코드는 아래 github에서 확인할수 있습니다.
https://github.com/HyeonGuJ/spring-boot-sleuth-zipkin-grpc-sample

적용하기

촤상위 build.gradle에 의존성 추가

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buildscript {
...
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.16'
}
}
subprojects {
apply plugin: 'com.google.protobuf'

dependencies {
...
//zipkin
implementation 'org.springframework.cloud:spring-cloud-sleuth-zipkin'
implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'
...
}
}

각 서비스의 application.yml에 zipkin collector 정보 추가

본 예제에서는 zipkin collector server를 9411 port로 설정했다고 가정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#application.yml
server:
port: 8082

spring:
application:
name: privateService
sleuth:
sampler:
probability: 1.0
zipkin:
base-url: http://localhost:9411
service:
name: privateService

grpc 서비스들 셋팅하기

spring-cloud-sleuth 의 문서를 기반으로 구현했습니다.
https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/integrations.html#sleuth-rpc-grpc-variant1-integration

grpc server

build.gradle에 의존성을 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
dependencies {
implementation 'io.github.lognet:grpc-spring-boot-starter:4.5.5'
implementation 'io.zipkin.brave:brave-instrumentation-grpc'
}

sourceSets {
src {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.12.0"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.39.0"
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}

application.yml 에 grpc.port를 설정해줍니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
grpc:
port: 8084

spring:
application:
name: grpcServer
sleuth:
sampler:
probability: 1.0
zipkin:
base-url: http://localhost:9411
service:
name: grpcServer

HelloService.proto 를 미리 작성했습니다.
build 후 생성된 소스코드를 사용할수 있는 상태임을 가정합니다.

요청시 처리해야할 로직을 구현하고 @GRpcService를 통해 노출 할 수 있습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//HelloService.java
@Slf4j
@GRpcService
public class HelloService extends HelloServiceGrpc.HelloServiceImplBase {

@Override
public void sayHello(HelloRequest request,
StreamObserver<HelloResponse> responseObserver) {

log.info("[S4] called");

//get message
String message = new StringBuilder().append("hello ")
.append(request.getName())
.toString();


//build response
HelloResponse response = HelloResponse.newBuilder()
.setMessage(message)
.build();

//send message and complete.
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}

grpc client

서버측과 마찬가지로 build.gradle 을 준비합니다(동일하게 셋팅, 예제 생략)

client에서는 server측과 연동하기위해 channel을 열고, stub을 생성후 request를 전송해야합니다.

channel을 열고 stub을 생성성하는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//GrpcClientConfig.java

@Configuration
public class GrpcClientConfig {
private final SpringAwareManagedChannelBuilder springAwareManagedChannelBuilder;

public GrpcClientConfig(SpringAwareManagedChannelBuilder springAwareManagedChannelBuilder) {
this.springAwareManagedChannelBuilder = springAwareManagedChannelBuilder;
}

@Bean
public HelloServiceGrpc.HelloServiceBlockingStub helloServiceBlockingStub() {
ManagedChannel channel = this.springAwareManagedChannelBuilder.forAddress("localhost", 8084).usePlaintext().build();
return HelloServiceGrpc.newBlockingStub(channel);
}
}

주의
ManagedChannel 을 생성할때 반드시 SpringAwareManagedChannelBuilder로 해야합니다.
SpringAwareManagedChannelBuilder로 생성해야만 Spring에서 grpc 를 inject 할 수 있습니다.
그렇지 않으면 trace id가 전달되지 않습니다. (경험담)
https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/integrations.html#sleuth-rpc-grpc-variant1-client-integration

test

구조

log1 - public
외부 요청을 받는 public 서버가있고, 내부적으로 private, grpcClient를 호출
grpcClient는 http를통해 요청을받고, grpc로 grpcServer와 통신합니다
이 서버들은 모두 zipkin collector server로 데이터를 전송합니다

log2 - public
(출처 zipkin 공식 홈페이지 : https://zipkin.io/pages/architecture.html)

자세한 실행방법은 github - readme.md 를 참고하세요
https://github.com/HyeonGuJ/spring-boot-sleuth-zipkin-grpc-sample

logs


로그상에 trace id가 모두 동일하게 잡히고, span id 는 다르게 보이는것을 확인 할 수 있습니다.

zipkin 에서 확인하기

필자는 docker를 이용해 zipkin을 실행했으며 별도의 설정을 하지않아도 dashboard를 제공하기때문에 이를 통해 확인했습니다.

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

http://localhost:9411/zipkin/traces/{traceId}

success
위와같이 성공적으로 나오는 모습을 확인 할 수 있었다.
비동기 요청의 경우 아래와같이 동시에 실행되는것 또한 볼 수 있다

asnyc

Comments