본문 바로가기
프로그래밍

GitHub Actions 러너로 VPS 사양 재는 법: 36% 편차와 측정 함정

by me_in_sk 2026. 8. 9.
반응형
이미지 편집 엔진

GitHub Actions 러너로 VPS 사양 재는 법: 36% 편차와 측정 함정

분모 오염, 재본 적 없는 구간, 그리고 성공을 실패로 표시하는 신호
핵심 요약
  • 서버를 사기 전에 사양을 정하려면 배포 아키텍처에서 재야 한다. CI 러너를 쓰면 돈을 안 들이고 상대 비교를 얻을 수 있지만, 절대 시간은 못 믿는다.
  • 같은 코드가 8,004ms와 10,927ms로 나왔다(36% 차이). 한 번만 쟀다면 최적화 효과와 러너 상태를 구분하지 못했을 것이다.
  • 동시성 배율이 한 번 1.30배로 튀었다. 그것만 보면 "병렬 처리가 이득"이라는 정반대 결론이 나온다. 네 번 중 세 번은 1.00배였다.
  • 더 위험한 건 편차가 아니라 재고 있다고 믿었지만 안 재고 있던 구간이었다. 측정 도구가 값을 계산한 뒤 마지막에 버리거나, 애초에 대상 경로를 태우지 않는 일이 각각 한 번씩 있었다.

1.역설: 사양을 정하려면 서버가 필요하고, 서버를 사려면 사양이 필요하다

이미지 처리 서비스는 CPU를 많이 쓴다. 그래서 VPS를 얼마짜리로 살지 정하려면 처리 시간과 메모리를 알아야 하는데, 그걸 알려면 서버가 있어야 한다.

빠져나가는 길은 공개 CI 러너다. 배포 대상과 같은 x86_64이고 무료다. 다만 러너는 공유 vCPU에 2코어다. 그래서 처음부터 규칙을 정했다 — 절대 시간이 아니라 배율로 읽는다. 백엔드 A와 B의 비, 콜드와 웜의 비, 동시 실행과 순차 실행의 비.

이 규칙으로 걸러낸 것이 있고, 이 규칙을 지키고도 놓친 것이 있다.

2.편차: 같은 코드가 36% 벌어졌다

성능 개선 후 효과를 확인하려고 두 번 측정했다. 코드는 동일하다.

측정 시점감지 요청 warmrun 내 min-max
개선 전 (2회)16,369 / 16,034ms2% 이내
개선 후 run A8,004ms1% 이내
개선 후 run B10,927ms1% 이내
개선 후 run C7,333ms0.7%

주목할 점은 run 안에서는 안정적이라는 것이다. 같은 run에서 3회 반복하면 1% 이내로 모인다. 벌어지는 건 run 사이다. 러너 인스턴스가 바뀌고, 이웃 부하가 바뀌기 때문이다.

감지 요청 warm — run별 굵은 선 = 한 run 안 3회 반복 범위 개선 전 A16,369ms개선 전 B16,034ms개선 후 A8,004ms개선 후 B10,927ms개선 후 C7,333ms같은 코드 · 36% 차이 run 안에서는 1% 이내로 모인다. 벌어지는 건 run 사이다.
"1% 이내로 안정적"이 주는 착각

한 run의 min-max가 좁으면 측정이 정밀해 보인다. 그건 같은 조건에서의 재현성일 뿐 조건이 바뀌었을 때의 안정성이 아니다. run 내 편차와 run 간 편차는 다른 값이고, 사양 결정에 필요한 건 후자다.

개선 자체는 확실하다 — 세 번 다 16초대보다 한참 아래다. 하지만 "2.05배 빨라졌다"고 단정하면 안 됐다. 실제로 그렇게 보고했다가 다음 run에서 정정해야 했다.

3.단발 값이 결론을 뒤집을 뻔한 순간

