분산 트레이스로 End-to-End 레이턴시 메트릭 추출하기
📺 원본 영상: End-to-End Latency Metrics From Distributed Trace - Kusha Maharshi - CppCon 2025 🎙️ 채널: CppCon 📅 정리 일자: 2026-03-07 ⚠️ Note: 본 리포트는 영상의 음성을 텍스트로 변환(STT)하여 정리한 것입니다. 화면에 표시된 자료(차트, 코드, 슬라이드 등)는 포함되지 않을 수 있습니다.
개요
Bloomberg의 시니어 소프트웨어 엔지니어 Kusha Maharshi는 CppCon 2025에서 자사 텔레메트리 인프라 팀이 구축한 분산 트레이스(Distributed Trace) 기반 End-to-End 레이턴시 메트릭 수집 시스템을 소개한다. 금융 도메인에서 수십 밀리초의 지연조차 수백만 달러의 손실로 이어질 수 있다는 현실적 배경 아래, 하루 500억 개(50 billion) 이상의 스팬(span)을 처리해야 하는 초고처리량 환경에서 C++로 이 문제를 해결한 두 가지 아키텍처 접근법과 그 과정에서 맞닥뜨린 성능 병목, 그리고 프로파일링을 통한 해결 과정을 상세히 다룬다.
배경: 왜 End-to-End 레이턴시가 필요한가
Bloomberg의 텔레메트리 인프라 팀은 회사 전체 엔지니어링 조직을 클라이언트로 두는 내부 플랫폼 팀이다. 이 팀의 고객들은 트레이딩 시스템, 메시징 플랫폼, 뉴스 플랫폼 등 고처리량·저레이턴시가 핵심인 시스템을 운영하며, 이들에게 서비스의 속도, 요청 실패율, 캐시 히트율 같은 헬스 데이터를 제공하는 것이 팀의 임무다.
문제는 현대적 분산 아키텍처에서는 어떤 사용자 액션도 단일 서비스 내에서 완결되지 않는다는 점이다. 온라인 쇼핑 사이트에서 결제 버튼 하나를 누르더라도 수많은 서비스가 연쇄적으로 상호작용한다. 이런 환경에서 엔지니어링 팀들이 반복적으로 요청한 것은 “서비스 A가 서비스 B를 거쳐 서비스 C에 도달하는 전체 경로의 레이턴시를 알고 싶다”는 것이었다. 기존에는 서비스 단위 메트릭만 존재하거나, 각 팀이 직접 세션 ID나 요청 ID를 전파하는 맞춤형 추적 메커니즘을 자체 구축하는 방식이 전부였다. 이런 파편화된 접근을 통합하는 중앙화된 솔루션으로 분산 트레이싱이 주목받게 되었고, Maharshi 팀은 이를 텔레메트리의 통합 기반으로 삼기로 결정했다.
분산 트레이싱(Distributed Tracing)의 기초
분산 트레이싱은 복잡한 분산 시스템을 통과하는 요청의 흐름을 추적하고 시각화하는 기술이다. 사용자의 API 요청이 파싱 서비스를 거쳐 데이터베이스 조회로 이어지는 일련의 흐름 전체가 하나의 **트레이스(trace)**로 표현되며, 그 안의 각 단계—각각의 홉(hop), 노드, 서비스 호출—는 **스팬(span)**이라는 블록으로 구성된다.
모든 트레이스는 고유한 Trace ID를 가지며, 각 스팬은 고유한 Span ID를 가진다. 스팬 사이에는 부모-자식 관계가 존재하는데, 호출된 쪽(callee)이 호출한 쪽(caller)을 참조하는 방식으로 이 관계가 인코딩된다. 스팬이 보유하는 가장 중요한 정보는 타이밍 정보로, 각 작업이 언제 시작되고 언제 종료되었는지를 기록한다. 이 타이밍 데이터가 바로 End-to-End 레이턴시 메트릭 계산의 원천이 된다.
실제 사례: 주식 주문 실행 시스템(Equity Order Execution System)
강연 전반에 걸쳐 사용되는 예시는 주식 주문 실행 파이프라인이다. 트레이더들이 Apple, Microsoft 같은 주식 매수 주문을 제출하면, 해당 주문은 브로커(broker)를 거쳐 처리되고 체결 메시지(fill message)가 생성된다. 이 메시지는 주문 관리 시스템(Order Management System)을 통과한 뒤 Kafka 큐에 쌓이고, 이를 퍼블리셔 서비스(publisher service)가 배치(batch)로 처리하여 최종 구독자들—즉 트레이더들—에게 주문 체결 상태를 전달한다.
Kafka는 스트리밍 데이터의 실시간 처리를 위한 분산 데이터 스토어로, 이 파이프라인에서는 메시지 큐로서 높은 확장성과 파티셔닝 로직을 제공한다. 이 파이프라인에서 지연이 발생하면 트레이더가 주문 체결 상태를 제때 확인하지 못해 투자 기회를 잃을 수 있으므로, 이 파이프라인 전체의 End-to-End 레이턴시 모니터링은 비즈니스에 직결된 문제다.
핵심 개념: 트레이스는 DAG이다
분산 트레이스는 구조적으로 **방향성 비순환 그래프(DAG, Directed Acyclic Graph)**와 동일하다. 각 스팬은 노드이고, 부모-자식 참조는 엣지의 방향을 결정한다. 이 관점에서 두 가지 중요한 패턴이 등장한다. 하나의 스팬이 여러 자식 스팬을 호출하는 **팬아웃(fan-out)**과, 여러 개의 서로 다른 트레이스에서 출발한 스팬들이 하나의 스팬으로 수렴되는 **팬인(fan-in)**이다.
팬인 패턴은 큐잉(queuing)과 배칭(batching) 구조를 가진 시스템에서 자연스럽게 발생한다. 독립적으로 시작된 여러 요청이 워커 스레드 등에 의해 배치로 묶여 함께 처리되는 경우가 전형적인 예다. 이때 하나의 process 스팬이 서로 다른 Trace ID를 가진 여러 consume 스팬을 부모로 참조하게 된다. 이것이 강연에서 다루는 핵심 문제의 출발점이다.
문제 정의: 룰(Rule)과 이벤트(Event)
사용자(다른 엔지니어링 팀)는 텔레메트리 팀에게 **룰(rule)**을 정의하여 원하는 End-to-End 레이턴시를 요청한다. 룰은 시작점과 끝점을 명시한다. “consume에서 publish까지의 레이턴시를 알고 싶다”는 것이 룰의 전형적인 형태다. 이때 특정 스팬 집합을 식별하는 방식을 **이벤트(event)**라고 부르는데, consume 오퍼레이션에 해당하는 스팬 전체가 시작 이벤트이고, publish 오퍼레이션에 해당하는 스팬 전체가 종료 이벤트가 된다.
단일 트레이스 안에서 레이턴시를 계산하는 방법은 간단하다. DAG를 아래에서 위로(스팬이 부모 참조를 저장하기 때문에) DFS(깊이 우선 탐색)로 순회하여 시작 이벤트와 종료 이벤트를 잇는 유효한 경로들을 찾고, 각 경로에서 종료 시각과 시작 시각의 차이를 구하면 된다. 문제는 이를 하루 500억 개의 스팬 규모로, 특히 팬인이 존재하는 상황에서 확장 가능하게 처리하는 것이다.
스케일의 문제: 팬인이 모든 것을 복잡하게 만든다
팬인 패턴에서는 서로 다른 Trace ID를 가진 트레이스들이 논리적으로 연결된다. 이렇게 팬인을 통해 서로 연결된 트레이스들의 묶음을 강연자는 **트레이스 번들(trace bundle)**이라고 부른다. 그래프 이론적으로는 약하게 연결된 그래프(weakly connected graph)의 개념과 동일하다. 문제는 Kafka에서 스팬들이 Trace ID 기준으로 파티셔닝되어 있을 때, 팬인으로 연결된 서로 다른 트레이스들의 스팬이 서로 다른 파티션에 흩어져 있다는 점이다. DFS를 완수하려면 하나의 트레이스 번들에 속하는 모든 스팬이 같은 곳에 모여야 한다. 이것을 어떻게 달성할 것인가가 이 강연의 핵심 기술적 질문이다.
접근법 1: 룰 기반 트레이스 번들 집계 (Greedy Approach)
첫 번째 접근은 팬인 관계를 미리 파악해 트레이스 번들을 사전에 구성하는 그리디(Greedy) 전략이다.
아키텍처는 세 단계의 파이프라인으로 구성된다. 먼저 파티셔너(Partitioner) 서비스가 Kafka 토픽에 유입되는 스팬들을 Trace ID 기준으로 재파티셔닝하여, 동일한 Trace ID를 가진 스팬들이 모두 같은 파티션에 모이도록 보장한다. 이어서 번들러(Bundler) 서비스가 핵심 역할을 담당하는데, 팬인 관계를 파악하여 동일한 트레이스 번들에 속하는 스팬들을 같은 파티션으로 모으고, 동시에 룰의 시작 이벤트와 매칭되지 않는 트레이스는 조기에 필터링한다. 마지막으로 Dagger 서비스(DAG에서 이름을 딴 것)가 DFS를 실행하여 레이턴시 메트릭을 생성한다.
번들러 인스턴스들 사이의 협업은 인스턴스 간 통신을 위한 별도의 소규모 Kafka 토픽을 통해 이루어진다. 번들러 인스턴스 하나가 팬인 관계를 발견하면, 이 정보를 다른 인스턴스들에게 브로드캐스트한다. 예를 들어 번들러 1이 T1, T2, T3 트레이스 간의 팬인 관계를 발견했다면, “T1과 T2가 팬인으로 연결되어 있고, T1이 consume to publish 룰에 매칭된다”는 메시지를 번들러 2에게 전달한다. 이를 통해 모든 번들러 인스턴스가 시스템 전체의 트레이스 번들 상태를 담은 인메모리 맵을 동기화하여 유지하게 된다.
💡 [영상 참조] 강연자는 이 시점에서 번들러 인스턴스 간 gossip 메시지 흐름과 인메모리 맵 구조를 시각화한 슬라이드를 통해 설명합니다. 상세 내용은 원본 영상을 확인하세요.
C++ 데이터 구조
이 접근법에서 번들러가 유지하는 핵심 데이터 구조는 두 가지 unordered_map으로 구성된다. 첫 번째는 트레이스 번들의 대표 Trace ID(선택 트레이스 ID)에서 해당 번들에 속하는 모든 Trace ID와 매칭 룰 집합으로의 맵이다. 두 번째는 역방향 조회 맵으로, 번들 내 개별 Trace ID에서 대표 Trace ID로의 매핑을 담는다. 이 두 맵을 조합하면 임의의 스팬이 들어왔을 때 O(1)에 가까운 시간으로 해당 스팬이 속한 번들과 관련 룰을 조회할 수 있다.
병목 발견: 불필요한 데이터 구조
구현 과정에서 gossip 메시지 처리가 비정상적으로 느려지는 문제가 발생했다. perf와 플레임 그래프(flame graph)를 이용한 프로파일링 결과, 처리 시간의 대부분이 trace_to_rule_flush_indices라는 추가 unordered_map의 조회와 쓰기에 소비되고 있음이 드러났다. 이 맵은 각 트레이스별로 매칭된 룰과 이미 Kafka로 플러시된 스팬의 수를 추적하기 위해 만들어졌는데, 알고 보니 이것은 조기 최적화의 산물이었다. 실제로 필요한 것은 트레이스 번들 단위로 매칭된 룰이었지 개별 트레이스 단위가 아니었다. 이 데이터 구조를 제거하자 gossip 처리 성능이 크게 향상되었다. 강연자는 이를 “조기 최적화는 대가가 따른다”는 교훈으로 정리했다.
이 접근법의 한계
이 그리디 접근법은 룰이 소수의 트레이스에만 매칭되는 경우에 효과적이다. 그러나 미들웨어 스팬처럼 거의 모든 트레이스에 매칭되는 룰이 존재하거나, 수천 개의 트레이스가 팬인으로 연결되어 거대한 번들을 형성하는 경우에는 특정 파티션과 Dagger 인스턴스에 부하가 집중되는 **로드 불균형(load imbalance)**이 발생한다.
접근법 2: 부분 DFS 위임 (Lazy Approach)
두 번째 접근은 팬인 문제를 사전에 해결하는 대신, DFS 실행 중 필요한 시점에 다른 인스턴스에게 처리를 위임하는 레이지(Lazy) 전략이다.
이 접근에서는 Dagger 서비스가 Trace ID 기준으로 파티셔닝된 Kafka 토픽에서 직접 스팬을 소비한다. Dagger 인스턴스는 DFS를 실행하다가 현재 자신이 보유하지 않은 다른 Trace ID의 스팬이 필요한 팬인 상황을 만나면, 해당 트레이스를 보유한 다른 Dagger 인스턴스에게 “나는 여기까지 DFS를 진행했고, 종료 시각은 이러이러하며, 목표 룰은 consume to publish이니, 당신이 가진 T2 트레이스에서 DFS를 완성해달라”는 메시지를 전달한다. 이 위임은 팬인 부모의 Trace ID를 기준으로 파티셔닝된 전용 Kafka 토픽을 통해 이루어진다.
이 접근법은 트레이스 번들을 사전에 구성하지 않기 때문에 특정 룰이나 인스턴스에 부하가 집중되는 문제를 구조적으로 회피한다. 그러나 팬인이 체인 형태로 깊게 연결된 경우—팬인의 부모가 또 다른 팬인의 자식인 경우—에는 gossip 홉이 반복되어 지연이 누적되는 단점이 있다.
C++ 데이터 구조
Dagger에서의 핵심 자료구조는 세 가지다. 유입되는 스팬들과 해당 스팬에 잠재적으로 매칭되는 룰 목록을 담는 구조체, 처리 대상 트레이스들을 Trace ID와 해당 트레이스 데이터(스팬 및 룰)에 대한 포인터로 관리하는 맵, 그리고 아직 완료되지 않은 부분 DFS들의 버퍼가 그것이다.
병목 발견: 이벤트 매칭 중복 실행
이 Dagger 구현에서는 특정 트레이스 하나의 처리가 수 초씩 걸리는 이상 현상이 관찰되었다. 원인을 파악하기 위해 역시 perf와 플레임 그래프로 해당 코드를 프로파일링했고, 문제는 예상 밖의 지점에서 발견되었다.
일부 사용자는 “DFS를 건너뛰고 단순 산술로 레이턴시를 계산하라”는 고급 설정을 활용한다. 시작 이벤트와 종료 이벤트가 동일한 트레이스 내에 각각 하나씩만 존재할 때는 publish 시각 - consume 시각이라는 단순 계산으로도 충분하기 때문이다. 그런데 이 단순 모드에서 오히려 처리가 더 느렸다. 프로파일링 결과, 하나의 트레이스에 500개의 publish 스팬이 존재하는 경우, consume 이벤트 매칭 함수가 500번 반복 호출되고 있었다. 해결책은 단순했다. 이미 시작 이벤트로 매칭된 스팬에 대한 캐시를 추가하여 동일한 스팬에 대한 재매칭을 방지하는 것이었다. 이 작은 수정으로 처리 성능이 크게 개선되었다.
C++를 선택한 이유
강연자는 이 시스템을 C++로 구현한 이유를 다음과 같이 정리한다. 이동 시맨틱스(move semantics), 참조(reference), 포인터 관리를 통한 세밀한 메모리 제어가 가능하고, 멀티스레딩 라이브러리와 Bloomberg 내부 래퍼를 통한 대규모 동시성 처리가 지원되며, 성능이 검증된 표준 라이브러리 자료구조(STL)를 활용할 수 있다는 점이 핵심이다. 또한 C++의 성숙한 생태계가 제공하는 디버깅 및 테스팅 도구—특히 이번 사례에서처럼 perf를 이용한 프로파일링과 플레임 그래프 시각화—가 중요한 역할을 했다.
한 청중이 문자열(string) 키를 사용하는 unordered_map이 고성능 환경에 적합한지 질문했는데, 강연자는 정수 기반 ID나 string_view 사용도 검토했지만 프로파일링 결과 문자열 연산이 병목이 아니었기 때문에 코드 단순성을 위해 그대로 유지했다고 답했다. BSL(Bloomberg Standard Library)의 커스텀 알로케이터 사용 여부에 대해서도 질문이 있었는데, 현재는 표준 알로케이터를 사용하고 있으며 이 역시 향후 검토 대상이라고 밝혔다.
Q&A에서 드러난 운영 현실
강연 후 Q&A에서는 이 시스템의 실제 운영 관련 중요한 내용들이 논의되었다.
클락 스큐(clock skew) 문제에 대해서는, 일부 초고속 레이턴시 민감 팀들은 자체 커스텀 솔루션을 사용하며 이 시스템은 그 문제를 아직 해결하지 않았다고 솔직하게 인정했다. 분산 트레이스 클락 스큐에 관한 연구 논문도 존재하며, 관심 있는 청중에게 검색을 권했다.
재정렬(reordering) 및 타이밍 불확실성 문제는 버퍼링으로 대응한다. 스팬을 일정 시간 동안 버퍼에 쌓아두고 경험적 기준(heuristics)—예컨대 마지막 스팬 수신 후 일정 시간이 지나면 완료로 간주—을 적용하여 처리를 시작한다. 현재 레이턴시 데이터의 신선도(freshness)는 3분에서 6분 사이이며, 이 지연의 대부분은 트레이스 완료 여부를 판단하기 위한 대기 시간에서 비롯된다. 강연자는 사용자가 예상 소요 시간을 직접 알려주는 기능을 개발 중임을 언급했다.
**장애 복구(disaster recovery)**에 대해서는, 현재 Dagger 인스턴스가 다운되면 해당 인메모리 데이터는 손실된다고 솔직히 인정했다. 향후 Write-Ahead Log를 디스크 또는 퍼시스턴트 볼륨에 기록하는 방식의 내구성 확보를 계획 중이다.
**완전성 메트릭(completeness metrics)**도 현재 개발 중인 기능으로, 예상 스팬이 수신되지 않거나 지정 시간 내에 완료되지 않은 경우를 별도의 메트릭으로 사용자에게 알려주는 것을 목표로 한다. 이를 통해 레이턴시 메트릭과 함께 “완료되지 않은 케이스의 비율”도 제공할 예정이다.
마무리: 핵심 교훈
Maharshi는 이 여정에서 얻은 핵심 교훈을 세 가지로 압축한다. 첫째, End-to-End 레이턴시 메트릭은 복잡한 분산 아키텍처에서도 생성 가능하며, 팬인이라는 구조적 복잡성을 DAG로 모델링하는 것이 해결의 출발점이다. 둘째, 최선의 접근법은 하나가 아니다. 그리디 접근(접근법 1)은 룰 매칭 범위가 좁을 때 효율적이고, 레이지 접근(접근법 2)은 대규모·다양한 룰에서의 로드 균형에 유리하며, 실제로는 운영 지식을 바탕으로 두 접근법의 하이브리드를 선택하는 것이 현명하다. 셋째, 성능은 설계 단계부터 고려하되, 조기 최적화는 오히려 역효과를 낸다. 이번 사례에서도 두 번의 프로파일링이 각각 불필요한 데이터 구조와 중복된 이벤트 매칭이라는 진짜 병목을 찾아냈다. “측정하지 않고 최적화하지 말라”는 원칙이 실전에서 어떤 모습인지를 이 강연은 구체적으로 보여준다.
📚 출처 및 참고
| 구분 | 내용 |
|---|---|
| 원본 영상 | End-to-End Latency Metrics From Distributed Trace - Kusha Maharshi - CppCon 2025 |
| 채널 | CppCon |
| 리포트 작성 | 2026-03-07 |
| 작성 방식 | YouTube 영상 STT 변환 후 에디팅 |
💬 본 리포트는 영상 내용을 바탕으로 재구성되었습니다. 정확한 내용은 원본 영상을 참조해 주세요.