#Backend

로그가 서비스를 죽였다

로그가 서비스를 죽였다
01

Summary

로그 남기다 서버가 터졌다? 무신사가 30줄의 코드로 구한 서비스 가용성

관측성을 살리려다 가용성을 잃는 트레이드오프, 비동기 로깅과 분산 추적의 충돌을 우아하게 타파하기

본 아티클은 로그를 표준 포맷으로 다듬는 과정에서 무의식적으로 적용한 동기 로깅이 대용량 분산 환경에서 어떻게 치명적인 가용성 장애로 번졌는지 실사례 분석을 통해 조명합니다. 비동기식 로그 처리 도입 시 발생하는 분산 추적 식별자(trace_id) 유실 제약사항을 극복하는 전략을 담고 있습니다. 30줄 미만의 경량 커스텀 클래스를 통해 요청 스레드의 블로킹 대기 현상을 근절하고 대량 트래픽 대피 경로를 완벽히 확보한 해결책을 제시합니다.

  • 01표준 출력(stdout) 버퍼 포화가 직렬화 락 경합 및 DB 커넥션 고갈로 이어지는 연쇄 장애 메커니즘 분석
  • 02OpenTelemetry의 ThreadLocal 컨텍스트가 로깅 워커 스레드로 분기 시 무력화되는 기술적 원인 파악
  • 03호출자(caller) 스레드에서 trace_id 컨텍스트를 선제적으로 박제(Freeze)하는 'TraceAwareAsyncAppender' 구현
  • 04큐 임계점 도달 시 'neverBlock=true' 정책을 사용해 로그를 드롭함으로써 서비스 가용성을 최우선으로 보호
  • 05초당 1만 5천 건의 극한 테스트에서도 trace_id 누락율 0% 및 안정적인 응답 속도 유지 검증

RECOMMENDATION

고트래픽을 처리하는 JVM 기반 MSA 아키텍처 환경에서 로그 유실과 가용성 확보 사이의 저울질을 고민하고 있는 플랫폼 엔지니어들에게 적극 추천합니다. 특히 비동기 로깅 프레임워크와 분산 추적 에이전트(OTel) 간의 스레드 전파 문제를 마주한 개발자에게 실전 가이드라인을 선사합니다.

The Problem

고트래픽 서비스에서 로그 포맷 표준화를 달성하기 위해 비동기 로깅 대신 동기 로깅을 적용했습니다. 그러나 대량의 디버그 로그가 발생할 때 표준 출력(stdout) 버퍼가 가득 차면서 로그를 쓰던 요청 스레드들이 차례로 대기 상태에 빠졌고, 이로 인해 DB 커넥션 풀이 고갈되며 전체 서비스 요청이 30초 주기로 무더기 실패하는 장애가 발생했습니다.

The Solution

AsyncAppender를 확장한 커스텀 'TraceAwareAsyncAppender'를 개발하여 문제를 해결했습니다. 기존의 스레드가 전환된 후 trace_id를 읽는 방식에서 탈피하여, 로그를 보내는 호출자(caller) 스레드 환경에서 미리 OpenTelemetry의 trace context를 캡처하여 이벤트 래퍼 객체에 저장한 뒤 비동기 큐에 집어넣는 방식을 구현했습니다.

The Result

부하 테스트 결과 초당 1만 5천 줄의 극단적 부하 상황에서도 p95 응답 지연 시간이 29ms로 매우 안정적으로 방어되었습니다. 대량 과부하 시 'neverBlock=true' 옵션에 따라 일부 로그 유실(약 65%)이 발생하더라도 요청 처리 스레드는 전혀 블로킹되지 않았으며, 출력된 로그의 trace_id 누락은 0%를 기록했습니다.

Trade-off

로그를 전송할 때마다 매번 TraceStampedEvent 객체를 신규 생성 및 할당해야 하므로 메모리와 JVM 가비지 컬렉션(GC) 성능에 미세한 영향을 미칩니다. 아울러 극심한 트래픽 증가 상태에서는 가용성 방어를 위해 'neverBlock=true' 설정을 활성화하여 일부 로그 유실을 동반하므로 일시적인 관측 데이터 공백 현상이 발생할 수 있습니다.

03

Key Concepts

Concept · 01

AsyncAppender

Logback 로깅 라이브러리에서 로그 작성을 동기식이 아닌 비동기식으로 처리하기 위해 지원하는 컴포넌트입니다. 로그 유휴 데이터를 버퍼 큐에 보관하고 별개의 데몬 스레드가 처리하여 애플리케이션의 대기 시간을 줄입니다.

  • 메인 비즈니스 요청 스레드가 로그 파일/표준 출력 완료 대기로 인해 차단(Blocking)되는 것을 격리시킵니다.
  • 큐 임계점을 설정하여 특정 용량 도달 시 동기 로깅으로 즉시 복귀할지(neverBlock=false) 또는 로그를 버리고 성능을 지킬지(neverBlock=true) 조율합니다.
Concept · 02

OpenTelemetry

애플리케이션 메트릭, 분산 추적 데이터 등을 기기 중립적으로 생성하고 내보낼 수 있는 개방형 표준 가용성 프레임워크입니다. 스레드 로컬에 관련 분산 추적 정보를 밀어 넣고 네트워크 상의 하위 호출 흐름을 지속적으로 이어갑니다.

  • W3C Trace Context 표준을 지원하여 각 마이크로서비스 인스턴스 전반에 통합된 trace_id를 끊김 없이 배포시킵니다.
  • 내장 스레드 바인딩 제약으로 인해 백그라운드 워커 스레드로 인계되는 영역에서는 Context 전파 유실 이슈가 수반됩니다.
Concept · 03

MDC (Mapped Diagnostic Context)

개별 로그 이벤트에 요청 세션별 유니크한 진단 속성 값을 가미해 남길 수 있도록 로깅 라이브러리가 제공하는 로컬 맵 저장소입니다.

  • 스레드별 고유 영역을 참조해 고속으로 로그 정보를 필터링해 줍니다.
  • 수동 관리 성격이 짙고 네트워크 분산 시스템 경계를 넘어서 컨텍스트를 동적으로 자동 복사 및 전송할 수 없어 단독으로는 분산 추적 수단으로 한계가 있습니다.