본문으로 건너뛰기

[starlette] 신뢰할 수 있는 성능 측정을 위한 전략: Starlette의 벤치마크 안정화 기법

PR 링크: Kludex/starlette#3462 상태: Merged | 변경: +16 / -6

들어가며

소프트웨어의 성능을 지속적으로 모니터링하는 것은 현대적인 오픈소스 관리의 핵심입니다. 하지만 벤치마크 결과가 실행할 때마다 요동친다면(Flaky Benchmarks), 우리는 성능 개선이 실제 코드 변경 때문인지 아니면 단순히 운이 좋아서인지 구분할 수 없게 됩니다.

최근 Python의 대표적인 ASGI 프레임워크인 Starlette 프로젝트에서는 CodSpeed를 이용한 벤치마크의 안정성을 높이기 위한 중요한 변경사항이 반영되었습니다. 이번 글에서는 프로세스 상태에 따른 민감도를 줄이고, 측정의 일관성을 확보하기 위해 어떤 기법들이 사용되었는지 실제 코드를 통해 살펴보겠습니다.


코드 분석: 무엇이 왜 바뀌었는가?

1. 결정론적 해시 동작을 위한 PYTHONHASHSEED 고정

가장 먼저 눈에 띄는 변화는 CI 환경 설정입니다. Python은 보안상의 이유(Hash DoS 공격 방지)로 실행 시마다 dictset에서 사용하는 해시 시드값을 무작위로 생성합니다. 하지만 벤치마크 환경에서는 이 무작위성이 오히려 독이 됩니다.

Before (.github/workflows/benchmark.yml)

benchmarks:
  name: Run benchmarks
  runs-on: ubuntu-latest

  steps:
    - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"

After (.github/workflows/benchmark.yml)

benchmarks:
  name: Run benchmarks
  runs-on: ubuntu-latest
  env:
    PYTHONHASHSEED: "0"

  steps:
    - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"

PYTHONHASHSEED: "0"을 설정하면 Python은 고정된 시드값을 사용합니다. 이는 해시 테이블의 메모리 레이아웃과 순회 순서를 결정론적으로 만들어, 실행 간의 미세한 성능 차이를 제거하는 데 큰 도움을 줍니다.

2. 전략적 Warmup 도입 (Multipart & URL-encoded)

벤치마크 측정 직전에 실제 워크로드를 한 번 실행해보는 'Warmup' 단계가 추가되었습니다. 이는 CPU 캐시를 채우고, Python의 내부 객체 풀을 활성화하며, 필요한 경우 JIT 컴파일러(PyPy 등을 사용할 경우)가 최적화를 마칠 시간을 벌어줍니다.

Multipart 벤치마크의 변화

Before (benchmarks/multipart_benchmark.py)

CASES = (
    BenchmarkCase("fields-1000", fields=1000),
    BenchmarkCase("file-512KiB", file_sizes=(512 * KiB,)),
    # ...
)

def test_multipart(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
    body = make_body(case)
    chunks = make_chunks(body, case.chunk_size)
    messages = benchmark.pedantic(dispatch, args=(asgi_runner, MultipartApp(case), chunks), rounds=1)

After (benchmarks/multipart_benchmark.py)

CASES = (
    BenchmarkCase("fields-1000", fields=1000),
    # 특정 케이스에 warmup=True 플래그 추가
    BenchmarkCase("file-512KiB", file_sizes=(512 * KiB,), warmup=True),
    BenchmarkCase("mixed", fields=100, file_sizes=(512 * KiB,), warmup=True),
    BenchmarkCase("boundary-like-file", file_sizes=(MiB,), boundary_like=True, warmup=True),
    # ...
)

def test_multipart(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
    body = make_body(case)
    chunks = make_chunks(body, case.chunk_size)
    app = MultipartApp(case)
    # 측정 전 Warmup 실행
    if case.warmup:
        dispatch(asgi_runner, app, chunks)
    messages = benchmark.pedantic(dispatch, args=(asgi_runner, app, chunks), rounds=1)

여기서 주목할 점은 모든 케이스가 아닌, 불안정한 리포트를 생성했던 특정 대용량 케이스(file-512KiB, mixed 등)에만 warmup=True를 적용했다는 것입니다. 불필요한 전체 벤치마크 시간을 늘리지 않으면서도 문제가 되는 지점만 정밀하게 타격한 최적화입니다.

URL-encoded 벤치마크의 변화

동일한 패턴이 urlencoded_benchmark.py에도 적용되었습니다.

After (benchmarks/urlencoded_benchmark.py)

CASES = (
    # ...
    BenchmarkCase("field-1MiB-64KiB", fields=1, field_size=MiB, max_part_size=2 * MiB, warmup=True),
    # ...
)

def test_urlencoded_form(asgi_runner: ASGIRunner, benchmark: BenchmarkFixture, case: BenchmarkCase) -> None:
    # ...
    app = FormApp(case)
    if case.warmup:
        dispatch(asgi_runner, app, chunks)
    messages = benchmark.pedantic(dispatch, args=(asgi_runner, app, chunks), rounds=1)

1MiB 크기의 큰 필드를 처리하는 케이스는 메모리 할당 오버헤드가 크기 때문에, Warmup을 통해 메모리 관리자의 상태를 안정화시킨 후 측정하는 것이 훨씬 정확합니다.


왜 이게 좋은 최적화인가?

1. 노이즈 대 신호 비(Signal-to-Noise Ratio) 개선

벤치마크에서 가장 무서운 것은 '가짜 양성(False Positive)'입니다. 코드가 나빠지지 않았는데도 환경 요인으로 인해 성능이 하락한 것처럼 보이면 개발자는 혼란에 빠집니다. PYTHONHASHSEED 고정은 이러한 환경적 변수를 원천 차단합니다.

2. 측정의 의미 유지

PR 설명에서도 언급되었듯, PayloadChunk 생성 과정은 여전히 측정 범위(benchmark.pedantic) 밖에 있습니다. 즉, 우리가 실제로 측정하고자 하는 'ASGI 디스패치 로직' 자체의 시맨틱은 유지하면서, 그 로직이 실행되는 '무대(Process State)'만 더 깨끗하게 정돈한 것입니다.

3. 실질적인 안정성 확보

단순히 이론적인 접근이 아니라, 실제로 CodSpeed 리포트에서 불안정하게 나타났던 케이스들을 타겟팅하여 warmup을 적용했습니다. 이는 데이터에 기반한 실용적인 엔지니어링의 좋은 예시입니다.

결론

성능 최적화는 단순히 코드를 빠르게 만드는 것만을 의미하지 않습니다. 성능을 측정하는 도구 자체를 신뢰할 수 있게 만드는 것 또한 시니어 엔지니어의 중요한 역량입니다. Starlette의 이번 변경사항은 Python 환경에서 벤치마크를 수행할 때 고려해야 할 표준적인 가이드라인을 잘 보여주고 있습니다.

여러분의 프로젝트에서도 벤치마크 결과가 들쭉날쭉하다면, 해시 시드 고정과 전략적 Warmup 도입을 고려해보시기 바랍니다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글