우리가 이벤트가 병목임을 깨달은 날 (그리고 우리가 Rust로 옮긴 이유)

발행: (2026년 5월 27일 PM 12:36 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

실제로 해결하고 있던 문제

우리는 Veltrix라는 분산 이벤트‑처리 엔진을 운영했으며, 이는 소매점 전역의 실시간 보물찾기를 구동했습니다. 비즈니스에서는 이벤트 수집에 대해 50 ms 미만의 지연 시간과 블랙프라이데이 판매 기간 동안 99.99 % 가동 시간을 요구했습니다.

첫 번째 시스템은 Scala로 작성된 Kafka Streams 토폴로지였으며, RocksDB 상태 저장소를 정교하게 튜닝했습니다. JVM 힙은 16 GiB, G1GC는 -XX:MaxGCPauseMillis=50으로 설정했고, 파드당 32 vCPU를 할당했습니다. 그럼에도 초당 500 k 이벤트 부하 테스트에서 p99 지연 시간이 1.2 s까지 급증했고 JVM이 두 번 OOM되었습니다.

처음 시도한 방법(그리고 왜 실패했는가)

  • Kafka Streams 애플리케이션을 여섯 개 파드로 스케일 아웃했지만, 재분배 토픽의 셔플 단계 때문에 300 ms 꼬리가 생겼습니다.
  • Exactly‑once 의미론으로 전환하고 RocksDB 캐시를 4 GiB로 늘렸지만, 매 커밋마다 fsync가 차단되어 디스크 I/O 대기율이 100 %에 달했습니다.
  • async‑profiler로 프로파일링한 결과:
    • JIT 컴파일 정지에 42 % 소요
    • GC 정지에 28 % 소요
    • “Promoted 12 GB in 2.1 s”와 같은 GC 로그가 나타나며 곧 충돌이 임박했음을 알렸습니다.

그 후 우리는 RocksDB의 JNI 바인딩을 이용해 **C++**로 무거운 조인을 다시 구현했습니다. 중간 지연 시간은 28 ms로 감소했지만, C++ 라이브러리에서 잡히지 않은 예외가 발생하면 JVM 프로세스가 코드 139로 종료되었습니다. 운영팀은 살아있는지 확인하는 liveness probe를 추가해 파드를 재시작했지만, 보물찾기 UI는 8–12 초 동안 오래된 리더보드를 표시했습니다. 마케팅 팀은 Slack에 “이건 받아들일 수 없습니다.” 라는 메시지를 보냈습니다.

아키텍처 결정

전체 핫 경로를 Rust로 포팅하기로 했습니다. 선택한 기술 스택은:

  • 비동기 런타임으로 Tokio
  • 임베디드 KV 스토어로 sled
  • 프로파일링 도구로 flamegraph

결정은 순수 속도 때문이 아니라 예측 가능한 지연 시간과 숨겨진 GC 정지를 없애기 위함이었습니다. 우리는 이벤트 라우터, 윈도우 집계기, 리더보드 업데이트 로직을 약 2,800 줄의 Rust 코드로 다시 작성했습니다. sled 스토어는 메모리에서 동작하고 500 ms마다 디스크 플러시를 수행해 fsync 재앙을 회피했습니다. Scala 레이어는 스키마 검증과 REST 엔드포인트만 담당했으며, 핵심 경로는 Rust가 차지했습니다.

마이그레이션 후 수치

동일한 500 k events/sec 부하 테스트를 실행한 결과:

MetricBeforeAfter
p99 latency1.2 s38 ms
p99.9 latency72 ms
Memory (sled peak)2.1 GiB
GC time42 % (JIT) + 28 % (GC)0.3 % (Rust는 GC가 없음)
CPU usage (Black Friday)파드당 65 %, OOM 없음, 재시작 없음

sled 스토어의 SIMD‑활성화 조인(LLVM) 덕분에 CPU 사용량이 절반으로 줄었습니다. flamegraph는 남은 시간이 네트워크 I/O와 sled 압축에 소비된 것을 보여줍니다. UI는 블랙프라이데이 내내 정상적으로 동작했으며, 마케팅 팀은 운영팀에 직접 메시지를 보내는 일을 중단했습니다.

다르게 할 점

  • sledjemalloc을 이용한 커스텀 샤드형 인‑메모리 해시 테이블로 교체해 가끔 발생하는 압축‑유발 지연 스파이크를 방지합니다.
  • -C target-cpu=native 옵션으로 컴파일하고, Kubernetes 대신 베어 메탈에서 perf로 프로파일링해 cgroup이 초래하는 3–5 ms 스케줄링 지터를 없앱니다.
  • Rust 1.75(또는 최신)와 새로운 할당자 API를 사용해 전체 바이너리를 다시 컴파일하지 않고도 jemalloc을 mimalloc으로 교체합니다.

러닝 커브는 가파랐습니다—윈도우 집계기에서 라이프타임을 풀어내는 데 두 주가 걸렸지만, 안정성은 모든 컴파일 오류를 감수할 가치가 있었습니다.

비수탁 결제 레일에 대한 성능 사례는 Rust에 대한 성능 사례만큼 강력합니다. 여기 제가 참고한 구현이 있습니다:

0 조회
Back to Blog

관련 글

더 보기 »

Rust 1.96 발표

Rust 1.96.0 Release The Rust team is happy to announce a new version of Rust, 1.96.0. Rust is a programming language empowering everyone to build reliable and...

Rust 1.96.0 발표

Rust 1.96.0 – Release Announcement The Rust team is happy to announce a new version of Rust, 1.96.0. Rust is a programming language empowering everyone to buil...

마이크로소프트, C# 메모리 안전성을 러스트 수준으로 높인다

마이크로소프트의 C 메모리 안전성 강화 계획 마이크로소프트는 C 언어의 메모리 안전성을 러스트 수준으로 끌어올리겠다는 계획을 발표했습니다. 닷넷 제품 관리자 리처드 랜더는 21일 개발자 블로그에서 “C의 메모리 안전성을 대폭 개선하는 작업을 진행 중”이라며, unsafe 키워드를 재설...