동시 처리량을 재는 시나리오가 있다. 순차 1건 × 작업자 수 ÷ 동시 배치 총시간으로 배율을 낸다. 1.0이면 줄을 서는 것, 작업자 수에 가까우면 진짜 병렬이다.

작업자run 1run 2run 3run 4
20.99x0.99x1.30x0.99x
41.00x0.96x1.28x1.01x

run 3만 봤다면 "동시 실행이 30% 이득이니 작업자를 늘리자"가 된다. 실제로는 작업자를 늘려도 처리량은 그대로이고 메모리만 작업 하나당 0.7~1.0GB 늘어난다.

왜 튀었는지도 설명이 된다. 배율의 분모는 그 run에서 잰 순차 1건 시간이다. 순차 측정 구간에 이웃 부하가 몰려 기준값이 부풀면, 배치가 상대적으로 빨라 보인다. 비율로 읽으면 안전하다는 규칙에도 구멍이 있다 — 분자와 분모를 다른 시점에 재면 그렇다.

쓸 만한 방어

같은 run 안에서 A와 B를 번갈아 재고 차분을 취하면 러너 상태가 상당 부분 상쇄된다. 실제로 폰트 추론 비용을 잴 때 이 방식을 썼다 — 같은 run에서 "스타일 추론 켜고"와 "끄고"를 각각 재서 차이만 보고했다. 그 값은 run이 바뀌어도 방향이 일관됐다.

4.더 위험한 실패: 재고 있다고 믿었는데 안 재고 있었다

편차는 눈에 보인다. 보이지 않는 쪽이 두 번 있었다.

① 값을 계산하고 마지막에 버렸다. 시나리오 8개가 전부 정상 수행됐는데 잡이 실패했다. 컨테이너가 비루트 사용자로 돌고 출력 디렉터리는 러너 사용자 소유라, 결과 파일 쓰기에서만 권한 오류가 났다. 수 분씩 걸린 측정이 통째로 사라졌다.

CI 로그text
scenario ocr        … 1분 49초 수행 …
PermissionError: [Errno 13] Permission denied: '/out/ocr.json'

다행히 스크립트가 파일로 쓰기 전에 표준출력으로 결과를 찍고 있어서 로그에서 회수했다. 다만 그 과정에서 또 하나를 배웠다 — 트레이스백이 표준에러로 나가면서 병합 로그에서 JSON과 뒤섞여 파싱이 깨졌다. 이후 쓰기 실패는 트레이스백 대신 한 줄 오류로 내도록 고쳤다.

② 대상 경로를 아예 태우지 않았다. 폰트 추론을 다른 단계로 옮긴 뒤 그 비용이 어디에 붙었는지 재려 했다. 그런데 옮겨간 쪽 수치가 1.5%밖에 안 늘었다. 옮겼는데 비용이 사라졌을 리 없다.

측정 스크립트 (문제)python
CleanupRegionSpec(
    x=..., y=..., width=..., height=...,
    # text가 빠져 있다 — 이 필드의 존재가
    # 스타일 추론을 요청하는 신호인데
)

측정 도구가 새 경로를 요청하지 않고 있었다. 수치는 변경 전과 같은 것을 재고 있었다. 고친 뒤 같은 run에서 요청 유무를 각각 재니 차이가 드러났다 — 영역당 804ms, 이후 최적화로 253ms.

0을 보고하는 측정은 실패시켜야 한다

고치면서 스크립트에 조건을 넣었다 — 스타일이 하나도 추론되지 않으면 실패. 측정 도구가 조용히 0을 보고하면 "비용이 없다"로 읽히고, 같은 착각이 반복된다. 측정 대상이 실제로 실행됐는지 스스로 확인하지 않는 도구는 언제든 이렇게 거짓말한다.

5.거짓 신호는 반대 방향으로도 난다

반대 방향의 거짓 신호도 있었다. 시나리오 8개가 전부 성공하고 요약 표까지 정상 출력됐는데 잡이 빨간불이었다.

