Just One Function, 10x Faster? Reading a Rust Performance PR

Lobsters · 2026.08.13

단 하나의 함수가 CPU를 36%나 잡아먹고 있었다

GreptimeDB는 Prometheus의 원격 읽기 백엔드로 동작할 수 있다. 이 과정의 마지막 단계는 쿼리 엔진이 생성한 컬럼 기반의 RecordBatch를 Prometheus 프로토콜의 행 기반 TimeSeries로 변환하는 것이다. 구체적으로는 레이블 집합을 기준으로 행을 그룹핑하고, 각 시리즈의 샘플을 모아 정렬하는 작업이다.

이 변환을 담당하는 함수 recordbatches_to_timeseries는 오랫동안 코드베이스에서 손대지 않은 채로 방치되어 있었다. 그러던 중 한 커미터가 PR을 열어 해당 함수를 전면 재작성했다. 변경된 파일은 src/servers/src/prom_store.rs 단 하나였다.

PR에 첨부된 CPU 프로파일이 이 작업의 출발점이었다. 300K 시리즈, 4개의 동시 읽기 환경에서 RecordBatch → TimeSeries 단계가 전체 CPU의 36.2% 를 소모했고, 딕셔너리 구체화(dictionary materialization)만으로도 8.9%를 차지했다. 디스크 접근도 없고, 압축 해제도 없는, 순수하게 메모리 내에서 데이터를 이동하는 함수가 CPU의 3분의 1 이상을 태우고 있었다는 것은 명백한 비정상이다.

벤치마크로 문제를 재현하고, 수치로 증명한다

PR 자체에는 벤치마크 코드가 포함되지 않았기 때문에, 이 분석에서는 공개된 진입점을 기준으로 벤치마크를 직접 작성하여 변경 전후를 비교했다. 이 접근 방식은 "코드를 읽기 전에 먼저 재현한다"는 성능 최적화의 기본 원칙을 따른다.

변경 전 수치는 다음과 같았다:

  • 10K 행: 약 3.7ms
  • 100K 행: 약 44ms
  • 처리량: 초당 300만 행 미만

변경 후, 최적의 케이스에서는 10배 이상 빠른 결과가 나왔다. 단일 함수 재작성으로 이런 수치가 나올 수 있었던 이유는 최적화의 방향이 명확했기 때문이다. 딕셔너리 인코딩된 컬럼을 반복적으로 구체화하는 대신, 사전에 레이블 구조를 파악하고 재사용하며, HashMap 충돌을 줄이는 방식으로 데이터 이동 전략 자체를 바꾼 것이다.

// 변경 전: 매 행마다 딕셔너리를 구체화하여 String 생성 반복
for row_idx in 0..num_rows {
    let label_value = dict_array.value(row_idx).to_string(); // 비용 큼
    ...
}

// 변경 후: 딕셔너리 값을 먼저 한 번만 추출하여 재사용
let dict_values: Vec<&str> = (0..dict_array.values().len())
    .map(|i| dict_array.values().value(i))
    .collect();
// 이후 row_idx → key index → dict_values[key] 방식으로 접근

Java 백엔드 개발자에게 주는 시사점

이 사례는 언어와 무관하게 적용되는 보편적인 교훈을 담고 있다. Java 기반 시스템에서도 동일한 패턴의 성능 함정이 자주 발생한다.

반복적인 객체 생성과 변환 비용을 과소평가하지 말 것. Java에서도 Stream 파이프라인 내부에서 toString(), new HashMap<>(), String.format() 등을 반복 호출하는 코드는 GC 압력을 높이고 처리량을 크게 낮출 수 있다. 특히 대용량 데이터를 행 단위로 처리하는 배치성 로직이나 API 응답 직렬화 구간에서는 프로파일링 없이 "잘 동작한다"고 넘기기 쉽다.

CPU 프로파일이 없으면 최적화 방향을 잡을 수 없다. 이 PR이 존재할 수 있었던 이유는 누군가 실제 부하 환경에서 CPU 프로파일을 찍었기 때문이다. Java 환경에서는 async-profiler, JFR(Java Flight Recorder), Pyroscope 같은 도구를 평소에 익혀두고, 실제 트래픽 환경에서의 핫스팟을 주기적으로 확인하는 습관이 필요하다.

정리

  • 디스크나 네트워크 없이 메모리 내 변환만으로도 잘못된 자료구조 접근 패턴은 CPU 병목을 만들 수 있다.
  • 성능 개선은 코드를 읽기 전에 벤치마크로 기준선을 잡는 것에서 시작해야 한다.
  • 단 하나의 함수, 단 하나의 파일 변경으로 10배 이상의 처리량 개선이 가능하다는 것은 기존 코드의 프로파일링이 얼마나 중요한지를 보여준다.
Source
Lobsters
원문 보기 →
← 목록으로 돌아가기