Featured image of post [Regression 12] 벤치마크 CI 통합

[Regression 12] 벤치마크 CI 통합

벤치마크를 CI 파이프라인에 통합할 때 발생하는 노이즈 문제와 이를 다루는 CodSpeed·Bencher의 서로 다른 접근을 비교합니다. 계측 기반 측정과 베어메탈 동일성 벤치마킹의 원리를 정리하고, 각각의 한계와 선택 기준도 함께 다룹니다.

벤치마크 CI 통합이란 마이크로벤치마크를 CI 파이프라인에 직접 연결해, 사람이 수동으로 벤치마크를 돌리고 결과를 비교하지 않아도 매 커밋·매 PR마다 성능 지표가 자동으로 측정·기록·비교되게 만드는 작업을 말합니다. 이전 장에서 성능 테스트를 자동으로 “돌리는” 파이프라인을 만들었다면, 이 장의 문제는 그다음 단계입니다 — 공유 CI 러너에서 실행 시간을 재면 같은 코드도 실행마다 결과가 크게 흔들리는데, 이 흔들림 위에서 어떻게 신뢰할 수 있는 비교를 만들 것인가입니다. 이 장에서는 이 문제를 상반된 방식으로 푸는 두 도구 — 명령어 수를 세는 계측 기반 측정으로 러너의 노이즈 자체를 우회하는 CodSpeed와, 로컬·CI 모두에서 동일한 전용 하드웨어를 제공해 노이즈의 원인을 물리적으로 없애는 Bencher — 를 통해 벤치마크 CI 통합의 두 가지 설계 축을 이해합니다.

이 장을 읽기 전에

선행 챕터: 성능 테스트 자동화 구축(챕터 02)에서 다룬 “벤치마크를 CI에서 자동 실행한다"는 전제를 그대로 이어받습니다. 그 챕터가 “무엇을 자동으로 돌릴 것인가"를 다뤘다면, 이 장은 “그 결과를 어떻게 신뢰할 수 있게 비교할 것인가"를 다룹니다. 01장: 성능 회귀란 무엇인가에서 정의한 “회귀"의 통계적 의미와, Tr.01의 통계적 벤치마킹·Google Benchmark 실전에서 다룬 반복 측정·분산 개념을 전제로 합니다.

이 장의 깊이: 중급입니다. 벤치마크 CI 통합이 어려운 근본 이유(공유 러너의 노이즈)부터 시작해, 이를 다루는 두 가지 상반된 설계 — 계측 기반 결정론적 측정(CodSpeed)과 전용 베어메탈 하드웨어(Bencher) — 의 내부 동작 원리와 2026년 시점의 기능 확장을 다룹니다. 다루지 않는 것: PR에서 실패·경고를 어떤 기준으로 판정할지는 PR 성능 게이트(챕터 04)로, GitHub Actions/GitLab CI 워크플로 자체를 처음부터 구성하는 방법은 Benchmark as Code(챕터 14)로, 기준선을 어떻게 저장·갱신할지는 기준선 관리(챕터 06)로 위임합니다.

당신의 수준에 맞는 경로

수준읽을 부분핵심 목표
입문“왜 공유 CI 러너에서는” ~ “계측 기반 측정 vs 벽시계 시간”벤치마크 CI 통합이 왜 단순 자동 실행보다 어려운지 이해
중급“CodSpeed 2026” ~ “Bencher의 베어메탈 동일성 벤치마킹”두 도구의 내부 동작 원리와 2026년 기능 확장을 파악
실무 적용“흔한 오개념” ~ “비판적 시각”팀 상황에 맞는 도구 선택과 한계·트레이드오프 판단

배경: 로컬 벤치마크에서 CI 네이티브 플랫폼까지