CI 로그text
##[error] Failed to CreateArtifact: Artifact storage quota has been hit.

계정의 아티팩트 저장 용량이 찼을 뿐이다. 측정은 멀쩡했다. 그런데 잡이 확인하려는 바로 그것에 대해 거짓 신호를 내고 있었다.

업로드 단계를 지웠다. 표는 잡 요약에 남고 원본 JSON은 로그에 찍히므로 잃는 데이터가 없다 — 앞서 권한 오류 때 로그만으로 전부 회수한 게 그 증거였다. 세 번의 실행 동안 아티팩트를 실제로 열어 본 적이 한 번도 없기도 했다.

6.그래서 무엇을 믿었나

확정근거
메모리 상한peak RSS가 같은 구성 안에서 편차 1% 이내(최적화 전 2,403·2,401MB / 후 2,077·2,078·2,083MB). 시간이 36% 흔들리는 동안에도 메모리는 흔들리지 않았다
작업자 수는 1네 번 중 세 번이 1.00배. 튄 한 번은 분모 오염으로 설명됨
최적화 효과의 방향세 run 모두 개선 전보다 한참 아래. 배수는 범위로만 말함
미확정이유
vCPU 수러너가 2코어라 스레드 4개는 원리상 이득을 낼 수 없다. 평평하게 나온 건 측정 설계의 한계이지 결론이 아니다
절대 지연공유 vCPU. 전용 서버에서 다시 재야 한다

두 번째 표가 이 글의 핵심에 가깝다. "측정했더니 차이가 없었다"와 "차이가 날 수 없는 조건에서 측정했다"는 다른 문장인데, 표에 숫자가 나란히 찍히면 똑같이 생겼다.

7.트레이드오프

비용완화
같은 측정을 두 번 이상 돌린다 — 잡 하나가 이미지 빌드 + 모델 다운로드 + 실제 추론으로 수십 분PR 경로에 올리지 않는다. 사양을 정하거나 최적화 효과를 확인할 때만 수동 실행
편차가 큰 항목은 결론까지 오래 걸린다 — 동시성은 네 번 재고서야 판정했다같은 run 안에서 A/B를 번갈아 재고 차분을 보면 한 번으로 끝나는 항목도 있다
"결론 없음"으로 남는 항목이 생긴다 — 러너가 2코어라 vCPU 수는 끝내 못 정했다못 정했다는 사실 자체를 기록한다. 평평한 표를 "영향 없음"으로 읽는 것보다 낫다

이 방식은 느리다. 한 번 재고 넘어가면 될 것을 두 번 재고, 그러고도 답이 안 나오는 항목이 남는다. 그래도 값이 나오는 지점이 있다 — 서버를 사거나 아키텍처를 바꾸는 결정 앞에서다. 코드 한 줄 고치는 데 이 절차를 붙이면 과하다.

8.정리

공유 러너로 성능을 재는 건 서버 없이 상대 비교를 얻는 좋은 방법이다. 다만 다음을 지켜야 쓸모가 있었다.

  • 같은 조건에서 최소 두 번 — run 내 편차가 아니라 run 간 편차를 봐야 한다.
  • 분자와 분모를 같은 run에서 — 비율로 읽는 규칙도 시점이 어긋나면 깨진다.
  • 측정 도구가 대상을 태웠는지 스스로 확인 — 0을 조용히 보고하면 실패시킨다.
  • 결론을 낼 수 없는 항목은 그렇게 적는다 — 평평한 표가 "영향 없음"을 뜻하지 않을 수 있다.

한 문장으로 줄이면 — 한 번 잰 값은 결과가 아니라 표본 하나다. 그 표본으로 서버를 사거나 아키텍처를 바꾸기 전에, 한 번 더 재는 비용이 대개 훨씬 싸다.

이미지 편집 엔진 · 성능 측정

반응형

댓글