๋ชจ๋๋ฆฌ์ ํ๊ฒฝ์์ ํธ์ถ์ด ํ ๋ฉ์ด๋ฆฌ๋ผ ๋น๊ต์ ์ง๊ด์ ์ด๋ค.
๋ก๊ทธ ๋ช ์ค๋ง ๋ด๋ โ์ด๋์ ๋ฌด์จ ์ผ์ด ๋ฌ๋์งโ ๊ฐ์ด ์จ๋ค.
ํ์ง๋ง ์๋น์ค๊ฐ ๋๋๋ ์๊ฐ, ๋ก๊ทธ๋ ์์ด๊ณ ํ์๋ผ์ธ์ ํํธ๋ฌ์ง๋ค.
โ200 OKโ๋ง ๋ณด๊ณ ๋ ํ๋ฆ ์ ์ฒด๋ฅผ ์ ๋ขฐํ ์ ์๋ ์ํฉ์ด ๋ฐ์ํ๋ค.
๊ทธ๋์ ์ค๊ณ ๋ชฉํ๋ฅผ ์ด๋ ๊ฒ ์ก์๋ค:
์ด๋ฒ ๊ธ์์๋ ์ด๋ฐ ๋ฐฐ๊ฒฝ ์์์, ์ค์ ๋ก ์ด๋ป๊ฒ ํ ์คํธํ๊ณ ๊ฒ์ฆํ๋์ง๋ฅผ ์ค์ฌ์ผ๋ก ํ์ด๊ฐ๋ ค ํ๋ค.
์ด๊ธฐ์ ์์ฒญ๋ง๋ค UUID ํ๋๋ฅผ ๋ถ์๋ค. ๋ชจ๋๋ฆฌ์์์ ์ถฉ๋ถํ๋ค๊ณ ์๊ฐํ๋ค.
ํ์ง๋ง MSA์์ ์์ฒญ์ด A โ B โ C
๋ก ํ๋ฌ๊ฐ๋ค.
์๋น์ค ๊ฒฝ๊ณ๋ฅผ ๋์ ๋๋ง๋ค UUID๊ฐ ์๋ก ์๊ธฐ๋ฉด, ํ ๋ฒ์ ์ฌ์ฉ์ ์์ฒญ์ ํ๋์ ํ์๋ผ์ธ์ผ๋ก ๋ณผ ์ ์๋ค.
์ฆ, MSA์์ ์๋น์ค ๊ฒฝ๊ณ๋ฅผ ๋๋ ์๊ฐ, ๋จ์ผ ID๋ง์ผ๋ก ์ ์ฒด ํธ์ถ ํธ๋ฆฌ ์๊ฐํ๋ฅผ ๋ง๋ค ์ ์๋ค๋ ๊ฒ์ด๋ค.
๊ทธ๋์ ์๋ UUID๋ ๋ฒ๋ฆฌ๊ณ , Micrometer Tracing
์ ๋์
ํด traceId/spanId๋ฅผ ์๋ ์์ฑยท์ ํํ๋๋ก ํ๋ค.
์์ :
[traceId: AAA]
โโโ spanId: A1 (Gateway)
โ โโโ spanId: B1 (Service A: Controller)
โ โ โโโ spanId: C1 (Service A: DB)
โ โโโ spanId: B2 (Service B: External API)
โโโ spanId: A2 (Service C)
โ
`traceId`: AAA "์์ฒญ ์ ์ฒด ์๋ณ์"
โ
๊ฐ๊ฐ์ `spanId`: "์ค์ ์์
์ ๊ตฌ์ฒด์ ์์น"
โ ํ๋์ traceId ์์๋ ์ฌ๋ฌ spanId๊ฐ ์๊ณ , ์ด๋ค์ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ์ฐ๊ฒฐ๋จ (parent/child)
๋ํ MdcKeys ํค๋ enum
๋ง๊ณ public final class MdcKeys๋ก ๋ฌธ์์ด ์์
๋ก ๊ด๋ฆฌํ์ฌ ๋ณ๋ ๋ถ๋ฆฌํด์ฃผ๊ณ , ํํฐ์์ ๊ฐ์ ธ๋ค๊ฐ ์ฌ์ฉํ์๋ค.
โก๏ธ ์ฅ์ : ๋ก๊ทธ๋ฐฑ ํจํด%X{}
๋ ๋ฌธ์์ด ํค๋ง ๋ฐ๊ธฐ๋ ํ๊ณ , ํ ๊ตฐ๋ฐ๋ง ๊ณ ์น๋ฉด ์ ์ญ ๋ฐ์ ๊ฐ๋ฅ
MDC
๋ ๋ด๋ถ์ ์ผ๋ก๋ ThreadLocal<Map<String, String>>
๊ตฌ์กฐ๋ก ๋์ํ๋ค.
๋ง์ง๋ง์ผ๋ก MDC.put()
์ผ๋ก ์ฃผ์
ํด์ฃผ๊ณ , ์์ฒญ์ด ๋๋ ์์ ์์ ThreadLocal ๊ฐ์ ๋ช
์์ ์ผ๋ก ๋น์์ค์ผ ๋ค์ ์์ฒญ์์ ์๋ชป๋ ๊ฐ์ด ์์ด๋ ๋ฒ๊ทธ + ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ฐฉ์งํ๊ธฐ์ํด MDC.clear()
๋ก ๋ง๋ฌด๋ฆฌํ์๋ค.
๊ทธ๋์ ์๋ traceId ๊ด๋ฆฌ์์ ํ ๊ฑธ์ ๋ ๋์๊ฐ, ์๋น์ค ๊ฐ ํธ์ถ ํ๋ฆ ์ถ์ ๊ณผ ์๊ฐํ๊น์ง ๊ณ ๋ คํด
Micrometer Tracing + OpenTelemetry
๋ฅผ ๋์ ํ๋ค.
์ฒ์์ ์ ํ๋ฆฌ์ผ์ด์
์์ Tempo ๊ฐ์ ๋ฐฑ์๋๋ก ์ง์ ์ ์ก์ ๊ณ ๋ฏผํ๋ค.
๊ณง๋ฐ๋ก ๋งํ๋ค. ๋ฐฑ์๋๋ฅผ ๋ฐ๊พธ๊ฑฐ๋, ์ฌ๋ฌ ๊ตฐ๋ฐ๋ก ๋์์ ๋ณด๋ด๊ฑฐ๋, ๋คํธ์ํฌ ์ด์์ ๋ฐฉ์ด์ ์ผ๋ก ๋์ํ๊ธฐ๊ฐ ๋ง๋ง์น ์์๋ค.
๊ทธ๋์ Exporter๋ฅผ ๋ฐ๋ก ๋ถ์ด์ง ์๊ณ , ์ค๊ฐ์ OpenTelemetry Collector๋ฅผ ์ธ์ ๋ค.
๋ฐฑ์๋๋ฅผ ๊ฐ์๋ผ์ฐ๊ฑฐ๋ ์ ์ก ์ ์ฑ ์ ์กฐ์ ํ ๋ ์ฑ ์์ ์ด ํ์ ์๊ฒ ๋ง๋๋ ๊ฒ ํฌ์ธํธ์๋ค.
App โ OTLP(4317/4318) โ Collector โ Tempo (or Zipkin, Jaeger)
Collector๋ฅผ ์ฐ๋ ์ ํ๋ฆฌ์ผ์ด์
์ โํ๋ฆ ๋ง๋ค๊ธฐโ์๋ง ์ง์คํ ์ ์์๊ณ ,
๋ฐฑ์๋๋ฅผ ๊ฐ์๋ผ์ฐ๊ฑฐ๋ ์ ์ก ์ ์ฑ
์ ๋ฐ๊ฟ ๋๋ ์ฑ ์์ ์์ด Collector ์ค์ ๋ง ๋ณ๊ฒฝํ๋ฉด ๋๋ค.
์ถ๊ฐ๋ก, Collector๋ ๋์์ ์ฌ๋ฌ Exporter๋ก ์ ์กํ ์ ์์ด ์ด์ ํ๊ฒฝ์์๋
Tempo + Jaeger ๊ฐ์ ๋ฉํฐ ์ ์ก๋ ๊ฐ๋ฅํ๋ค.
์ฆ, Collector๋ ๋จ์ ์ค๊ณ๊ธฐ๊ฐ ์๋๋ผ ํ์ฅ์ฑ๊ณผ ์ ์ฐ์ฑ์ ๋ณด์ฅํ๋ ์ค์ ํ๋ธ์๋ค.
์ฒ์์๋ ๋จ์ผ ์ ํ๋ฆฌ์ผ์ด์ ๋ด๋ถ์์ AโB ํธ์ถ์ ํ๋ด ๋ด๋ฉฐ ํ ์คํธํ๋ค.
๐ ํด๋ ๊ตฌ์กฐ (๋จ์ผ ์ฑ ์์์ A์ B๋ฅผ ๋๋ ํํ)
tracing/
โโ client/
โ โโ BServiceClient
โโ controller/
โ โโ AController
โ โโ BController
โโ service/
โ โโ AService
config/
โโ tracing/
โโ TracingWebClientConfig
โก๏ธ ํ๋์ ์ ํ๋ฆฌ์ผ์ด์ ์์ด์ง๋ง, ํจํค์ง๋ฅผ ๋ถ๋ฆฌํด A์ B ์๋น์ค์ฒ๋ผ ๊ตฌ์ฑํ๋ค.
๐ ํธ์ถ ํ๋ฆ (์์ฒญ์ด ์ค์ ๋ก ํ๋ฌ๊ฐ๋ ๊ฒฝ๋ก)
GET /a/call-b
โ AController
โ AService
โ BServiceClient (WebClient)
โ GET /b/hello
โ BController
๋๋ถ์ Postman ํธ์ถ๋ง์ผ๋ก๋ ๋ก๊ทธ + Tempo ์๊ฐํ๋ฅผ ๋์์ ํ์ธํ ์ ์์๋ค.
ํ์ง๋ง ๊ณง ํ๊ณ๊ฐ ๋ณด์๋ค.
โ ๊ฐ์ ์ ํ๋ฆฌ์ผ์ด์
๋ด๋ถ์์๋ง ํธ์ถ์ ํ๋ด ๋ด๋ค ๋ณด๋, ์ค์ MSA์ฒ๋ผ ์๋น์ค ๊ฒฝ๊ณ๋ฅผ ๋๋ ํ๋ฆ์ ์จ์ ํ ์ฆ๋ช
ํ๊ธด ์ด๋ ค์ ๋ค.
๋ ๋ช ํํ ํ ์คํธ๋ฅผ ์ํด ์ค์ MSA์ฒ๋ผ ์๋น์ค๋ฅผ ๋ถ๋ฆฌํ ๊ตฌ์กฐ์์ ๋ณด๋ ๊ฒ์ด ๋ ์ ํฉํ๋ค๊ณ ํ๋จํ๋ค.
๐๐ป ๊ทธ๋์ ํธ์ถ ๊ตฌ์กฐ๋ ๊ทธ๋๋ก ๋๊ณ , 8081(logsentry-api)์ 8082(service-b) ๋ ์๋น์ค๋ก ๋๋์ด ํ ์คํธ๋ฅผ ์งํํ๋ค.
[Before: ๋จ์ผ ์ ํ๋ฆฌ์ผ์ด์
]
logsentry-api (8081)
โโ AController
โโ AService
โโ BServiceClient
โโ BController
(TraceId/SpanId ๋์ผ JVM ๋ด ์ ํ)
----------------------------
[After: ์๋น์ค ๋ถ๋ฆฌ]
Service-A (8081, logsentry-api)
โโ /a/call-b
โโ WebClient โ Service-B ํธ์ถ
Service-B (8082, service-b)
โโ /b/hello
๋จผ์ ๊ธฐ์กด service-a(logsentry-api
)์์ service-b๋ฅผ ํธ์ถํ ์ ์๋๋ก ๊ตฌ์กฐ๋ฅผ ๋ฐ๊ฟจ๋ค.
๋๋ ๋ชจ๋ ์๋น์ค๋ฅผ Docker ํ๊ฒฝ์์ ์คํํ๊ธฐ ๋๋ฌธ์, service-b ์ปจํ
์ด๋๋ฅผ ๋จผ์ ์ถ๊ฐํ์๋ค.
1. service-b ๋์ปค ์ปจํ ์ด๋ ์ถ๊ฐ
# docker-compose.yml
service-b:
build:
context: ./service-b
dockerfile: Dockerfile
container_name: service-b
ports:
- "8082:8082"
environment:
- SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE}
networks:
- logsentry
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8082/actuator/health"]
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
2. service-a โ service-b ํธ์ถ ๊ตฌ์กฐ ๋ณ๊ฒฝ ๋ฐ ํ๊ฒฝ ์ค์ ์ถ๊ฐ
application-prod.yml
b:
base-url: ${B_BASE_URL:http://localhost:8082}
BProps
(ํ๊ฒฝ๋ณ์ ์ฃผ์
)
@ConfigurationProperties(prefix = "b")
public record BProps(String baseUrl) {}
TracingWebClientConfig
@Configuration
@RequiredArgsConstructor
@Slf4j
public class TracingWebClientConfig {
private final BProps props;
private final Tracer tracer;
@Bean
WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl(props.baseUrl())
.filter((request, next) -> {
var context = tracer.currentTraceContext().context();
if (context != null) {
log.info("[Tracing] traceId={}, spanId={}", context.traceId(), context.spanId());
}
return next.exchange(request);
})
.build();
}
}
BServiceClient
@Component
@RequiredArgsConstructor
public class BServiceClient {
private final WebClient webClient;
@Observed(name = "bServiceClient.callB", contextualName = "WebClient โ B")
public Mono<String> callB() {
return webClient.get() // http://localhost:8081/b/hello์์ ์๋ ๊ฒฝ๋ก๋ก ๋ณ๊ฒฝ โ service-b ํธ์ถ
.uri("/b/hello")
.retrieve()
.bodyToMono(String.class);
}
}
โ service-b ์๋น์ค์๋ BController์ ํจ๊ป, Tempo/Otel ์ ์ก ๊ตฌ์กฐ yml๊ณผ Dockerfile ์ ๋์ ์ต์ ์ค์ ๋ง ๋์๋ค.
BController
@RestController
@RequestMapping("/b")
public class BController {
private static final Logger log = LoggerFactory.getLogger(BController.class);
@Observed(name = "bController.hello", contextualName = "HTTP /b/hello")
@GetMapping("/hello")
public Mono<String> hello() {
log.info("[B] /b/hello ์์ฒญ ์ฒ๋ฆฌ");
return Mono.just("Hello from B!");
}
}
๋ ์๋น์ค๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด spring.application.name
์ ๊ฐ๊ฐ ๋ค๋ฅด๊ฒ ์ค์ ํ๋ค.
logsentry-api
service-b
Tempo ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ด ์ง์ ํ๋ค.
{resource.service.name=~"logsentry-api|service-b"}
์ด ์ํ์์ /a/call-b
์์ฒญ์ ๋ณด๋ด๋ฉด, Tempo๋ ์๋์ฒ๋ผ span ํธ๋ฆฌ๋ฅผ ๋ณด์ฌ์ค๋ค.
๊ทธ๋ฆฌ๊ณ , ์๋น์ค๋ณ๋ก ์์ด ๋ค๋ฅด๊ฒ ํ์๋์ด AโB ํธ์ถ ํ๋ฆ์ ํ๋์ ๊ตฌ๋ถํ ์ ์์๋ค.
logsentry-api (์๋น์ค A)
http get /a/call-b
โ ์ต์ด ์์ฒญ, root span/b/hello
ํธ์ถ ์ ์๋ก์ด child span ์์ฑservice-b (์๋น์ค B)
http get /b/hello
โ service-a์ WebClient ํธ์ถ์ ์ฒ๋ฆฌ/a/call-b
(service-a)/b/hello
(service-b)โ traceId๋ ๋์ผํ๊ณ , spanId๋ง ์๋ก ์์ฑ๋์ด ๋ถ๋ชจ-์์ ๊ด๊ณ๋ก ์ฐ๊ฒฐ๋๋ค.
๐ ๊ฒฐ๊ณผ์ ์ผ๋ก,
/a/call-b
(logsentry-api) ์์ฒญ์ด traceId๋ฅผ ์ ์งํ ์ฑ/b/hello
(service-b)๊น์ง ์ด์ด์ง๋ ํ๋ฆ์ ์๊ฐ์ ์ผ๋ก ์ฆ๋ช ํ ์ ์์๋ค.
๋จ์ผ ์ ํ๋ฆฌ์ผ์ด์
๋ด์์๋ traceId ์ ํ ๊ฒ์ฆ์ ๊ฐ๋ฅํ๋ค.
ํ์ง๋ง ์๋น์ค๋ฅผ ์ค์ ๋ก ๋ ๊ฐ ๋ถ๋ฆฌํ๋, AโB ํธ์ถ ํ๋ฆ์ด ํจ์ฌ ๋ช
ํํ๊ฒ ๋๋ฌ๋ฌ๋ค.
์ด๋ฒ ๊ธ์ ํฌ์ธํธ๋ ์์ฒญ์ด ์ด๋์ ์์ํด ์ด๋๊น์ง ๊ฐ๋์ง, ๊ทธ ๊ธธ์ ๋๊ณผ ํ๋ฉด์ผ๋ก ๋์์ ๋ฐ๋ผ๊ฐ ์ ์๊ฒ ๋ง๋๋ ์ค๊ณ์๋ค.
์ด์ โ200 OK๋ฉด ๋โ์ด ์๋๋ผ, โํ๋ฆ์ ํ์ธํ๋ ์ต๊ด" ์ด ์ ๋ง ์ค์ํ๋ค๊ณ ์ฒด๊ฐํ๋ค.
_
๐ ์ฉ์ด ์ ๋ฆฌ
Micrometer Tracing
: Spring Boot 3.x์ Sleuth ํ์ ๋ถ์ฐ ์ถ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, traceId/spanId ์๋ ์์ฑยท์ ํ
OpenTelemetry (OTEL)
: CNCF ํ์ค ๊ด์ธก ํ๋ ์์ํฌ, ๋ก๊ทธยท๋ฉํธ๋ฆญยทํธ๋ ์ด์ค๋ฅผ ํตํฉ ๊ด๋ฆฌ
OTLP Exporter
: ํธ๋ ์ด์ค ๋ฐ์ดํฐ๋ฅผ OTLP ํ๋กํ ์ฝ(HTTP/gRPC)๋ก Collector์ ์ ๋ฌํ๋ ๋ชจ๋
Collector
: Exporter์์ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ง๊ณยท๋ณํยท๋ผ์ฐํ ํด Zipkin, Tempo ๋ฑ ๋ฐฑ์๋๋ก ์ ์ก