SpringBoot3 WebFlux 관측 가능성 모범 사례¶
작성자: Liu Rui
Kotlin 리액티브 프로그래밍에서 Spring Boot 3 WebFlux 환경에서 Micrometer를 활용한 분산 추적 방법
분산 추적은 관측 가능성 소프트웨어 시스템에서 매우 유용한 도구입니다. 개발자는 애플리케이션 내부 및 애플리케이션 간의 다양한 상호작용이 언제, 어디서, 어떻게 발생하는지 파악할 수 있습니다. 또한 복잡한 소프트웨어 시스템을 더 쉽게 관측할 수 있게 해줍니다.
Spring Boot 3부터 Spring Boot에서 분산 추적을 위해 사용되던 기존 Spring Cloud Sleuth 솔루션이 새로운 Micrometer Tracing 라이브러리로 대체되었습니다.
이미 Micrometer에 익숙하실 수도 있습니다. 이전에 플랫폼 독립적인 메트릭을 제공하고 Prometheus와 같은 JVM 기반 마이크로서비스를 모니터링하는 기본 솔루션으로 사용되었기 때문입니다. 최신 제품은 플랫폼 독립적인 분산 추적 솔루션을 통해 Micrometer 생태계를 확장합니다. 이를 통해 개발자는 공통 API를 사용하여 애플리케이션을 계측하고, 다양한 형식으로 Jaeger, Zipkin 또는 OpenTelemetry와 같은 분산 추적 수집기로 내보낼 수 있습니다.
1. 마이크로서비스 설정¶
이제 간단한 Spring Boot 마이크로서비스를 생성해 보겠습니다. 이 서비스는 내부적으로 다른 타사 서비스를 조회하여 정보를 가져오는 리액티브 REST 엔드포인트를 제공합니다. 목표는 두 가지 작업에 대한 trace를 내보내는 것입니다.
다음 Spring Boot Initializr 프로젝트부터 시작하겠습니다. 이 프로젝트는 여기에서 찾을 수 있습니다. Kotlin Gradle DSL과 함께 제공되는 Spring Boot 3.0.1, Spring Web Reactive (WebFlux), Prometheus가 포함된 Spring Actuator가 포함되어 있습니다. 다음 코드는 주로 Kotlin을 사용하지만, Java를 사용해도 대부분의 메서드는 동일합니다.
Spring 초기화 템플릿: Webflux, Spring Actuator 및 Prometheus가 포함된 Spring Boot 3 Kotlin 템플릿
엔드포인트 정의¶
먼저 Spring WebClient를 사용하여 외부 API를 호출하는 테스트 엔드포인트가 있는 간단한 REST 컨트롤러 클래스를 추가합니다. Kotlin의 코루틴을 사용하기 위해 suspend 키워드를 사용하고 있습니다. 이를 통해 Spring WebFlux의 리액티브 스트림을 활용하면서 명령형 코드를 작성할 수 있습니다.
다음 예제에서는 Spring WebClient를 사용하여 외부 TODO-API를 호출합니다. 이 API는 TODO 항목을 JSON 문자열로 반환합니다. 또한 나중에 분산 추적 정보를 포함해야 하는 로그 메시지를 생성합니다.
@RestController
class Controller {
val log = LoggerFactory.getLogger(javaClass)
val webClient = WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
@GetMapping("/test")
suspend fun test(): String {
// simulate some complex calculation
delay(1.seconds)
log.info("test log with tracing info")
// make web client call to external API
val externalTodos = webClient.get()
.uri("/todos/1")
.retrieve()
.bodyToMono(String::class.java)
.awaitSingle()
return externalTodos
}
}
Micrometer tracing 추가¶
다음 단계에서는 Micrometer tracing 종속성을 build.gradle.kts 파일에 추가합니다. Micrometer는 다양한 분산 추적 형식과 공급업체를 지원하므로 종속성이 분리되어 있으며 필요한 것만 가져오면 됩니다. 모든 종속성을 동기화된 상태로 유지하기 위해 Micrometer Tracing BOM(bom 목록)을 사용합니다. 또한 Micrometer Tracing을 OpenTelemetry 형식(다른 형식도 사용 가능)으로 변환하기 위해 핵심 종속성과 브리지를 추가합니다.
implementation(platform("io.micrometer:micrometer-tracing-bom:1.0.0"))
implementation("io.micrometer:micrometer-tracing")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
또한 생성된 trace를 내보내기 위한 익스포터 종속성도 추가해야 합니다. 이 예제에서는 OpenTelemetry에서 유지 관리하고 Micrometer Tracing에서 지원하는 Zipkin 익스포터를 사용합니다.
구성¶
구성은 분산 추적을 설정하는 데 필수적인 단계이며, 구성 파일 application.yaml은 src/main/resources 디렉토리에 있습니다.
먼저 관리 설정에서 분산 추적을 활성화해야 합니다. 또한 분산 추적 샘플링 비율을 1(기본값은 0.1)로 설정하여 서비스가 수신하는 모든 호출에 대해 분산 추적을 생성합니다. 요청이 많은 프로덕션 시스템에서는 일부 호출 체인만 추적하려고 할 수 있습니다. 또한 Zipkin 익스포터가 분산 추적을 보낼 엔드포인트 URL을 정의할 수 있습니다. 마지막으로 분산 추적 및 spanId를 포함하도록 기본 로깅 패턴을 업데이트해야 합니다.
management:
tracing:
enabled: true
sampling.probability: 1.0
zipkin.tracing.endpoint: http://localhost:9411/api/v2/spans
logging.pattern.level: "trace_id=%mdc{traceId} span_id=%mdc{spanId} trace_flags=%mdc{traceFlags} %p"
2. 테스트¶
이제 서비스 설정이 완료되었으므로 실행할 수 있습니다. 애플리케이션을 시작하면 기본적으로 서버는 포트 8080에서 시작됩니다. 그런 다음 브라우저를 열어 생성한 엔드포인트 http://localhost:8080/test를 호출할 수 있습니다. 다음은 요청 응답 내용입니다.
엔드포인트 호출 시 생성된 실제 분산 추적을 확인하려면 수집하여 조회해야 합니다. 이 튜토리얼에서는 zipkin 익스포터를 사용하여 데이터를 Guance로 내보냅니다. 물론 Zipkin, Grafana Loki 또는 Datadog과 같은 다른 시스템을 사용할 수도 있습니다.
이제 Spring Boot 서비스의 엔드포인트를 다시 호출할 수 있습니다. 그런 다음 Guance에서 분산 추적을 검색하면 엔드포인트 요청의 분산 추적 정보를 찾을 수 있습니다.
3. 문제점¶
언뜻 보기에는 모든 것이 잘 작동하는 것 같습니다. 그러나 두 가지 문제가 있습니다.
일부 Issue 문제가 해결되었으며, 이 문제들은 Micrometer Tracing 문서에서 확인할 수 있습니다.
로그에 데이터 누락¶
애플리케이션 로그를 확인하면 엔드포인트 호출 시 생성된 로그 메시지를 볼 수 있습니다.
trace_id= span_id= trace_flags= INFO 43636 --- [DefaultExecutor] com.example.tracing.Controller : test log with tracing info
보시다시피 trace_id와 span_id가 설정되지 않았습니다. 이는 Micrometer Tracing이 리액티브 스트림에서 분산 추적 컨텍스트를 아직 제대로 처리하지 못하기 때문입니다. 또한 리액티브 스트림의 Kotlin 코루틴 래퍼가 분산 추적 컨텍스트를 숨깁니다. 따라서 분산 추적 정보를 얻기 위해 현재 리액티브 스트림의 컨텍스트를 연기해야 합니다. 실제로는 다음과 같이 보입니다.
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
log.info("test log with tracing info")
Mono.empty<String>()
}
}.awaitSingleOrNull()
더 재사용성을 높이기 위해 예제 코드를 별도의 함수로 추출할 수 있습니다.
@GetMapping("/test")
suspend fun test(): String {
// ...
observeCtx { log.info("test log with tracing info") }
// ...
}
suspend inline fun observeCtx(crossinline f: () -> Unit) {
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
f()
Mono.empty<Unit>()
}
}.awaitSingleOrNull()
}
이제 애플리케이션을 시작하고 엔드포인트를 호출하면 로그에서 trace_id를 볼 수 있습니다.
trace_id=6c0053eba01199f194f5f76ff8d61917 span_id=967d591266756905 trace_flags= INFO 45139 --- [DefaultExecutor] com.example.tracing.Controller : test log with tracing info
WebClient 호출이 trace 정보를 생성하지 않음¶
두 번째 문제는 Guance에서 trace를 확인하면 발견할 수 있습니다. 엔드포인트의 부모 분산 추적만 표시되고 WebClient 호출의 하위 span은 표시되지 않습니다. 이론적으로 Spring WebClient와 RestTemplate은 모두 Micrometer에 의해 자동으로 계측됩니다. 그러나 코드를 살펴보면 정적 빌더 메서드 WebClient를 사용하고 있음을 알 수 있습니다. WebClient에서 자동 분산 추적을 얻으려면 Spring 프레임워크에서 제공하는 빌더 bean을 사용해야 합니다. 이는 컨트롤러 클래스의 생성자를 통해 주입받을 수 있습니다.
@RestController
class Controller(
webClientBuilder: WebClient.Builder
) {
val webClient = webClientBuilder // use injected builder
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
// ...
}
WebClient의 span을 볼 수 있습니다. Micrometer Tracing은 또한 trace_id를 포함하는 헤더를 자동으로 추가합니다. 예를 들어, 분산 추적 기능이 있는 다른 마이크로서비스를 호출하면 ID를 가져와 Guance에 추가 정보를 보낼 수 있습니다.
4. 관측 가이드¶
Micrometer Tracing은 Spring에서 자동으로 많은 작업을 수행합니다. 그러나 때로는 분산 추적 범위에 특정 정보를 추가하거나 애플리케이션에서 수신 또는 발신 호출이 아닌 특정 부분을 관측하려는 경우가 있습니다.
span 태그 추가¶
사용자 정의 태그를 정의하고 현재 관측에 추가하여 분산 추적 데이터를 보강할 수 있습니다. 현재 분산 추적을 검색하려면 ObservationRegistry 클래스의 bean을 사용할 수 있습니다. 로깅 문제와 유사하게 올바른 컨텍스트를 얻으려면 래퍼 함수를 사용해야 합니다.
@GetMapping("/test")
suspend fun test(): String {
observeCtx {
val currentObservation = observationRegistry.currentObservation
currentObservation?.highCardinalityKeyValue("test_key", "test sample value")
}
// ...
}
이 코드를 추가하면 Guance에서 사용자 정의 태그와 그 값을 볼 수 있습니다.
사용자 정의 관측¶
Micrometer API를 사용하여 사용자 정의 관측(span)을 생성하는 것은 일반적으로 쉽습니다. 그러나 리액티브 스트림과 코루틴을 사용할 때는 컨텍스트 분산 추적에 도움이 필요합니다. 엔드포인트 핸들러 내에서 새 관측을 생성하면 별도의 분산 추적으로 간주됩니다. 코드를 재사용 가능하게 만들기 위해 새 관측점을 생성하는 간단한 래퍼 함수를 작성할 수 있습니다. 이는 trace_id를 사용하기 위해 이전에 만든 함수와 유사하게 작동합니다.
suspend fun runObserved(
name: String,
observationRegistry: ObservationRegistry,
f: suspend () -> Unit
) {
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
val observation = Observation.start(name, observationRegistry)
Mono.just(observation).flatMap {
mono { f() }
}.doOnError {
observation.error(it)
observation.stop()
}.doOnSuccess {
observation.stop()
}
}
}.awaitSingleOrNull()
}
이제 이 함수를 적용하여 delay 함수의 실행과 같은 모든 코드를 관측할 수 있습니다.
@GetMapping("/test")
suspend fun test(): String {
runObserved("delay", observationRegistry) {
delay(1.seconds)
}
// ....
}
5. 데이터베이스 분산 추적¶
일반적인 Spring Boot 애플리케이션은 실제 애플리케이션에서 데이터베이스에 연결하는 경우가 많습니다. 리액티브 기술 스택을 활용하려면 JDBC 대신 R2DBC API를 사용하는 것이 좋습니다.
Micrometer Tracing은 상당히 새로운 기술이므로 현재 사용 가능한 자동 추적이 없습니다. 그러나 Spring 팀은 자동 구성을 생성하기 위해 연구 중입니다. 실험적 저장소는 여기에서 찾을 수 있습니다.
현재 프로젝트에는 다음 종속성을 build.gradle.kts에 추가해야 합니다. 테스트를 위해 실제 데이터베이스 대신 H2 인메모리 데이터베이스를 사용합니다.
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")
// R2DBC micrometer auto tracing
implementation("org.springframework.experimental:r2dbc-micrometer-spring-boot:1.0.2")
Kotlin 코드에서는 코루틴을 지원하는 간단한 CRUD 저장소를 추가합니다. 다음과 같습니다.
@Table("todo")
data class ToDo(
@Id
val id: Long = 0,
val title: String,
)
interface ToDoRepository : CoroutineCrudRepository<ToDo, Long>
@RestController
class Controller(
val todoRepo: ToDoRepository,
// ...
) {
@GetMapping("/test")
suspend fun test(): String {
// ...
// save
val entry = ToDo(0,"Springboot3 + WebFlux + Kotlin ")
todoRepo.save(entry)
// Sample traced DB call
val dbtodos = todoRepo.findAll().toList()
// ...
return "${dbtodos.size} $externalTodos"
}
}
엔드포인트를 호출하면 span이 하나 더 추가됩니다. 새 span의 이름은 query이며, Spring Data R2DBC가 실행한 SQL 쿼리를 포함한 여러 태그를 포함합니다.
결론¶
Micrometer와 새로운 분산 추적 확장은 Spring Boot 3 이상 버전의 관측 가능성 기술 스택을 통합합니다. 다양한 회사와 기술 스택에서 사용하는 다양한 분산 추적 솔루션에 대한 훌륭한 추상화를 제공합니다. 따라서 개발자 작업을 간소화합니다.
Spring WebFlux의 리액티브 프로그래밍 측면에서는 특히 Kotlin의 경우 여전히 개선의 여지가 있습니다. Micrometer 팀은 Spring WebFlux에서 사용하는 리액티브 라이브러리인 Project Reactor 팀과 리액티브 기술 스택에서 Micrometer Tracing 사용을 간소화하기 위해 적극적으로 논의하고 있습니다.




