
관측성을 살리려다 가용성을 잃는 트레이드오프, 비동기 로깅과 분산 추적의 충돌을 우아하게 타파하기
본 아티클은 로그를 표준 포맷으로 다듬는 과정에서 무의식적으로 적용한 동기 로깅이 대용량 분산 환경에서 어떻게 치명적인 가용성 장애로 번졌는지 실사례 분석을 통해 조명합니다. 비동기식 로그 처리 도입 시 발생하는 분산 추적 식별자(trace_id) 유실 제약사항을 극복하는 전략을 담고 있습니다. 30줄 미만의 경량 커스텀 클래스를 통해 요청 스레드의 블로킹 대기 현상을 근절하고 대량 트래픽 대피 경로를 완벽히 확보한 해결책을 제시합니다.
고트래픽을 처리하는 JVM 기반 MSA 아키텍처 환경에서 로그 유실과 가용성 확보 사이의 저울질을 고민하고 있는 플랫폼 엔지니어들에게 적극 추천합니다. 특히 비동기 로깅 프레임워크와 분산 추적 에이전트(OTel) 간의 스레드 전파 문제를 마주한 개발자에게 실전 가이드라인을 선사합니다.
고트래픽 서비스에서 로그 포맷 표준화를 달성하기 위해 비동기 로깅 대신 동기 로깅을 적용했습니다. 그러나 대량의 디버그 로그가 발생할 때 표준 출력(stdout) 버퍼가 가득 차면서 로그를 쓰던 요청 스레드들이 차례로 대기 상태에 빠졌고, 이로 인해 DB 커넥션 풀이 고갈되며 전체 서비스 요청이 30초 주기로 무더기 실패하는 장애가 발생했습니다.
AsyncAppender를 확장한 커스텀 'TraceAwareAsyncAppender'를 개발하여 문제를 해결했습니다. 기존의 스레드가 전환된 후 trace_id를 읽는 방식에서 탈피하여, 로그를 보내는 호출자(caller) 스레드 환경에서 미리 OpenTelemetry의 trace context를 캡처하여 이벤트 래퍼 객체에 저장한 뒤 비동기 큐에 집어넣는 방식을 구현했습니다.
부하 테스트 결과 초당 1만 5천 줄의 극단적 부하 상황에서도 p95 응답 지연 시간이 29ms로 매우 안정적으로 방어되었습니다. 대량 과부하 시 'neverBlock=true' 옵션에 따라 일부 로그 유실(약 65%)이 발생하더라도 요청 처리 스레드는 전혀 블로킹되지 않았으며, 출력된 로그의 trace_id 누락은 0%를 기록했습니다.
Trade-off
로그를 전송할 때마다 매번 TraceStampedEvent 객체를 신규 생성 및 할당해야 하므로 메모리와 JVM 가비지 컬렉션(GC) 성능에 미세한 영향을 미칩니다. 아울러 극심한 트래픽 증가 상태에서는 가용성 방어를 위해 'neverBlock=true' 설정을 활성화하여 일부 로그 유실을 동반하므로 일시적인 관측 데이터 공백 현상이 발생할 수 있습니다.
Logback 로깅 라이브러리에서 로그 작성을 동기식이 아닌 비동기식으로 처리하기 위해 지원하는 컴포넌트입니다. 로그 유휴 데이터를 버퍼 큐에 보관하고 별개의 데몬 스레드가 처리하여 애플리케이션의 대기 시간을 줄입니다.
애플리케이션 메트릭, 분산 추적 데이터 등을 기기 중립적으로 생성하고 내보낼 수 있는 개방형 표준 가용성 프레임워크입니다. 스레드 로컬에 관련 분산 추적 정보를 밀어 넣고 네트워크 상의 하위 호출 흐름을 지속적으로 이어갑니다.
개별 로그 이벤트에 요청 세션별 유니크한 진단 속성 값을 가미해 남길 수 있도록 로깅 라이브러리가 제공하는 로컬 맵 저장소입니다.




