Discord, Elixir 시스템 추적 기술 도입으로 **성능 문제 해결**
Discord는 Elixir의 경량 프로세스(Lightweight Processes)를 활용하여 각 서버(Guild)를 독립적으로 운영하지만, 과부하 시 성능 저하 발생
분산 추적(Distributed Tracing) 도입을 위해 자체 메시지 전달 라이브러리인 Transport 개발 및 OpenTelemetry 활용
Head Sampling 및 동적 샘플링(Dynamic Sampling) 기법을 통해 스팬(Span) 폭주(Span Explosion) 문제 해결
CPU 사용량 최적화(CPU Usage Optimization)를 위해 불필요한 컨텍스트 언패킹(Context Unpacking) 방지
Elixir 환경에서의 분산 추적(Distributed Tracing) 도입
Discord는 Elixir의 경량 프로세스(Lightweight Processes)를 활용하여 서버(Guild)를 운영하지만, 메시지 전달 방식의 특성상 분산 추적(Distributed Tracing) 구현에 어려움이 있었다. 기존 HTTP 헤더 기반의 추적 방식 대신, 자체 개발한 Transport 라이브러리를 통해 메시지에 추적 컨텍스트(Trace Context)를 포함시키는 방식을 채택했다. Transport는 GenServer의 call/cast 함수를 대체하여, 개발자가 추적 컨텍스트를 직접 관리하는 부담을 줄였다.
Transport 라이브러리의 핵심 설계
Transport 라이브러리는 Envelope라는 새로운 구조체를 도입하여 메시지에 추적 컨텍스트를 추가한다. Envelope는 메시지와 추적 컨텍스트를 캡슐화하며, `wrap_message` 함수를 통해 메시지를 감싸고, `handle_message` 함수를 통해 컨텍스트를 추출한다. 이러한 설계는 기존 코드 변경 없이 점진적인 배포를 가능하게 했으며, Elixir의 메시지 전달(Message Passing) 방식을 활용하여 분산 시스템(Distributed System)의 추적 기능을 효과적으로 구현했다.
스팬(Span) 폭주(Span Explosion) 문제 해결
Discord는 Head Sampling 방식을 사용하지만, Fanout 모델로 인해 스팬(Span)의 수가 기하급수적으로 증가하는 문제를 겪었다. 이를 해결하기 위해 동적 샘플링(Dynamic Sampling)을 도입하여, Fanout 크기에 따라 샘플링 비율을 조절했다. Envelope에 `approximate_num_recipients` 필드를 추가하여, 각 세션(Session)이 샘플링 결정(Sampling Decision)을 Fanout 크기에 따라 다르게 하도록 했다. 이러한 접근 방식은 성능 오버헤드(Performance Overhead)를 줄이면서도 유용한 성능 데이터(Performance Data)를 유지하는 데 성공했다.
CPU 사용량 최적화(CPU Usage Optimization) 전략
추적(Tracing) 도입 후, CPU 사용량 증가(CPU Usage Increase) 문제가 발생했다. 주된 원인은 gRPC 요청 처리(gRPC Request Handling) 과정에서 추적 컨텍스트 언패킹(Trace Context Unpacking)에 과도한 CPU 자원이 소모되었기 때문이다. Discord는 샘플링 여부(Sampling Status)를 Trace Context의 마지막 바이트(Last Byte)를 통해 확인하는 방식으로 불필요한 언패킹(Unnecessary Unpacking)을 방지했다. 이러한 최적화를 통해 CPU 사용량(CPU Usage)을 정상 수준(Nominal Levels)으로 되돌릴 수 있었다.