벤치마크를 CI에 넣는다는 아이디어 자체는 새롭지 않습니다. Google Benchmark나 Criterion.rs 같은 통계적 마이크로벤치마크 프레임워크는 오래전부터 반복 측정·이상치 제거·신뢰구간 계산을 지원해왔고, 팀들은 이 결과를 CI 로그에 출력하거나 파일로 저장해 수동으로 비교해왔습니다. 문제는 “프레임워크가 통계적으로 엄밀한 숫자를 낸다"는 것과 “그 숫자를 CI에서 자동으로 신뢰할 수 있게 비교한다"는 것이 서로 다른 문제라는 점입니다. GitHub Actions 같은 공유 호스티드 러너는 매번 다른 물리 호스트에, 다른 이웃 워크로드와 함께 배정되므로, 같은 이진 파일을 두 번 실행해도 벽시계 시간(wall-clock time)이 크게 흔들릴 수 있습니다. 이 간극을 메우려는 시도에서 CodSpeed와 Bencher 같은 CI 네이티브 벤치마킹 플랫폼이 등장했습니다. 두 도구는 “노이즈가 있는 공유 환경에서 어떻게 신뢰할 수 있는 성능 비교를 만들 것인가"라는 같은 질문에 서로 다른 답을 내놓았고, 그 차이가 이 장의 핵심입니다.

왜 공유 CI 러너에서는 벤치마크가 무너지는가

공유 러너에서 벤치마크 시간이 흔들리는 원인은 한 가지가 아니라 여러 층으로 쌓여 있습니다. 가상화 계층에서는 CPU 스텝핑·주파수 스케일링·터보 부스트 여부가 매 실행마다 달라질 수 있고, 같은 물리 호스트를 공유하는 다른 컨테이너의 I/O·캐시 오염이 “노이지 네이버(noisy neighbor)” 효과를 만듭니다. 여기에 CI 프로바이더가 러너 세대를 교체하거나 커널·마이크로코드를 패치하면 코드는 그대로인데 벤치마크 숫자만 이동하는 “환경 회귀(environment regression)“가 섞여 들어옵니다. Bencher 문서는 이런 조건에서 일반 GitHub Actions 러너의 실행 간 변동성이 30%를 넘을 수 있다고 설명하는데, 수 퍼센트 수준의 회귀를 잡으려는 게이트에서는 이 변동성 자체가 신호보다 커서 게이트를 무력화시킵니다. 즉 “벤치마크를 CI에서 자동으로 돌린다"는 챕터 02의 전제가 갖춰져도, 노이즈를 다루는 장치가 없으면 그 결과는 회귀 탐지에 쓸 수 없습니다.

flowchart LR
  prCommit["PR 커밋"] --> ciRunner["일반 공유 CI 러너"]
  ciRunner --> noisySources["노이즈 원인들"]
  noisySources --> cpuScaling["CPU 주파수/터보 변동"]
  noisySources --> noisyNeighbor["노이지 네이버"]
  noisySources --> envDrift["커널/마이크로코드 드리프트"]
  cpuScaling --> unreliableResult["신뢰 불가능한 결과"]
  noisyNeighbor --> unreliableResult
  envDrift --> unreliableResult
  unreliableResult --> approachA["계측 기반 결정론 (CodSpeed)"]
  unreliableResult --> approachB["전용 베어메탈 (Bencher)"]

계측 기반 측정 vs 벽시계 시간 측정

CodSpeed가 택한 첫 번째 축은 “노이즈가 있는 하드웨어에서 시간을 재는 대신, 애초에 하드웨어 상태에 좌우되지 않는 지표를 잰다"는 접근입니다. CPU Simulation(계측 시뮬레이션) 모드는 계측 기반 시뮬레이션으로 프로그램을 실행해 실제 벽시계 시간이 아니라 실행된 명령어 수 같은 하드웨어 상태에 덜 좌우되는 지표를 셉니다. 명령어 수는 CPU 클럭·터보 상태·다른 프로세스의 간섭과 무관하게 같은 코드 경로에서는 항상 같은 값이 나오므로, 공유 러너 위에서 실행해도 “노이즈"라고 부를 만한 것이 거의 없습니다. 대신 이 지표는 실제 지연시간(latency)의 근사치일 뿐입니다 — 분기 예측 실패, 메모리 스톨, syscall 대기 시간처럼 명령어 수에 드러나지 않는 비용은 반영되지 않습니다. 그래서 CodSpeed는 별도로 Walltime 모드도 제공하는데, 이는 실제 실행 시간을 재는 전통적 방식이라 I/O·시스템 효과를 포함하지만, 다시 어느 정도의 러너 간 변동을 감수해야 합니다. 아래는 C++ 프로젝트에서 Google Benchmark로 작성한 벤치마크를 (CodSpeed가 Google Benchmark를 계측하려면 공식 codspeed-cpp의 codspeed-google-benchmark 호환 계층을 CMake나 Bazel로 연결해야 하며, 아래 예시는 그 연결 단계를 생략하고 실행하지 않은 개념 예시입니다) CodSpeedHQ/action의 계측 모드로 PR마다 실행하는 GitHub Actions 워크플로 예시입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# .github/workflows/codspeed.yml
name: codspeed-benchmarks
on:
  push:
    branches: [main]
  pull_request:

