pmady/keda-gpu-scaler. 쿠버네티스 기본 오토스케일러(HPA)는 CPU·메모리만 볼 수 있어 GPU가 100%로 불타는데도 "CPU 8%니 한가하네"라고 착각합니다. 이 프로젝트는 GPU 노드마다 DaemonSet을 띄워 NVIDIA의 NVML 라이브러리에서 GPU 사용률·VRAM 같은 진짜 하드웨어 지표를 직접 읽고, 그것을 KEDA 외부 스케일러(gRPC) 규격으로 KEDA에 넘겨 vLLM·Triton·학습 잡을 0개까지(scale-to-zero) 자동 증감하게 합니다. 기존 정석인 dcgm-exporter → Prometheus → PromQL → KEDA(컴포넌트 5개·지연 15~30초)를 컴포넌트 2개·지연 2~4초로 줄이는 게 핵심. (저장소: pmady/keda-gpu-scaler · 언어 Go 1.25 · go-nvml(CGO) + gRPC + Prometheus · Helm 차트·CI 포함 · Apache-2.0 · TrendShift 라이브 멘션 #62133)
"방 온도계(CPU)만 보던 보일러 자동조절기에, 오븐(GPU)에 직접 꽂은 온도계를 달아 준다."
쿠버네티스의 기본 오토스케일러 HPA는 CPU·메모리만 봅니다. 그런데 LLM 추론 서버(vLLM)는 요청 200개를 처리하느라 GPU가 100%인데 CPU는 8%인 게 흔합니다. HPA 눈에는 "한가한 파드"라 절대 늘려 주지 않죠. 이 프로젝트는 GPU가 달린 노드마다 작은 감시병(DaemonSet)을 세워, NVIDIA 드라이버의 NVML에서 GPU 사용률·VRAM·온도·전력 같은 진짜 계기판 수치를 읽습니다. 그리고 그 수치를 KEDA가 알아듣는 gRPC 규격으로 넘겨, "GPU 메모리가 80% 넘으면 파드 늘려, 요청 없으면 0개로 꺼"를 자동으로 시킵니다.
좀 더 구체적으로, keda-gpu-scaler는 Go로 만든 두 개의 작은 실행파일입니다. ① keda-gpu-scaler는 GPU 노드에서 도는 gRPC 서버(KEDA가 여기에 "지금 GPU 얼마나 써?"라고 물음), ② gpu-metrics는 쿠버네티스 없이도 터미널에서 GPU 지표를 보는 CLI(SLURM·Flux·맨몸 서버까지 같은 형식으로 출력). 핵심 자랑은 "dcgm-exporter + Prometheus 파이프라인(5개 컴포넌트, 15~30초 지연)을 컴포넌트 2개·2~4초 지연으로 압축"한다는 것 — 즉 이 프로젝트의 진짜 상품은 '단순함과 짧은 지연'입니다.
이 문서가 파고드는 건 사용법만이 아니라 그 안의 설계입니다 — "왜 GPU 지원을 KEDA 본체에 못 넣고 별도 컴포넌트로 뺐나(CGO·하드웨어 접근)", "왜 Deployment가 아니라 DaemonSet인가", "KEDA 외부 스케일러 gRPC 규격(4개 함수)은 정확히 무엇을 약속하나", "scale-to-zero는 코드로 어떻게 구현되나". 쿠버네티스 오토스케일링·gRPC·GPU 시스템 프로그래밍을 처음 공부하는 사람에게 아담하지만 알찬 실전 교본입니다.
"GPU는 시간당 수천 원짜리 자원이다. '안 쓸 때 0개로 끄기'를 빠르고 단순하게 해 주는 게 곧 돈이다."
vLLM·Triton 같은 GPU 추론 서버를 쿠버네티스에 올리면, 부하가 몰릴 때 파드를 늘리고 한가하면 줄이고 싶습니다. 그런데 HPA는 GPU 사용률을 볼 줄 모릅니다. 요청 200개를 처리하느라 GPU가 포화 상태여도 CPU는 8%라, HPA는 "널널하네"라며 파드를 안 늘립니다. 반대로 요청이 끊겨도 GPU 파드는 계속 켜져 있어 시간당 수천 원짜리 GPU가 놀면서 돈만 태웁니다.
지금까지의 표준 해법은 dcgm-exporter(GPU 지표 수집기)가 지표를 뽑고 → Prometheus가 주기적으로 긁어가고(scrape) → PromQL 쿼리로 가공하고 → 그걸 KEDA Prometheus 스케일러가 읽는 4~5단 사슬입니다. 컴포넌트가 5개나 되고, Prometheus의 scrape 주기(보통 15초) 때문에 GPU가 달아오른 뒤 KEDA가 알아채기까지 15~30초가 걸립니다. 갑작스러운 트래픽 폭주에는 너무 느립니다.
keda-gpu-scaler는 GPU 노드에서 NVML을 직접(2초 주기) 폴링해 메모리에 캐시하고, KEDA가 물으면 gRPC로 즉답합니다. 중간의 Prometheus·PromQL·exporter가 통째로 사라져 컴포넌트는 2개(이 스케일러 + KEDA), 지연은 2~4초. PromQL을 쓸 필요도 없이 3줄짜리 ScaledObject로 끝납니다.
이 질문이 프로젝트 전체를 이해하는 열쇠입니다. 개발자는 세 가지 '구조적 제약' 때문에 GPU 지원을 KEDA 코어에 넣는 게 불가능하다고 설명합니다(KEDA 이슈 #7538에서 논의됨).
| 제약 | 내용 |
|---|---|
| ① CGO 충돌 | NVIDIA의 Go 바인딩(go-nvml)은 C 라이브러리를 부르므로 CGO_ENABLED=1이 필수. 그런데 KEDA는 이식성을 위해 CGO_ENABLED=0으로 정적 빌드한다. 둘은 근본적으로 양립 불가. |
| ② 노드 하드웨어 접근 | NVML은 /dev/nvidiactl 같은 물리 GPU 노드의 장치 파일을 통해서만 읽을 수 있다. KEDA 오퍼레이터는 중앙에 1개 떠 있는 Deployment라 워커 노드의 GPU에 손댈 수 없다 → 각 GPU 노드에 도는 DaemonSet만이 정답. |
| ③ 독립 릴리스 | GPU 생태계는 빠르게 변한다. KEDA의 60여 스케일러 릴리스 주기에 묶이면 개선이 느려진다. 별도 컴포넌트면 몇 시간 만에 새 GPU 지표를 추가할 수 있다. |
KEDA 본체 = 본사 중앙 관제실. 회사 전체를 한 곳에서 감시하지만, 멀리 떨어진 공장 기계 내부 온도(GPU)는 직접 못 잽니다. 게다가 본사 컴퓨터에는 공장용 특수 계측 장비 드라이버(CGO/NVML)를 깔 수도 없습니다.
keda-gpu-scaler = 공장마다 배치된 계측 담당자(DaemonSet). 기계에 직접 센서를 꽂아 수치를 읽고, 관제실(KEDA)이 무전(gRPC)으로 물으면 즉답합니다. 담당자를 본사와 따로 교육·교체할 수 있어(독립 릴리스) 유연합니다.
정리하면 이 프로젝트가 주목받는 이유는 네 가지입니다. ① 단순함(Prometheus 파이프라인 제거, 3줄 ScaledObject), ② 짧은 지연(직접 NVML로 2~4초), ③ scale-to-zero(요청 없으면 GPU 파드 0개 → 비용 0), ④ 워크로드별 프로필(vllm-inference·triton-inference·training 등 검증된 기본값 5종 + 멀티 GPU 집계 max/avg/min/sum). 거기에 HPC 보너스로, 같은 코드의 gpu-metrics CLI가 쿠버네티스·SLURM·Flux·맨몸 서버에서 동일한 JSON 형식으로 GPU 지표를 뽑아 온프렘과 클라우드를 같은 도구로 비교하게 해 줍니다.
이 레포는 버전 v0.1~v0.4대의 신생 프로젝트이고 사실상 1인 메인테이너 중심입니다. README의 "2~4초", "30일간 무중단", "94% 토큰" 같은 수치 일부는 개발자가 4노드 A100 클러스터에서 측정·관찰한 값으로, 독립 재현 결과가 아닙니다. 또 GPU 지표 기반 스케일링은 지표가 튀면 파드도 출렁(flapping)일 수 있어, 실제로는 cooldown·activation 임계값 튜닝이 필요합니다. 개념과 코드를 배우기엔 훌륭하지만, 프로덕션 도입 전엔 충분한 부하 테스트가 권장됩니다.
"전부 Go 한 덩어리지만, 진짜 재미는 'Go가 C(NVML)와 손잡는 지점'과 'gRPC로 KEDA와 약속을 맺는 지점'에 있다."
keda-gpu-scaler는 화면(UI)이 없는 순수 백엔드/인프라 도구입니다. 무게중심은 "GPU에서 숫자를 읽어(NVML) → 약속된 규격으로 내보낸다(gRPC)"의 두 다리에 쏠려 있고, 나머지는 관측성·배포·테스트를 위한 견고한 살입니다.
| 기술 | 역할 | 한 줄 설명 |
|---|---|---|
| Go 1.25 | 전체 언어 | 단일 바이너리·고루틴(동시성)·빠른 시작. 폴링 루프와 gRPC 서버를 가볍게 돌린다. |
| NVIDIA/go-nvml (CGO) | GPU 지표 수집 | NVIDIA의 libnvidia-ml.so(NVML) C 라이브러리를 Go에서 호출. 사용률·VRAM·온도·전력·PCIe·NVLink를 읽는다. CGO 필수. |
| google.golang.org/grpc + protobuf | KEDA 인터페이스 | KEDA 외부 스케일러 규격(ExternalScaler 서비스)을 protobuf로 정의·구현. 타입 안전 + 스트리밍 지원. |
| go.uber.org/zap | 구조적 로깅 | JSON 형식 로그(필드 기반). 운영 환경에서 검색·집계하기 좋다. |
libnvidia-ml.so). nvidia-smi 명령이 안에서 쓰는 바로 그 라이브러리입니다. GPU 사용률·메모리·온도·전력·PCIe/NVLink 대역폭을 읽을 수 있고, 읽기 전용 호출이라 장치 설정을 바꾸지 않습니다. C라서 Go에서 쓰려면 go-nvml 바인딩 + CGO 다리가 필요합니다..proto 파일)로 미리 약속하면 코드를 자동 생성해 줍니다. KEDA는 외부 스케일러와 대화할 때 이 gRPC 규격을 쓰기로 정해 뒀습니다. HTTP로 텍스트를 주고받는 것보다 타입이 안전하고(PromQL 문자열 파싱 불필요) 빠르며 스트리밍이 됩니다.| 기술 | 역할 | 설명 |
|---|---|---|
| prometheus/client_golang | 선택적 /metrics | GPU 사용률·메모리·온도·전력 + 스케일러 자체 통계(NVML 호출 수·에러·지연 히스토그램)를 :9090/metrics로 노출. 스케일링 경로와 독립 — 꺼도(--metrics-port=0) 스케일링은 그대로. |
| HTTP 헬스 프로브 | liveness/readiness | /healthz(살아있음, 200)·/readyz(NVML 초기화 + 첫 수집 성공 후 200)를 :8081에 노출. 쿠버네티스가 파드 상태를 판단. |
| gRPC health + reflection | 디버깅 | 표준 gRPC 헬스체크와 reflection(서비스 목록 조회) 등록 → grpcurl로 직접 찔러볼 수 있다. |
| 항목 | 내용 |
|---|---|
| Dockerfile | 2단계 빌드: ① golang:1.26-bookworm에서 gcc로 CGO_ENABLED=1 빌드 → ② 런타임은 nvidia/cuda:13.3.0-base(libnvidia-ml.so 포함) 위에 바이너리만 복사. 비루트 UID(65534)로 실행. |
| Helm 차트 | deploy/helm/에 DaemonSet·Service·ServiceAccount 템플릿 + values.yaml(nodeSelector·tolerations·privileged·metrics/probes 토글·nvidia runtimeClass 등). |
| 생(raw) 매니페스트 | deploy/manifests.yaml — Helm 없이 kubectl apply 한 방으로 Service + DaemonSet 배포. |
| 예제 | deploy/examples/ — vLLM용 ScaledObject, 커스텀 GPU 사용률 ScaledObject. |
| 품질·CI | GitHub Actions(ci·codeql·scorecard·release), golangci-lint, Go Report Card 배지, mkdocs(ReadTheDocs) 문서 사이트 — 오픈소스 위생이 잘 갖춰짐. |
| Makefile | make build / build-metrics / test / test-e2e / lint / proto / docker-release / deploy / helm-test 한 줄 명령 모음. |
같은 코드베이스에 gpu-metrics라는 독립 CLI가 들어 있습니다. 쿠버네티스가 전혀 없어도 베어메탈·SLURM·Flux·싱귤래리티 컨테이너에서 GPU 지표를 table/json/csv로 뽑습니다. 환경을 자동 감지(--env auto)하고, 어디서 돌든 동일한 JSON 스키마(environment 블록 포함)로 출력해 온프렘과 클라우드 GPU 성능을 jq·pandas로 손쉽게 비교하게 합니다.
"GPU 하드웨어 → NVML → DaemonSet(2초 폴링) → gRPC → KEDA → HPA → 파드 개수. 한 줄로 흐른다."
흐름은 일곱 단계입니다. ① DaemonSet이 NVML 폴링 루프를 시작(기본 2초) → ② 매 주기 SM 사용률·메모리·VRAM·온도·전력을 읽어 ③ 메모리에 캐시(디스크·외부 저장소 없음) → ④ KEDA가 gRPC로 GetMetrics() 호출 → ⑤ 스케일러가 ScaledObject에 적힌 집계 방식대로 값을 계산해 응답 → ⑥ KEDA가 그 값을 HPA에 먹임 → ⑦ HPA가 파드를 늘리거나 줄이거나(0개 포함) 결정. 선택적으로 :9090/metrics가 Prometheus용 게이지를 노출하지만 이건 스케일링 경로와 완전히 분리돼 있어 꺼도 무방합니다.
이 프로젝트의 심장은 proto/externalscaler.proto에 정의된 KEDA와의 약속입니다. KEDA가 부를 수 있는 함수는 정확히 네 개입니다.
service ExternalScaler {
rpc IsActive(ScaledObjectRef) returns (IsActiveResponse) {}
rpc StreamIsActive(ScaledObjectRef) returns (stream IsActiveResponse) {}
rpc GetMetricSpec(ScaledObjectRef) returns (GetMetricSpecResponse) {}
rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse) {}
}
// 메타데이터는 map<string, string> 으로 전달 (ScaledObject에 쓴 설정 그대로)
| 함수 | 역할 (코드 기준) |
|---|---|
| IsActive | scale-to-zero의 핵심. 현재 GPU 지표가 activationThreshold를 넘으면 true. 0개로 꺼진 상태에서 true가 되면 KEDA가 파드를 0→1로 깨운다. 반대로 한가하면 false → 0개로 잠재움. |
| StreamIsActive | 위를 스트리밍으로. 폴링 주기(ticker)마다 활성 여부를 push. 끌어오기(pull) 대신 밀어주기(push) 방식. |
| GetMetricSpec | HPA에게 "이 지표 이름은 X, 목표값은 Y"라고 알려 준다(metricName + targetSizeFloat). |
| GetMetrics | 지금 이 순간의 GPU 지표 실제 값을 돌려준다. HPA가 "목표 대비 현재"를 비교해 파드 수를 계산. |
온도조절기(HPA)와 보일러 센서(스케일러)의 대화로 보면: GetMetricSpec은 "목표 온도는 23도야"라고 합의하는 단계, GetMetrics는 "지금 21도"라고 현재값을 보고하는 단계, IsActive는 "사람이 방에 들어왔나?(켤 가치가 있나)"를 묻는 단계입니다. 아무도 없으면(IsActive=false) 보일러를 완전히 꺼 버리는 게 scale-to-zero입니다.
NVML은 /dev/nvidiactl 등 물리 노드의 장치 파일을 통해서만 GPU를 읽습니다. 중앙에 1개 뜨는 Deployment로는 다른 노드의 GPU를 볼 수 없죠. 그래서 모든 GPU 노드에 1개씩 떠야 하는 DaemonSet이 유일한 정답입니다. 매니페스트의 핵심 설정을 보면 의도가 또렷합니다.
# GPU 있는 노드에만 스케줄
nodeSelector:
nvidia.com/gpu.present: "true"
# GPU 노드의 taint를 견뎌 쫓겨나지 않게
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
# NVML이 장치 파일에 접근하려면 권한 + hostPath 마운트 필요
securityContext:
privileged: true
volumeMounts:
- { name: dev-nvidiactl, mountPath: /dev/nvidiactl }
- { name: nvidia-driver, mountPath: /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 }
GPU 읽기는 C(NVML)에 의존하지만, 코드 대부분은 인터페이스 뒤에 숨겨져 있습니다. 핵심은 이 작은 약속입니다.
type MetricsCollector interface {
CollectAll() ([]Metrics, error) // 모든 GPU
CollectDevice(index int) (Metrics, error) // 특정 GPU
DeviceCount() (int, error)
Close() error
}
실제 구현(nvml.go)은 NVML을 호출하지만, 테스트용 mock 구현(mock.go)도 같은 인터페이스를 만족합니다. 덕분에 GPU가 없는 노트북·CI에서도 단위/E2E 테스트가 전부 돌아갑니다 — 스케일러 두뇌(gRPC·집계·프로필)는 GPU 없이 검증하고, 진짜 하드웨어는 클러스터에서만 확인. 이것이 의존성 주입(DI)과 인터페이스 설계의 교과서적 활용입니다.
바이너리는 libnvidia-ml.so를 런타임에 동적 로드합니다. NVIDIA 드라이버가 없는 머신(노트북·일반 CI 러너)에서는 nvml init failed로 즉시 종료됩니다. 빌드도 CGO_ENABLED=1 + C 컴파일러가 없으면 실패합니다. "GPU 없이 되는 건 테스트(mock)뿐"이라는 걸 기억하세요.
사용자가 "raw 지표 임계값"을 직접 정하는 건 실수하기 쉽습니다("vLLM에 GPU 80%가 맞나?"). 그래서 워크로드별 프로필을 미리 만들어 뒀습니다. 스케일러는 ScaledObject의 메타데이터를 parseMetadata()로 읽어 기본값 → 프로필 → 개별 override 순서로 설정을 겹쳐 해석합니다.
| 프로필 | 주 지표 | 목표 / 활성 | 용도 |
|---|---|---|---|
| vllm-inference | 메모리 % | 80 / 5 | vLLM·LLM 서빙(KV 캐시 선점형), scale-to-zero |
| triton-inference | GPU 사용률 | 75 / 10 | Triton 다중 모델 서빙(플래핑 방지로 활성 10) |
| training | GPU 사용률 | 90 / 0 | 학습 잡(체크포인트 보호 위해 0개로 안 끔) |
| batch | 메모리 % | 70 / 1 | 오프라인 배치 추론(공격적 scale-down) |
| distributed-training | NVLink TX | 800 / 100 | NVLink 기반 데이터 병렬 학습 |
멀티 GPU 노드(4~8장)에선 GPU별 값을 하나로 합쳐야 하는데, aggregation으로 max(아무 GPU나 뜨거우면 증설 — 추론 기본값)·avg(평균 — 학습)·min(보수적)·sum(총량 기반)을 고릅니다.
IsActive가 "지표 > 활성임계값"이면 true를 반환해 0→1로 깨웁니다. 목표값(target)이 '몇 개로 늘릴지'라면, 활성값은 '아예 켤지 말지'의 스위치입니다.gpu-metrics CLI는 HPC와 클라우드를 동등하게 노립니다. env.Detect()가 환경변수를 SLURM → Flux → 쿠버네티스 → 맨몸 우선순위로 검사해 자동 판별하고(예: SLURM_JOB_ID 있으면 SLURM), 스케줄러가 배정한 GPU만 골라 수집합니다. 출력은 어디서 돌든 항상 같은 스키마라 파서가 환경별로 분기할 필요가 없습니다.
{ "environment": { "orchestrator": "slurm", "node": "compute-01",
"job_id": "123", "task_rank": 0 },
"collected_at": "2026-06-17T10:00:00Z",
"devices": [ ... ] }
이 "통일된 어휘" 설계 덕에 온프렘 SLURM 클러스터와 클라우드 쿠버네티스의 GPU 성능을 동일한 도구·동일한 JSON으로 비교할 수 있습니다.
"표준 Go 레이아웃(cmd · pkg). 작지만 '실행파일 2개 · 패키지마다 _test.go' 로 군더더기 없이 짜여 있다."
main 진입점(여기선 서버와 CLI 둘), pkg/는 기능별로 나뉜 재사용 가능한 패키지입니다. 이 레포는 gortex처럼 거대하진 않지만, 패키지마다 _test.go가 짝지어 있어(env_test·collector_test·server_test…) 테스트 문화가 탄탄합니다. pkg/externalscaler/*.pb.go는 make proto가 자동 생성하므로 손대지 않습니다.| 묶음 | 대표 위치 | 하는 일 |
|---|---|---|
| 진입점 | cmd/keda-gpu-scaler, cmd/gpu-metrics | 서버 부팅(플래그·로거·gRPC·헬스·프로메테우스 와이어링) / 독립 CLI |
| 스케일러 두뇌 | pkg/scaler | gRPC 4개 함수, ScaledObject 메타데이터 해석, 지표 추출·멀티GPU 집계 |
| GPU 읽기 | pkg/gpu (nvml·mock·collector) | NVML 실제 호출 + 동일 인터페이스의 테스트용 mock |
| 설정·프로필 | pkg/profiles | vllm/triton/training/batch/distributed-training 기본값과 지표 타입 |
| 크로스 환경 | pkg/env, pkg/slurm, pkg/flux | 오케스트레이터 감지 + 잡/GPU 배정 파싱 + 통일 JSON 컨텍스트 |
| 관측성 | pkg/metrics, pkg/probes | Prometheus 계측 래퍼 / liveness·readiness 프로브 |
| 규격·배포 | proto/, deploy/(helm·manifests·examples) | KEDA gRPC 계약 + DaemonSet/Service/ScaledObject 배포물 |
레스토랑으로 보면, pkg/gpu는 재료를 직접 따 오는 텃밭(NVML), pkg/scaler는 주문을 해석해 요리하는 주방(gRPC 응답), pkg/profiles는 정해진 레시피북, deploy/는 가게를 차리는 인테리어 도면(어디에·어떻게 배치할지)입니다. mock.go는 텃밭 없이도 주방을 연습할 수 있는 '모형 재료'라, GPU 없이 주방 로직을 다 테스트하게 해 줍니다.
"이 한 레포 안에 쿠버네티스 오토스케일링 · gRPC · GPU 시스템 프로그래밍 · 인터페이스 설계가 다 들어 있다."
배울 것: KEDA가 "아무 지표로든" 스케일링하게 해 주는 외부 스케일러의 gRPC 4함수 계약(IsActive·StreamIsActive·GetMetricSpec·GetMetrics). 특히 scale-to-zero가 IsActive라는 별도 게이트로 구현된다는 점.
실습: externalscaler.proto를 그대로 가져와, GPU 대신 "랜덤 숫자"나 "현재 분(minute)"을 지표로 돌려주는 미니 외부 스케일러를 Go로 구현하고, ScaledObject로 KEDA에 물려 파드가 늘었다 줄었다 하는지 본다.
배울 것: HPA가 "목표값 대비 현재값"으로 파드 수를 계산하는 원리, KEDA가 그 위에서 어떻게 동작하고 scale-to-zero·cooldown·플래핑 방지(stabilization)를 어떻게 다루는지.
실습: 로컬 kind/minikube에 KEDA를 깔고, CPU 기반 ScaledObject로 부하를 줘서 파드가 0→N→0으로 변하는 과정을 kubectl get hpa,scaledobject -w로 관찰.
배울 것: nvidia-smi 뒤에 있는 NVML로 GPU 사용률·VRAM·온도·전력·NVLink를 읽는 법, 그리고 Go에서 C 라이브러리를 부르는 CGO의 빌드 제약(C 컴파일러·동적 링크).
실습: GPU 머신에서 go-nvml로 디바이스 개수와 각 GPU의 사용률·메모리를 출력하는 30줄짜리 프로그램 작성. nvidia-smi 값과 비교해 단위(MiB·milliwatt) 변환을 확인.
배울 것: 노드 단위 하드웨어를 다루는 쿠버네티스 패턴 — DaemonSet + nodeSelector + toleration + privileged + hostPath, 그리고 파드가 자기 노드/네임스페이스를 아는 Downward API(NODE_NAME·POD_NAME 주입).
실습: 모든 노드에서 호스트의 /proc/loadavg를 읽어 로그로 찍는 간단한 DaemonSet을 작성하고, hostPath 마운트와 nodeSelector를 직접 붙여 본다.
배울 것: .proto → 코드 생성(protoc) → 서버 구현 → 헬스체크·reflection 등록까지의 전체 흐름. grpcurl로 살아있는 gRPC 서버를 직접 찔러 보는 디버깅법.
실습: 이 레포의 서버를 mock으로 띄우고(make test-e2e 경로 참고), grpcurl -plaintext localhost:6000 externalscaler.ExternalScaler/GetMetrics로 응답을 받아 본다.
배울 것: MetricsCollector 인터페이스 하나로 실물(NVML)과 모형(mock)을 갈아끼우는 의존성 주입 패턴. "GPU 없는 CI에서도 핵심 로직을 100% 테스트"하는 비결이자, 좋은 Go 설계의 정석.
실습: 내 프로젝트의 외부 의존(파일·HTTP·DB) 한 곳을 인터페이스로 추출하고, 진짜 구현과 fake 구현을 만들어 단위 테스트를 fake로 돌려 본다.
배울 것: 컬렉터를 감싸서(decorator) 호출 수·에러·지연을 자동 기록하는 Prometheus 계측 패턴, 그리고 /healthz(liveness)·/readyz(readiness)의 의미 차이(살아있음 vs 트래픽 받을 준비됨).
실습: 임의의 함수를 감싸 "호출 횟수 카운터 + 실행시간 히스토그램"을 기록하는 래퍼를 만들고 /metrics로 노출해 본다.
"NVIDIA GPU + 드라이버는 필수. 대신 스케일러 자체는 노드당 ~20Mi RAM의 깃털 같은 무게다."
| 항목 | 요구사항 / 메모 |
|---|---|
| 쿠버네티스 | NVIDIA GPU 워커 노드가 있는 클러스터(OKE·GKE·EKS·AKS 등) + KEDA v2.10+ 설치. |
| GPU 드라이버 | NVIDIA 드라이버 + Device Plugin 필수. 드라이버가 libnvidia-ml.so를 제공한다. 노드에 nvidia.com/gpu.present=true 라벨 필요. |
| CGO (빌드) | CGO_ENABLED=1 + C 컴파일러(gcc) 필요 — go-nvml이 C 라이브러리라서. 순수 Go 크로스 컴파일이 까다로운 지점. |
| 런타임 의존 | 바이너리가 실행 시 libnvidia-ml.so를 동적 로드. NVIDIA 드라이버 없는 머신에선 즉시 종료(nvml init failed). |
| 권한 | DaemonSet은 privileged + runAsUser:0으로 장치 파일에 접근(또는 nvidia runtimeClass 사용). hostPath로 /dev/nvidiactl·/dev/nvidia-uvm·NVML 라이브러리 마운트. |
| 리소스(스케일러) | 매우 가벼움 — CPU 50~200m, 메모리 ~18Mi(1 GPU)~22Mi(8 GPU). 디스크·영구저장소 0. NVML 호출은 GPU당 ~0.3ms. |
| 포트 | 6000 gRPC(KEDA), 9090 Prometheus(선택), 8081 헬스 프로브. |
| 외부 서비스 | 없음. Prometheus·dcgm-exporter 없이도 스케일링 동작(Prometheus는 선택적 모니터링용). |
NVML 장치 접근을 위해 privileged 컨테이너를 띄우는 건 보안상 가벼운 결정이 아닙니다. 다만 이 스케일러는 읽기 전용 NVML 호출만 하고, gRPC 포트는 클러스터 내부 ClusterIP로만 노출되며, 비밀정보·외부 RBAC가 필요 없습니다. 가능하면 hostPath 직접 마운트 대신 nvidia runtimeClass로 장치 마운트를 위임하는 편이 더 깔끔합니다(values.yaml에 토글 있음).
"GPU 없이 코드부터 → 클러스터에 KEDA 깔기 → ScaledObject 붙이기 → 직접 스케일러 만들기, 단계별로."
proto/externalscaler.proto로 KEDA의 4함수 계약을 눈에 익히고, make test/make test-e2e를 실행한다(GPU 불필요 — mock 컬렉터 사용). pkg/scaler/server_test.go에서 "메타데이터 → 지표 추출 → gRPC 응답" 경로가 어떻게 검증되는지 따라간다.
목표: 하드웨어 없이도 핵심 로직이 전부 테스트된다는 걸 체감. mock이 인터페이스를 어떻게 만족시키는지 확인.
kind/minikube에 KEDA를 설치하고, CPU 기반 ScaledObject로 부하를 줘 파드가 0 → N → 0으로 변하는 걸 본다. kubectl get scaledobject,hpa,pods -w로 실시간 관찰.
목표: "외부 스케일러"를 붙이기 전에 KEDA·HPA·scale-to-zero의 기본 동작을 몸에 익히기. cooldownPeriod를 바꿔 가며 반응 속도 비교.
GPU 클러스터에 이 스케일러를 배포(kubectl apply -f deploy/manifests.yaml 또는 Helm)하고, deploy/examples/vllm-scaledobject.yaml처럼 profile: "vllm-inference" 한 줄로 vLLM 파드에 오토스케일링을 건다. 부하를 주며 GPU 메모리% 변화에 따라 파드가 느는지 확인.
목표: 프로필 → 실제 스케일링 동작 연결. 그다음 profile 대신 metricType: gpu_utilization + targetValue + aggregation: avg로 raw 설정도 시도해 차이를 본다.
이 레포의 proto를 재사용해, GPU 대신 임의 지표(예: 외부 API의 큐 길이, 또는 현재 시각)를 돌려주는 외부 스케일러를 Go로 작성한다. cmd/keda-gpu-scaler/main.go의 gRPC·헬스·graceful shutdown 와이어링을 본보기로 삼는다.
목표: "GPU"라는 껍데기를 벗기면 결국 KEDA 외부 스케일러 패턴이 남는다는 걸 이해. 내 비즈니스 지표로 쿠버네티스를 자동 스케일하는 길을 연다.
pkg/gpu(NVML 수집)·pkg/profiles(지표 타입)·pkg/scaler(extractMetric/aggregate)를 읽고, 새 지표(예: NVLink 합산 방식 개선, 또는 ROADMAP의 MIG per-instance 지표)나 새 집계 전략(예: p95)을 추가한다. mock으로 단위 테스트까지.
목표: 수집 → 추출 → 집계 → gRPC 응답의 데이터 경로를 끝까지 손으로 확장. NVIDIA MIG(GPU 분할)나 AMD ROCm 같은 미래 과제에 기여할 준비.
"쿠버네티스 오토스케일링에서 출발해 gRPC·GPU 시스템·관측성까지, 이 레포를 길잡이 삼아."
| 주차 | 주제 | 핵심 학습 + 이 레포 연결점 |
|---|---|---|
| 1주 | 쿠버네티스 + HPA 기초 | 파드·디플로이먼트·서비스, HPA의 목표값 기반 스케일링 원리 → "왜 HPA는 GPU를 못 보나" |
| 2주 | KEDA · ScaledObject · 스케일러 | 이벤트 기반 오토스케일링, scale-to-zero, 트리거 종류 → deploy/examples ScaledObject 읽기 |
| 3주 | Go에서 gRPC + protobuf | .proto → 코드생성 → 서버 구현, 헬스/reflection → proto/ + pkg/scaler |
| 4주 | 외부 스케일러 계약 — 직접 구현 | IsActive/GetMetricSpec/GetMetrics의 의미, 메타데이터 해석 → pkg/scaler/server.go |
| 5주 | NVML · CGO · DaemonSet 하드웨어 | GPU 지표 읽기, CGO 빌드, nodeSelector/toleration/hostPath/Downward API → pkg/gpu + deploy/manifests.yaml |
| 6주 | 관측성 + Helm 패키징 + 배포 | Prometheus 계측·헬스 프로브, Helm 차트 작성, 2단계 Docker(CGO) 빌드 → pkg/metrics·probes + deploy/helm + Dockerfile |
이 로드맵은 "쿠버네티스 운영 → 이벤트 기반 자동화 → 분산 통신(gRPC) → 하드웨어 시스템 → 프로덕션 패키징"을 한 줄로 꿴 코스입니다. 클라우드 네이티브 인프라 엔지니어가 갖춰야 할 핵심 축을 작고 읽기 쉬운 실전 레포 하나로 훑을 수 있어, MLOps·플랫폼 엔지니어링 입문에 특히 좋습니다.
"이 문서와 저장소에서 반복되는 용어를 한곳에 모았다."
| 용어 | 의미 |
|---|---|
| HPA | 쿠버네티스 기본 오토스케일러. CPU·메모리 기준으로 파드 개수를 가로로 증감. GPU는 못 봄 |
| KEDA | 이벤트/임의 지표 기반 오토스케일링 확장(CNCF). scale-to-zero 지원, 60여 스케일러 내장 |
| 외부 스케일러 (external scaler) | KEDA에 gRPC 서버로 끼워 넣는 커스텀 지표 공급자. 이 프로젝트가 그 서버 |
| ScaledObject | "무엇을·어떤 지표로·몇 개까지 스케일링할지" 정의하는 KEDA 커스텀 리소스(YAML) |
| scale-to-zero | 일이 없을 때 파드를 0개까지 줄이는 것. 비싼 GPU 비용을 0으로 만드는 핵심 기능 |
| activation threshold | 0개에서 다시 켤지를 정하는 임계값. IsActive가 "지표 > 활성값"이면 0→1로 깨움 |
| NVML | NVIDIA 드라이버의 GPU 상태 조회 C 라이브러리(libnvidia-ml.so). nvidia-smi의 내부 |
| go-nvml / CGO | NVML의 Go 바인딩 / Go에서 C를 부르는 다리(CGO_ENABLED=1 + C 컴파일러 필요) |
| gRPC / protobuf | 타입 안전한 고성능 RPC / 그 입출력 스키마를 정의하는 .proto. KEDA-스케일러 통신 규격 |
| DaemonSet | 해당 노드마다 파드를 정확히 1개씩 띄우는 워크로드. 노드 단위 하드웨어 접근에 필수 |
| nodeSelector / toleration | "GPU 노드에만 배치" / "GPU 노드의 taint를 견딤". GPU 노드에 스케줄되게 하는 짝 |
| privileged / hostPath | 장치 접근 권한 / 호스트의 장치·라이브러리를 컨테이너로 마운트하는 통로 |
| Downward API | 파드가 자기 노드명·파드명·네임스페이스를 환경변수로 알게 해 주는 쿠버네티스 기능 |
| dcgm-exporter | NVIDIA의 GPU 지표 수집기. 기존 정석(→Prometheus)의 출발점. 이 프로젝트가 제거 대상으로 삼음 |
| Prometheus / PromQL | 지표 수집·저장 시스템 / 그 질의어. 기존 파이프라인의 지연(scrape 15s)·복잡성의 원인 |
| vLLM / Triton | 대표적 GPU 추론 서버. vLLM은 KV 캐시 선점형(메모리 기준 스케일), Triton은 다중 모델(사용률 기준) |
| SM 사용률 | GPU 연산 코어(Streaming Multiprocessor)의 사용률 %. "GPU가 얼마나 바쁜가"의 핵심 지표 |
| VRAM / memory % | GPU 비디오 메모리 사용량/비율. vLLM처럼 메모리를 선점하는 워크로드의 스케일 기준 |
| NVLink / PCIe 처리량 | GPU↔GPU(NVLink)·CPU↔GPU(PCIe) 데이터 대역폭. 분산 학습 스케일 신호로 활용 |
| aggregation | 멀티 GPU 노드에서 GPU별 값을 하나로 합치는 방식: max·avg·min·sum |
| profile | 워크로드별로 검증된 지표·목표·활성값 묶음(vllm/triton/training/batch/distributed-training) |
| MIG | NVIDIA Multi-Instance GPU — 한 GPU를 여러 인스턴스로 쪼개는 기술(ROADMAP의 향후 지원 대상) |
| liveness / readiness 프로브 | /healthz(살아있음) · /readyz(NVML 초기화+첫 수집 성공해 트래픽 받을 준비됨) |
| SLURM / Flux | HPC 클러스터의 잡 스케줄러. gpu-metrics CLI가 자동 감지해 배정된 GPU만 수집 |