jobs:
  benchmarks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure and build benchmarks
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_BENCHMARKS=ON
          cmake --build build --target bench_hotpath
      - name: Run benchmarks with CodSpeed
        uses: CodSpeedHQ/action@v5
        with:
          mode: simulation
          run: ./build/bench_hotpath --benchmark_format=json

이 워크플로가 하는 일은 빌드까지는 챕터 02의 자동 실행 파이프라인과 동일하지만, 마지막 단계에서 실행 파일을 직접 부르는 대신 CodSpeed 액션에 위임한다는 점이 다릅니다. 액션은 bench_hotpath 실행을 계측 하네스로 감싸 명령어 수를 추출하고, 이를 같은 PR의 기준 브랜치 결과와 자동으로 비교해 리포트를 답니다. 어떤 실패 조건에서 머지를 막을지는 챕터 04(PR 성능 게이트)의 정책 문제입니다.

CodSpeed 2026: 계측을 넘어선 기능 확장

CodSpeed의 2026년 변경 이력을 보면 계측 기반 측정이라는 기본 축 위에 몇 가지 실무적인 확장이 얹혔음을 알 수 있습니다.

  • 메모리 계측(Memory Instrument, 2026년 1월): 실행 시간이 아니라 힙 할당 패턴을 추적하는 계측 모드로, 할당·해제·피크 메모리 소비를 추적하는 데 초점을 둡니다. 이는 앞서 명령어 수 계측이 놓치는 축(연산량이 아니라 메모리 압력)을 별도로 채우는 보완 지표입니다.
  • AI Wizard(2026년 3월 24일): CodSpeed 변경 이력에는 GitHub에서 @codspeedbot을 멘션해 Wizard에게 직접 요청하는 기능이 이 날짜로 올라와 있으며, 성능 데이터를 분석하는 도우미로 소개됩니다(수정 제안 등 세부 동작은 이 글이 확인하지 않았습니다).
  • MCP 서버(2026년 3월): 플레임그래프 조회, 두 실행 간 비교 리포트 생성, 실행 상세 조회, 최근 실행 목록, 저장소 목록까지 다섯 개 도구를 AI 코딩 어시스턴트에 노출해, “이 함수가 느려졌다"는 관찰에서 원인 함수 특정까지 에이전트가 직접 조회할 수 있게 합니다.
  • 환경 변화 탐지(2026년 7월): 비교 화면에 두 실행 사이의 툴체인 버전·링크된 라이브러리·CPU 플래그 차이를 함께 표시해, 코드 변경 때문에 느려진 것인지 빌드 환경이 바뀌어서 느려진 것인지를 구분할 단서를 제공합니다.

이 확장들의 공통점은 “계측 기반 결정론적 숫자"라는 CodSpeed의 원래 강점을 유지한 채, 그 숫자 하나만으로는 답할 수 없는 질문들(메모리는 어떤가, 왜 느려졌는가, 환경 탓인가 코드 탓인가)에 부가 데이터를 붙였다는 점입니다. 세부 항목과 날짜는 CodSpeed 공식 변경 이력에서 그대로 확인할 수 있습니다. 다만 AI Wizard와 MCP 서버는 외부 서비스에 쌓인 성능 데이터에 접근하는 기능이므로, 어떤 데이터가 어디까지 나가는지는 도입 전에 공식 문서로 확인하고 사내 정책과 맞춰봐야 합니다.

Bencher의 베어메탈 동일성 벤치마킹

Bencher가 택한 두 번째 축은 계측으로 노이즈를 우회하는 대신, 노이즈의 물리적 원인 자체를 없애는 접근입니다. 핵심 주장은 “로컬에서 벤치마크를 돌릴 때 쓰는 것과 정확히 같은 베어메탈 하드웨어를 CI에서도 그대로 제공한다"는 것입니다. 가상화 계층·공유 테넌트·러너 세대 교체가 없는 전용 물리 서버에서 실행하면, CPU 주파수 스케일링을 고정하고 노이지 네이버를 원천적으로 배제할 수 있어 벽시계 시간 그대로를 재도 변동성을 낮게 유지할 수 있습니다. Bencher의 연속 벤치마킹 설명 문서는 Bare Metal 러너에서 실행 간 변동성을 2% 미만으로 유지한다고 설명하는데, 이는 앞서 언급한 일반 GitHub Actions 러너의 30%대 변동성과 대비됩니다. 이 접근의 실질적 이점은 Google Benchmark, Criterion, hyperfine 등 팀이 이미 쓰던 벤치마크 하네스를 그대로 쓸 수 있다는 점입니다 — 계측 모드로 다시 작성할 필요 없이, bencher run CLI가 기존 실행 파일을 감싸 벽시계 시간을 기록하고 웹 콘솔에 브랜치·테스트베드·벤치마크·측정 지표 기준으로 추적합니다.

1
2
3
4
5
6
7
8
# Bencher CLI로 기존 Google Benchmark 실행 파일을 CI에서 추적
bencher run \
  --project my-hotpath-service \
  --branch "$GITHUB_HEAD_REF" \
  --testbed bare-metal-x86-01 \
  --adapter cpp_google \
  --err \
  "./build/bench_hotpath --benchmark_format=json"

이 CLI 어댑터 목록과 각 언어별 예시는 Bencher의 C++/Google Benchmark CI 연동 가이드에 정리되어 있습니다. --adapter cpp_google은 Google Benchmark의 JSON 출력 형식을 파싱하도록 지정하고, --testbed는 같은 하드웨어 프로파일을 가리키는 식별자여서 기준선과 비교할 때 “정말 같은 기계에서 잰 결과끼리 비교하는지"를 보장하는 역할을 합니다. --err 플래그는 회귀로 판정되면 CLI가 0이 아닌 종료 코드를 반환하게 해 CI 잡을 실패시킵니다. 이 글의 bencher run 명령은 실행해 보지 않은 예시이므로 플래그는 공식 CLI 문서로 확인해야 하며, 셀프호스팅 가능 여부와 라이선스·요금 조건도 이 글이 확인하지 않았습니다. 다만 셀프호스팅을 택하면 전용 물리 서버를 실제로 확보·유지보수해야 하므로, “베어메탈 동일성"이라는 이점은 공짜가 아니라 인프라 운용 부담과 맞바꾸는 것임을 분명히 해야 합니다.

흔한 오개념

“CI에서 벤치마크를 자동으로 돌리기만 하면 회귀를 잡을 수 있다”는 챕터 02 수준의 자동화와 이 장에서 다루는 노이즈 통제를 혼동하는 오개념입니다. 자동 실행은 필요조건일 뿐이고, 노이즈가 신호보다 큰 상태에서는 게이트가 우연히 통과·실패를 반복하는 “가짜 안정성"만 만듭니다.

“계측이 셈한 명령어 수가 곧 실제 지연시간이다”도 흔한 착각입니다. 명령어 수는 CPU 클럭·메모리 스톨·분기 예측 실패와 무관하게 동일하므로 결정론적이지만, 그 자체가 사용자가 체감하는 밀리초 단위 지연시간과 선형 관계라는 보장은 없습니다. 명령어 수가 줄었는데도 캐시 미스가 늘어 실제 지연시간이 늘어나는 경우가 있으므로, 계측 지표와 실제 지연시간 지표(Walltime, 프로덕션 관측치)를 함께 봐야 합니다.

“베어메탈이면 노이즈가 완전히 0이다”도 과장입니다. Bencher가 주장하는 2% 미만이라는 수치도 0은 아니며, 전용 하드웨어라도 열 스로틀링·펌웨어 업데이트·디스크 마모 같은 물리적 변화는 여전히 존재합니다. “노이즈가 크게 줄어든다"와 “노이즈가 없다"는 다른 주장이므로, 베어메탈 환경에서도 여전히 통계적 임계값(예: 표준편차 기반 밴드)을 두는 것이 안전합니다.

판단 기준

상황권장비권장
오픈소스·개인 프로젝트, 언어별(Python/Rust/JS/C++) 표준 하네스로 빠르게 시작CodSpeed 계측 모드처음부터 자체 베어메탈 인프라 구축
I/O·syscall 비중이 큰 코드 경로의 실제 지연시간 검증Walltime 모드 또는 Bencher 베어메탈명령어 수 계측 단독
이미 Google Benchmark/Criterion으로 작성된 벤치마크 자산이 많음Bencher(하네스 그대로 재사용)계측 하네스로 전면 재작성
사내 코드 반출 정책이 엄격해 서드파티 SaaS 전송이 불가자체 베어메탈 + 셀프호스팅 가능 여부는 공식 문서로 확인CodSpeed SaaS, AI Wizard/MCP 자동 연동
과도한 할당·피크 메모리가 핫패스의 주 의심 지점CodSpeed 메모리 계측벽시계 시간 지표만으로 판단
PR당 비용·러너 대기 시간을 최소화해야 함계측 기반(공유 러너에서도 신뢰 가능)베어메탈 전용 러너 신규 도입

비판적 시각: 한계와 트레이드오프

계측 기반 측정과 베어메탈 동일성은 각각 다른 종류의 리스크를 팀에 들여옵니다. 계측 기반 접근은 서드파티 SaaS에 대한 의존을 만들어, 그 서비스가 장애를 겪거나 가격 정책을 바꾸면 머지 게이트 자체가 흔들리는 새로운 단일 실패 지점이 됩니다. AI Wizard·MCP 서버처럼 코드와 실행 메타데이터를 자동으로 외부로 보내는 기능은 편의성이 크지만, 그만큼 감사(audit) 대상이 넓어지므로 규제 산업이나 보안 민감 조직에서는 기능별로 개별 승인을 받아야 합니다. 또한 명령어 수 계측은 CPU 바운드 코드에는 강하지만, 락 경합·네트워크 대기·디스크 I/O가 지배적인 코드 경로에서는 실제 사용자 체감 지연시간과 괴리될 수 있어 반드시 Walltime이나 프로덕션 관측 지표로 교차 검증해야 합니다. 베어메탈 접근은 반대로 물리 인프라 운용 부담을 팀에 되돌려주는데, 셀프호스팅을 택하면 서버 조달·전력·장애 대응까지 떠안아야 하고, 클라우드 베어메탈을 쓰더라도 공유 러너 대비 비용 부담을 따져 봐야 합니다. 어느 쪽을 택하든 “도구가 노이즈를 없애준다"는 말을 문자 그대로 믿기보다, 자신의 코드 경로가 계측 친화적인지(연산 위주인지), 팀이 감내할 수 있는 인프라·비용 부담이 얼마인지를 먼저 판단해야 합니다.

마무리

이 장을 마치면 다음을 스스로 점검할 수 있어야 합니다.

  • 공유 CI 러너에서 벤치마크 시간이 흔들리는 원인(CPU 스케일링, 노이지 네이버, 환경 드리프트)을 설명할 수 있다.
  • 계측 기반 측정(CodSpeed)과 벽시계 시간 측정의 차이, 그리고 각각이 놓치는 것을 설명할 수 있다.
  • CodSpeed의 2026년 기능 확장(메모리 계측, AI Wizard, MCP 서버, 환경 변화 탐지)이 각각 어떤 문제를 보완하는지 말할 수 있다.
  • Bencher가 베어메탈 동일성으로 노이즈를 줄이는 원리와 그 비용을 설명할 수 있다.
  • 자신의 팀 상황(코드 특성, 보안 정책, 인프라 여력)에 맞춰 계측 기반·베어메탈·자체 러너 중 하나를 선택할 근거를 제시할 수 있다.

다음 장에서는 이렇게 신뢰할 수 있게 된 측정 결과를 실제로 PR 머지 여부에 연결하는 정책 — 허용 오차, 실패 시 대응, 예외 처리 — 을 다룹니다.

→ PR 성능 게이트 (챕터 04)