부트캠프에서 쿠버네티스의 오토스케일링을 파고들며 정리한 글이다. HPA는 부하에 따라 파드를 늘려 주는 편리한 도구지만, 티켓팅이나 플래시 세일처럼 초당 요청이 순식간에 수만 단위로 뛰는 상황에서는 시스템을 보호하기는커녕 장애를 가속화하는 요인이 되기도 한다. 이 글은 HPA의 동작 원리와 스케일링 알고리즘을 먼저 해부한 뒤, 급증 트래픽 앞에서 반응형 스케일링이 실패하는 세 가지 이유를 분석하고, 그 한계를 KEDA, 비동기 메시지 버퍼링, 가상 대기열, 엣지 인프라 최적화라는 계층적 방어로 어떻게 극복하는지 정리한다.
Workload Resource
자원(Resource)
쿠버네티스(컨테이너 오케스트레이션의 사실상 표준)에는 pod, namespace, service 등 여러 가지 리소스 유형(resource type)이 있으며, 모든 리소스 유형은 종류(kind)라 불리는 정의를 가지고 있는 객체다. 리소스 유형의 인스턴스를 리소스라 부른다.
그림 1. 쿠버네티스 클러스터 아키텍처.
워크로드(Workload)
워크로드는 여러 개의 파드(pod)가 모인 집합 안에서 실행되는 애플리케이션으로, 자신이 속한 집합에 있는 파드들의 생명주기를 관리한다.
워크로드 리소스의 종류
쿠버네티스는 다음과 같은 워크로드 리소스를 제공한다.
- Deployment, ReplicaSet
- StatefulSet
- DaemonSet
- Job, CronJob
Horizontal Pod Autoscaler
수평적 확장(Horizontal Scaling)
수평적 확장이란 서버에 걸리는 부하를 분산시키기 위해 서버를 확장하는 방법 중 하나다. 수평적 확장의 목적은 서버의 사양을 높이는 대신 서버를 하나 더 연결해, 부하가 더 많은 서버로 분산되도록 하는 것이다.
그림 2. 수평적 확장의 개념.
쿠버네티스에서는 HPA(Horizontal Pod Autoscaler)를 통해 수평적 확장을 구현한다.
HorizontalPodAutoscaler
HPA는 수평적 확장을 통해 부하를 분산시키고 시스템 처리량을 증가시키는데, 이때 Deployment나 StatefulSet과 같은 워크로드 자원을 자동으로 갱신한다.
그림 3. HPA가 워크로드 자원을 갱신하는 구조.
쿠버네티스에서 HPA에 해당하는 자원의 유형은 HorizontalPodAutoscaler이며, HorizontalPodAutoscaler는 클러스터 안에서 지속적으로 실행되는 데몬 프로세스가 아니라 kube-controller-manager 컴포넌트 내에서 간헐적으로 실행되는 컨트롤 루프(control loop) 형태로 구현되어 있다. 실제 구현(kube-controller-manager의 컨트롤러 등록 코드)의 핵심부는 다음과 같다.
func newHorizontalPodAutoscalerControllerDescriptor() *ControllerDescriptor {
return &ControllerDescriptor{
name: names.HorizontalPodAutoscalerController,
aliases: []string{"horizontalpodautoscaling"},
constructor: newHorizontalPodAutoscalerController,
}
}
func newHorizontalPodAutoscalerController(ctx context.Context, controllerContext ControllerContext, controllerName string) (Controller, error) {
// ... (중략: HPA 클라이언트, scale 클라이언트, 리소스·커스텀·외부 메트릭 클라이언트 초기화)
pas := podautoscaler.NewHorizontalController(
ctx,
hpaClient.CoreV1(),
scaleClient,
hpaClient.AutoscalingV2(),
controllerContext.RESTMapper,
metricsClient,
controllerContext.InformerFactory.Autoscaling().V2().HorizontalPodAutoscalers(),
controllerContext.InformerFactory.Core().V1().Pods(),
controllerContext.ComponentConfig.HPAController.HorizontalPodAutoscalerSyncPeriod.Duration,
controllerContext.ComponentConfig.HPAController.HorizontalPodAutoscalerDownscaleStabilizationWindow.Duration,
controllerContext.ComponentConfig.HPAController.HorizontalPodAutoscalerTolerance,
controllerContext.ComponentConfig.HPAController.HorizontalPodAutoscalerCPUInitializationPeriod.Duration,
controllerContext.ComponentConfig.HPAController.HorizontalPodAutoscalerInitialReadinessDelay.Duration,
)
return newControllerLoop(concurrentRun(
func(ctx context.Context) {
custom_metrics.PeriodicallyInvalidate(
apiVersionsGetter,
controllerContext.ComponentConfig.HPAController.HorizontalPodAutoscalerSyncPeriod.Duration,
ctx.Done())
},
func(ctx context.Context) {
pas.Run(ctx, int(controllerContext.ComponentConfig.HPAController.ConcurrentHorizontalPodAutoscalerSyncs))
},
), controllerName), nil
}
컨트롤러 등록 코드의 핵심부만 발췌한 것이다. 전체 코드는 kubernetes/kubernetes의 cmd/kube-controller-manager에 있다.
Controller
쿠버네티스에서 컨트롤러는 컨트롤 루프의 형태로 구현되어 있다. 컨트롤 루프는 시스템 상태를 조절하는 종료되지 않는 루프를 의미하는데, 그중에서도 컨트롤러는 쿠버네티스 클러스터의 상태를 관찰하는 컨트롤 루프다.
그림 4. 제어 루프.
모든 컨트롤러는 목표 상태와 현재 상태를 비교하며 현재 상태를 목표 상태와 최대한 같게 만들고자 한다. 이때 관찰(observe), 행동(act), 분석(analyze)이라는 세 가지 활동을 반복하며 현재 상태를 목표 상태와 같아지도록 만든다.
그림 5. 컨트롤러의 역할.
'목표 상태'라는 개념은 쿠버네티스 리소스 유형을 추적하는 데서 오며, 어떤 쿠버네티스 객체의 목표 상태는 spec에 제시되어 있다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: example-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: example-deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
Metric
HPA가 스케일링 결정을 내리기 위해서는 시스템의 상태를 정확히 파악할 수 있는 지표 수집 체계가 필수적이다. 쿠버네티스는 이를 위해 다단계의 리소스 메트릭 파이프라인(resource metrics pipeline)을 구성한다. 메트릭 파이프라인의 데이터 흐름은 다음과 같은 계층적 구조를 통해 완성된다.
그림 6. 리소스 메트릭 파이프라인의 계층 구조.
- cAdvisor: 가장 기저에는 워커 노드(worker node)의 Kubelet에 내장된 오픈소스 에이전트인 cAdvisor(Container Advisor)가 존재한다. cAdvisor는 실행 중인 컨테이너의 CPU 사이클 소모량, 메모리 점유율 등 하드웨어 리소스 지표를 실시간으로 수집하고 집계한다. 수집된 원시 데이터는 Kubelet을 통해 노드 수준의 요약된 통계로 변환되며,
/metrics/resource및/statsAPI 엔드포인트를 통해 클러스터 내부로 노출된다. - kubelet: 컨테이너 리소스를 관리하기 위한 각 노드의 에이전트다. 리소스 지표는 kubelet API의
/metrics/resource와/stats엔드포인트를 통해 접근할 수 있다. - 노드 수준 리소스 메트릭: 노드 단위로 요약된 수치를 발견하고 가져오기 위해 정의된 kubelet API의
/metrics/resource엔드포인트. - metrics-server: 각 노드에 위치한 kubelet의 리소스 메트릭을 수집하고 하나로 정리하기 위한 클러스터의 애드온 컴포넌트. API 서버는 HPA, VPA(수직 확장 오토스케일러), 그리고
kubectl top명령어가 사용할 Metrics API 엔드포인트를 제공하며, metrics-server는 이 API의 참조 구현을 제공한다. - Metrics API: 워크로드 오토스케일링을 위해 CPU와 메모리에 접근할 수 있게 해 주는 쿠버네티스 API. 실제 클러스터에서 사용하기 위해서는 Metrics API를 제공하는 API 확장 서버가 필요하다.
그림 7. HPA의 반응형 스케일링 파이프라인.
Metrics Server는 클러스터 내의 모든 Kubelet으로부터 주기적으로 리소스 메트릭을 풀링(pulling)하여 중앙 인메모리 저장소에 집계한다. API 서버는 Metrics Server가 집계한 데이터를 metrics.k8s.io API로 확장하여 제공하며, HPA 컨트롤러는 이 API를 쿼리하여 각 파드의 현재 CPU 및 메모리 사용 상태를 파악하게 된다.
HPA의 스케일링 알고리즘
HPA가 파드의 개수를 늘리거나 줄이는 기준은 단순한 절대 임계치의 초과 여부가 아니라, 목표 활용률(target utilization)을 유지하기 위한 비례 계산식에 기초한다. HPA는 CPU 및 메모리 요청량 대비 실제 사용량의 비율을 계산하므로, 파드 사양에 리소스 요청량이 명시적으로 설정되어 있지 않으면 HPA는 사용률 백분율을 계산할 수 없어 작동을 멈추게 된다.
스케일링이 평가될 때, HPA는 다음의 수식을 사용하여 원하는 복제본 수를 산출한다.
이 공식에 따르면 현재 실행 중인 파드의 개수가 2개이고 이들의 평균 CPU 사용률(currentMetricValue)이 80%이며 관리자가 정의한 목표 사용률(desiredMetricValue)이 50%일 경우, 계산식은 가 되어 HPA는 파드를 4개로 확장하도록 워크로드 리소스에 지시한다. 만약 목표 사용률이 70%로 설정되어 있었다면 개의 파드로 확장되었을 것이다. 이는 목표치 설정이 클라우드 리소스 최적화 및 프로비저닝 요구량에 직접적인 영향을 미침을 보여 준다.
그림 8. VPA와 HPA가 잘못 맞물릴 때 벌어지는 죽음의 소용돌이(death spiral).
동시에 HPA는 트래픽의 미세한 출렁임으로 인해 파드 수가 지속적으로 요동치는 플래핑(flapping) 현상을 방지하기 위해, 안정화 창(stabilization window)과 스케일링 속도 제한(scaleUp/scaleDown behavior) 알고리즘을 적용한다. 예를 들어 기본 설정에서 스케일 업은 평가 주기당 최대 2배 혹은 사전에 정의된 특정 수량까지만 허용될 수 있으며, 스케일 다운 시에는 지난 5분(300초)간의 가장 높은 추천 값을 사용하여 일시적인 부하 감소로 인해 파드가 급격히 삭제되는 것을 방지한다. 이 로직이 실제로 구현된 reconcileAutoscaler의 골격은 다음과 같다.
func (a *HorizontalController) reconcileAutoscaler(ctx context.Context, hpaShared *autoscalingv2.HorizontalPodAutoscaler, key string) (retErr error) {
// ... (중략: 모니터링 계측, 공유 캐시 보호용 DeepCopy, 스케일 대상 조회와 오류 처리)
currentReplicas := scale.Spec.Replicas
a.recordInitialRecommendation(currentReplicas, key)
var (
metricStatuses []autoscalingv2.MetricStatus
metricDesiredReplicas int32
metricName string
)
desiredReplicas := int32(0)
rescaleReason := ""
var minReplicas int32
if hpa.Spec.MinReplicas != nil {
minReplicas = *hpa.Spec.MinReplicas
} else {
// Default value
minReplicas = 1
}
rescale := true
logger := klog.FromContext(ctx)
if currentReplicas == 0 && minReplicas != 0 {
// Autoscaling is disabled for this resource
desiredReplicas = 0
rescale = false
setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "ScalingDisabled", "scaling is disabled since the replica count of the target is zero")
} else if currentReplicas > hpa.Spec.MaxReplicas {
rescaleReason = "Current number of replicas above Spec.MaxReplicas"
desiredReplicas = hpa.Spec.MaxReplicas
} else if currentReplicas < minReplicas {
rescaleReason = "Current number of replicas below Spec.MinReplicas"
desiredReplicas = minReplicas
} else {
var metricTimestamp time.Time
metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(ctx, hpa, scale, hpa.Spec.Metrics)
// computeReplicasForMetrics may return both non-zero metricDesiredReplicas and an error.
// That means some metrics still work and HPA should perform scaling based on them.
// ... (중략: 메트릭 계산 실패 처리와 제안 값 로깅)
rescaleMetric := ""
if metricDesiredReplicas > desiredReplicas {
desiredReplicas = metricDesiredReplicas
rescaleMetric = metricName
}
if desiredReplicas > currentReplicas {
rescaleReason = fmt.Sprintf("%s above target", rescaleMetric)
}
if desiredReplicas < currentReplicas {
rescaleReason = "All metrics below target"
}
if hpa.Spec.Behavior == nil {
desiredReplicas = a.normalizeDesiredReplicas(hpa, key, currentReplicas, desiredReplicas, minReplicas)
} else {
desiredReplicas = a.normalizeDesiredReplicasWithBehaviors(hpa, key, currentReplicas, desiredReplicas, minReplicas)
}
rescale = desiredReplicas != currentReplicas
}
if rescale {
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
// On each retry, set the desired replicas on the scale object.
// A deep copy is not needed here as the 'scale' object was read
// directly from the API server and is not from a shared cache.
scale.Spec.Replicas = desiredReplicas
// Attempt to UPDATE the scale subresource.
_, updateErr := a.scaleNamespacer.Scales(hpa.Namespace).Update(ctx, targetGR, scale, metav1.UpdateOptions{})
if updateErr == nil {
return nil // Success
}
// ... (중략: 충돌 시 최신 scale 객체를 다시 읽어 재시도 준비)
// Return the original update error to be checked by RetryOnConflict.
return updateErr
})
// ... (중략: 재시도 소진 시 실패 처리, 성공 시 이벤트와 스케일 이벤트 기록)
} else {
// ... (중략: 스케일 보류 로깅)
desiredReplicas = currentReplicas
}
a.setStatus(hpa, currentReplicas, desiredReplicas, metricStatuses, rescale)
// ... (중략: desiredReplicas 계측과 상태 업데이트 오류 처리)
return retErr
}
오류 처리·계측·로깅을 // ... (중략)으로 줄인 발췌본이다. 전체 구현은 kubernetes/kubernetes의 pkg/controller/podautoscaler에 있다.
이벤트 기반 트래픽 급증 상황에서 HPA
앞서 설명한 HPA의 메커니즘은 점진적으로 변화하는 일반적인 웹 트래픽 패턴에서는 매우 효과적으로 동작한다. 그러나 초당 요청 수가 순식간에 수만 단위로 뛰어오르는 티켓팅, 플래시 세일, 서버리스 아키텍처의 급격한 활성화 상황에서는 HPA가 시스템을 보호하기는커녕 오히려 장애를 가속화하는 요인으로 전락할 수 있다. 그 원인은 리소스 처리 커널 레벨의 차이, 누적되는 물리적 지연 시간, 그리고 캐시 쇄도(cache stampede) 현상으로 나누어 분석할 수 있다.
리소스 처리 커널 레벨의 차이
HPA가 주로 모니터링하는 CPU와 메모리는 커널 관점에서 근본적으로 다른 방식으로 취급되며, 이는 트래픽 폭증 시 애플리케이션의 생존 여부를 가르는 결정적 차이를 만든다.
CPU
CPU는 선점형(preemptive)이며 압축 가능한(compressible) 리소스이다. 여러 프로세스와 스레드가 제한된 CPU 자원을 놓고 경쟁할 때, Linux 커널의 스케줄러는 프로세스 간의 컨텍스트 스위칭을 통해 시간을 분할하여 할당한다.
그림 9. 컨텍스트 스위칭의 흐름.
즉, 트래픽이 폭증하여 파드의 CPU 사용량이 100% 한계치에 도달하더라도 애플리케이션 자체가 즉시 붕괴하거나 강제 종료되지는 않는다. 커널이 CPU 사이클을 '압축'하여 각 요청에 더 적은 시간을 할당하므로, 전체적인 응답 지연 시간이 급격히 상승하고 시스템이 느려질 뿐이다. 이러한 압축성 덕분에 CPU 지표가 80%, 90%로 치솟아 HPA가 이를 인지하고 스케일 아웃을 트리거할 때까지 애플리케이션은 어떻게든 버티며 동작할 수 있다. 이것이 CPU가 반응형 스케일러의 지표로 널리 사용되는 이유이다.
메모리
반면 메모리는 비선점형(non-preemptive)이며 압축 불가능한(incompressible)1 자원이다. 프로세스가 데이터 처리나 커넥션 유지를 위해 메모리 블록을 요청하면 커널은 이를 물리적으로 할당해야 하며, 해당 프로세스가 작업을 마치고 명시적으로 메모리를 해제하기 전까지는 다른 프로세스에게 이를 강제로 양보하게 할 수 없다. 폭증하는 트래픽으로 인해 파드가 설정된 메모리 한계를 초과하여 할당을 요구할 경우, 커널은 이를 압축할 방법이 전혀 없으므로 시스템 전체를 보호하기 위해 즉각적으로 OOM(Out-Of-Memory) 킬러2를 호출하여 해당 컨테이너 프로세스를 강제 종료시킨다.
그림 10. 메모리와 스왑(swap)의 관계.
이러한 차이로 인해 HPA의 반응형 스케일링 모델은 메모리 지표에서 치명적으로 실패한다. 트래픽 유입 후 메모리 사용량이 치솟아 Metrics Server가 이를 집계하고 15초 주기의 HPA 루프가 이를 인지하여 스케일링 명령을 내리기도 전에, 이미 파드는 OOMKilled 상태가 되어 버린다. 더 끔찍한 것은 파드가 죽는 순간 해당 파드가 처리 중이던 모든 인플라이트(in-flight) 네트워크 요청, 맺어져 있던 커넥션 풀, 그리고 쿼리 결과를 저장해 둔 '따뜻한' 캐시가 일거에 증발한다는 점이다. 이는 메모리가 사전 확장 지표로는 적합하지 않으며, 본질적으로 너무 늦게 나타나는 시그널임을 의미한다.
누적되는 물리적 지연 시간
HPA는 근본적으로 트래픽이 시스템에 물리적인 타격을 입히고 난 이후에 나타나는 증상(CPU/메모리 상승)을 보고 반응하는 구조다. 이러한 구조로 인해 폭우처럼 쏟아지는 요청 앞에서는 복합적인 지연 시간이 누적되어 거대한 병목을 형성한다.
| 지연 발생 구간 | 평균 소요 시간 | 기술적 원인 및 한계 |
|---|---|---|
| 메트릭 수집 및 HPA 동기화 | 15초 ~ 30초 | Kubelet 집계, Metrics Server 풀링, HPA의 15초 고정 주기 평가로 인한 태생적 지연 |
| 스케일링 속도 제한 (Behavior) | 평가 주기당 다수 발생 | 급격한 변동을 막기 위한 최대 확장 비율(예: 한 번에 2배까지만 증가) 제한으로 파드 증가가 계단식으로 지연됨 |
| 노드 프로비저닝 (CA/Karpenter) | 1분 ~ 3분 | 워커 노드의 공간이 부족할 경우, 클라우드 제공자(AWS, GCP 등)가 새로운 인스턴스(VM)를 생성하고 부팅 및 클러스터에 조인하는 시간 |
| 파드 기동 (Cold Start) | 10초 ~ 60초+ | 컨테이너 이미지 다운로드, 애플리케이션 런타임 초기화, 데이터베이스 커넥션 수립, Readiness Probe3 통과 시간 |
그림 11. 메트릭 파이프라인 각 구간에서 누적되는 지연.
사용자가 사이트에 몰려든 시점부터 실제 트래픽을 처리할 추가 파드가 활성화되기까지, 이미지가 캐싱되어 있고 노드 용량이 충분한 최상의 시나리오에서도 최소 30초가 소요되며, 새로운 노드를 띄워야 하는 최악의 상황에서는 3분 이상의 공백기가 발생한다. HPA가 스케일 아웃을 준비하는 이 지연된 시간 동안 기존의 파드들이 수백 배 폭증한 트래픽을 오롯이 견뎌야 하며, 이는 즉각적인 CPU 스로틀링, 처리량 저하, 응답 지연 시간의 기하급수적 상승, 그리고 최종적으로 500번대 서버 에러를 초래한다.
캐시 쇄도
캐시 미스가 동시에 많이 발생하면 데이터베이스에 부담이 가중된다. 이것이 보통 Redis 등 인메모리 저장소에서 발생하는 '캐시 쇄도'라고 부르는 상황인데, 캐시가 전부 정확히 같은 시간에 만료되도록 구현하면 자주 발생한다.
그림 12. 동시에 발생한 캐시 미스가 데이터베이스로 몰려가는 캐시 쇄도.
쿠버네티스에서 캐시 쇄도가 발생하는 과정
극단적인 트래픽 상황에서 스케일링 지연은 단순한 응답 지연을 넘어 클러스터 전체의 연쇄적 붕괴를 일으킨다. 소수의 기존 파드들이 폭증하는 부하를 견디지 못하고 메모리 한계에 도달하여 OOMKilled로 사망하면, 쿠버네티스의 인그레스 또는 서비스 로드밸런서는 해당 파드가 감당하던 방대한 트래픽을 살아남은 극소수의 '따뜻한' 파드들에게 즉시 재분배한다. 이는 살아남은 파드들의 부하를 폭발적으로 가중시켜, 릴레이 경주처럼 차례대로 OOMKilled를 초래한다.
그림 13. 하나의 인그레스가 여러 서비스로 트래픽을 분배하는 Ingress Fan Out 구조.
더불어, 지연 시간 끝에 HPA에 의해 새롭게 생성된 파드들은 이전의 맥락을 전혀 알지 못하는 '차가운' 상태로 구동된다. 이들은 메모리에 캐시가 비어 있기 때문에, 들어오는 엄청난 양의 읽기 요청을 처리하기 위해 캐시 대신 백엔드 데이터베이스로 직접 무거운 쿼리를 날리게 된다. 새롭게 구동된 수십에서 수백 개의 콜드 파드들이 일제히 무거운 쿼리를 들고 데이터베이스로 돌진하는 현상이 곧 캐시 쇄도다 (CDN 계층에서 같은 성격의 문제가 나타나는 사례는 스트리밍 서비스의 지연시간·대역폭·전송 안정성 참고). 이로 인해 백엔드 데이터베이스마저 커넥션 풀 고갈과 CPU 100% 한계에 부딪혀, 시스템 전체가 회복 불가능한 상태로 빠지게 된다.
HPA는 비즈니스를 이해하지 못한다
이러한 모든 실패의 기저에는 HPA가 비즈니스 로직과 이벤트를 전혀 이해하지 못한다는 치명적인 단점이 존재한다. 넷플릭스, 우버, 도어대시와 같은 선도적인 테크 기업들은 시스템에 트래픽이 몰리기 전, 비즈니스 이벤트나 머신러닝 모델을 활용하여 사전 확장(predictive/preemptive autoscaling)을 수행한다.
그림 14. 반응형 스케일링과 사전 확장의 비교.
하지만 HPA는 내일 오전에 대규모 쇼핑몰 세일이 예정되어 있거나, 메시지 큐에 수십만 건의 처리 대기열이 쌓여 처리량이 급증할 것이라는 맥락을 알지 못한다. HPA는 그저 파드의 CPU 사용률이 임계치를 넘어설 때까지 눈을 감고 대기할 뿐이다. 사용자 요청 수나 대기 중인 큐의 깊이 등 실제 애플리케이션 부하를 직접적으로 대변하는 선행 지표(leading indicators)를 활용하지 못하는 HPA 단일 구조는, 현대의 100만 명 단위 동시 접속 애플리케이션을 지탱하기에는 명백히 역부족이다.
KEDA
위와 같은 HPA의 반응성 부족, 지표의 한계, 그리고 비즈니스 이벤트 인지 능력을 극복하기 위해 클라우드 네이티브 진영에서 도입한 대안이 바로 KEDA(Kubernetes-based Event Driven Autoscaler)이다.
HPA가 철저하게 API 서버의 기본 리소스 메트릭(CPU/RAM)에 갇혀 있다면, KEDA는 외부의 방대한 시스템(이벤트 소스)에서 발생하는 다양한 메트릭을 클러스터 내부로 끌어들여, 이를 기반으로 HPA가 스케일링 결정을 내릴 수 있도록 '통역'을 수행한다. 즉 HPA의 두뇌를 활용하되, 그 시야를 외부 데이터베이스, 메시지 큐, 클라우드 제공자의 원격 통계로 확장시키는 것이다.
주요 구성요소
KEDA 아키텍처는 클러스터 내에서 긴밀하게 협력하는 세 가지 주요 컴포넌트로 구성된다.
그림 15. KEDA의 아키텍처.
- KEDA Operator (Controller & Scaler): 클러스터 내에서 실행되는 핵심 에이전트로, 두 가지 상반된 역할을 동시에 수행한다. Controller는 쿠버네티스의 CRD(Custom Resource Definition, 사용자 정의 리소스)인
ScaledObject및ScaledJob리소스를 지속적으로 감시하며, 트래픽이나 이벤트가 전혀 없을 경우 파드의 개수를 0에서 1로 부팅하거나 1에서 0으로 완전히 축소하는 특수한 생명주기를 직접 관리한다. Scaler는 외부 세계와 직접 소통한다. Kafka, RabbitMQ, AWS SQS, Redis, Prometheus 등 70개가 넘는 내장 스케일러 모듈을 통해 외부 이벤트 소스에 직접 연결하여, 실시간 메트릭(예: Kafka 토픽의 지연 메시지 수)을 능동적으로 수집한다. - Metrics Adapter/Server: KEDA Operator가 획득한 다양한 형태의 외부 메트릭을, HPA가 이해할 수 있는 쿠버네티스 표준 규격인 Custom Metrics API(
external.metrics.k8s.io)로 변환하여 노출하는 어댑터이다. HPA 컨트롤러는 이 API를 통해 외부 지표를 클러스터 내부의 지표처럼 인식하고, 수리적 모델에 대입하여 스케일 아웃 여부를 평가한다. - Admission Webhooks: HPA와 KEDA 간의 설정 충돌을 방지하고, 올바른 CRD 스펙이 배포되었는지 검증하여 시스템 안정성을 유지한다.
KEDA vs HPA
| 비교 항목 | 기본 HPA | KEDA |
|---|---|---|
| 스케일링 지표 (Trigger) | 워크로드의 CPU, 메모리 등 리소스 후행 지표 | 큐 길이, 이벤트 발생 수, 커스텀 메트릭 등 선행 지표 |
| 0으로의 축소 (Scale-to-Zero) | 불가능 (최소 1개의 파드 항시 유지 필요) | 가능 (이벤트 부재 시 0으로 축소하여 비용 절감) |
| 스케일링 의사결정의 주체 | CPU 부하 누적 후 반응형 스케일링 | 외부 이벤트 트리거 발생 즉시 사전 대응 |
| 잡(Job) 스케일링 지원 | Deployment 등 롱-러닝 파드 위주 지원 | ScaledJob을 통한 이벤트 기반 일회성 잡 실행 지원 |
| 커스텀 수식 및 Fallback | 단일 임계치 기반 산술 | 복합 지표 수식 작성 및 메트릭 유실 시 Fallback 지원 |
순수 HPA의 경우 15초 단위의 메트릭 수집 및 나눗셈 연산의 한계로 인해 파드의 최소 개수를 1 이하로 설정할 수 없다. 즉 시스템을 사용하지 않는 새벽 시간대에도 최소한 하나의 컨테이너가 컴퓨팅 리소스를 점유하며 비용을 발생시킨다. 반면 KEDA는 외부 큐에 대기 중인 메시지가 전혀 없거나 HTTP 요청이 0인 상태를 감지하면, 독자적인 컨트롤러를 개입시켜 해당 워크로드를 0으로 축소시키는 강력한 효율화 기능을 제공한다. 이는 특히 불규칙적으로 실행되는 데이터 파이프라인이나 서버리스 형태의 백그라운드 프로세서에서 클라우드 비용을 극적으로 절감한다.
트래픽이 유입되어 이벤트가 1 이상 감지되면 KEDA 컨트롤러가 즉시 개입하여 파드를 0에서 1로 부팅시킨다. 그리고 파드가 1개 이상 생성된 직후부터는 그 제어권을 넘겨받아, KEDA 어댑터가 노출하는 수치를 바탕으로 쿠버네티스 기본 HPA가 계산식에 따라 거대한 수평 확장을 이끌어 가게 된다.
실무에서는 이러한 KEDA의 장점을 활용하여, 단순 자원 사용률이 아닌 초당 HTTP 요청 수(RPS)나 메시지 큐의 지연 길이 등 실제 애플리케이션의 부하를 직접 대변하는 지표를 바탕으로 오토스케일링을 구현한다. 예를 들어 Amazon CloudWatch와 KEDA를 연동하여 특정 네임스페이스 내 애플리케이션의 총 요청 수가 목표 임계치를 넘을 때 즉시 파드를 스케일링하도록 ScaledObject에 pollingInterval을 30초, cooldownPeriod를 300초 등으로 세밀하게 조정할 수 있다. 또한 KEDA HTTP 애드온을 활성화하면 첫 번째 스파이크 트래픽이 유입되는 즉시 OpenTelemetry 컬렉터를 통해 HPA가 반응하도록 유도하여, 파드 수가 실제 요청 수 곡선과 실시간으로 일치하도록 동기화할 수 있다.
비동기 메시지 버퍼링
KEDA를 활용하여 HPA의 지표 한계를 선행 이벤트 지표로 교체하더라도, 여전히 컨테이너 부팅 시간과 클라우드 노드 프로비저닝에 소요되는 절대적인 물리적 시간이 남아 있다. 티켓팅 오픈과 동시에 1초 만에 유입되는 수십만 건의 API 요청을 백엔드 데이터베이스가 그대로 맞이하게 둔다면 어떠한 오토스케일러도 붕괴를 막을 수 없다. 따라서 시스템 엔지니어링에서는 급증하는 트래픽 파도를 시스템이 흡수 가능한 완만한 언덕으로 변환시키는 거대한 '댐'을 구축해야 하며, 이를 위해 메시지 브로커(Apache Kafka, RabbitMQ 등)를 활용한 비동기 버퍼링 아키텍처가 필수적으로 적용된다.
그림 16. 트래픽 파도를 흡수하는 비동기 메시지 버퍼.
생산자와 소비자의 분리
생산자-소비자 문제
그림 17. 생산자, 공유 버퍼, 소비자의 관계.
어떤 정보를 생성하는 객체를 생산자(producer)라 부르고, 그 정보를 전달받는 객체를 소비자(consumer)라 부른다. 모든 시스템은 생산자가 소비자에게 직접 전달할 수 없게 설계되어 있기 때문에, 생산자는 어떤 공유 버퍼를 거칠 수밖에 없다. 얼핏 보면 과정은 간단해 보인다.
- 생산자가 공유 버퍼에 정보를 넣는다.
- 소비자가 공유 버퍼에서 정보를 가져온다.
하지만 진짜 문제는, 실제 서버에서는 생산자, 공유 버퍼, 그리고 소비자가 하나가 아니라는 데 있다.
그림 18. 생산자, 버퍼, 소비자가 여럿인 실제 서비스의 구조.
데이터베이스나 외부 API(결제망 등) 의존성이 높은 시스템에서는 사용자 요청을 웹 서버가 동기적으로 처리하여 데이터베이스에 직접 트랜잭션을 실행하게 두어선 안 된다. 트래픽이 치솟는 상황에서 이러한 동기식 아키텍처는 즉각적인 스레드 풀 고갈과 락 경합을 유발한다. 대신 최전방의 API 서버(생산자)는 들어오는 트래픽의 페이로드를 즉시 Apache Kafka나 RabbitMQ와 같은 분산 메시지 시스템의 큐에 이벤트 형태로 발행하고, 사용자 클라이언트에게는 202 Accepted 상태 코드와 함께 비동기 처리 중임을 알리는 응답만 밀리초 단위로 빠르게 반환해야 한다.
그림 19. Kafka를 완충 지대로 삼는 전체 아키텍처.
이 아키텍처에서 Kafka는 시스템 내구성을 보장하는 강력한 완충 지대 역할을 수행한다.
- 완벽한 결합 분리(decoupling): 생산자(API 서버)와 소비자(백엔드 워커 파드)는 서로의 상태를 전혀 알 필요 없이 독립적으로 동작한다. 백엔드 데이터베이스가 응답을 멈추거나 컨슈머 파드들이 아직 HPA에 의해 스케일 업되지 않아 처리 속도가 느려지더라도, API 서버는 데이터 드롭 없이 트래픽을 계속해서 큐로 밀어 넣을 수 있다.
- 영속성 및 재처리 보장(durability & replay): Kafka는 메시지를 메모리가 아닌 디스크에 순차적으로 영속화한다. 폭주하는 트래픽은 데이터베이스를 폭파시키는 대신 Kafka 클러스터 내부에 안전한 로그 파일 형태로 쌓이게 되며, 특정 워커 파드가 장애로 크래시되더라도 오프셋 조정을 통해 얼마든지 이벤트를 재처리할 수 있다.
이러한 패턴은 대규모 로그 수집 환경에서도 동일하게 적용된다. OpenTelemetry 에이전트 계층과 백엔드 데이터 저장소 사이에 Kafka 계층을 위치시키면, 백엔드가 다운되더라도 에이전트의 데이터 유실 없이 최대 보존 기한까지 텔레메트리 데이터를 버퍼링할 수 있다.
소비자 동적 스케일링
물론 메시지가 큐에 무한정 쌓이도록 방치할 수는 없기 때문에, 밀려드는 버퍼를 신속하게 비워낼 백엔드 파드의 확장이 동반되어야 한다. 바로 이 지점에서 버퍼링 전략과 KEDA의 이벤트 주도형 오토스케일링이 시너지를 발휘한다.
그림 20. 큐의 지연 길이에 따라 소비자를 동적으로 확장하는 구조.
KEDA의 kafka 트리거를 활용하여 ScaledObject를 구성하면, KEDA는 Kafka 토픽 내 특정 컨슈머 그룹의 미처리 메시지 개수인 오프셋 지연(consumer offset lag)을 지속적으로 폴링한다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: consumer-scaled
spec:
scaleTargetRef:
name: consumer-deployment # (1)
cooldownPeriod: 30 # (2)
maxReplicaCount: 10 # (3)
advanced:
horizontalPodAutoscalerConfig: # (4)
behavior:
scaleDown:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 50
periodSeconds: 30
triggers: # (5)
- type: kafka
metadata:
bootstrapServers: one-node-cluster.redpanda:9092
consumerGroup: a
topic: test-topic
lagThreshold: '5'
- 소비자 역할을 수행하는 애플리케이션에 오토스케일러가 적용된다. 이 애플리케이션은
consumer-deployment라는 이름으로 배포된다. cooldownPeriod의 기본값을 300초에서 30초로 줄여, 트래픽이 없을 때 리소스를 모두 회수하는 scale-to-zero 메커니즘을 테스트한다.- 동시에 실행되는 파드의 최대 개수는 기본값 100개 대신 토픽 내 파티션의 개수와 동일한 10개로 설정한다.
- 쿠버네티스 HPA의 행동을 직접 구성할 수 있다. 스케일 다운 때 현재 실행 중인 레플리카의 50%까지만 허용한다.
- 트리거를 설정하는 것이 핵심이다. 여기에는 카프카 클러스터의 주소, 토픽의 이름, 그리고 이 애플리케이션에 접근하는 소비자 그룹의 이름이 들어가야 한다. 지연의 한계값(lag threshold)은
5다.
참고로 (1)에서 대상이 되는 소비자 애플리케이션의 Deployment는 다음과 같다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: consumer-deployment
spec:
selector:
matchLabels:
app: consumer
template:
metadata:
labels:
app: consumer
spec:
containers:
- name: consumer
image: piomin/consumer
ports:
- containerPort: 8080
일반적인 HPA 설정의 느린 반응을 압도하기 위해, 실무 환경에서는 KEDA의 pollingInterval을 10초 내외로 촘촘하게 설정하여 지연을 실시간으로 감지하고, 트리거의 lag threshold 값을 매우 낮게(예: 20~50개) 설정한다. 이렇게 구성하면 트래픽 폭증으로 큐에 10만 개의 데이터가 쌓이는 순간 HPA 수식의 비율이 압도적으로 커지게 되어, HPA가 허용하는 최대 복제본 수까지 매우 공격적인 스케일 아웃 명령을 하달하게 된다.
워커 파드들이 충분히 늘어나면 각 파드는 지정된 파티션을 할당받아 안정적인 속도로 메시지를 소비하며 데이터베이스 트랜잭션을 안전하게 수행한다. 폭풍이 지나가고 지연 길이가 0에 수렴하면, KEDA는 설정된 cooldownPeriod(예: 300초) 대기 후 워커 파드들을 스케일 인하여 초기 상태 혹은 0개로 되돌려 놓는다.
가상 대기열(Virtual Waiting Room) 시스템 설계와 동기적 트래픽 제어
비동기식 메시지 큐는 백그라운드 작업이나 결제 승인 후 처리에는 완벽하지만, 좌석 예매 시스템, 선착순 수강 신청, 한정판 스니커즈 구매와 같이 사용자가 브라우저 화면에서 자신의 순서를 명확하게 확인하고 동기적인 상호작용(예: 맵에서 좌석 선택)을 이어가야 하는 시나리오에서는 적합하지 않다. 이러한 비즈니스 요구사항을 충족하며 동시에 트래픽으로부터 백엔드를 보호하는 아키텍처가 바로 가상 대기열(virtual waiting room) 시스템이다.
그림 21. 가상 대기열 시스템의 구조.
그림 22. 가상 대기열의 단순화된 흐름.
가상 대기열은 100만 명의 사용자가 메인 시스템의 정문을 부수고 들어오는 것을 막고, 이들을 통제된 광장에 세워둔 뒤 클러스터의 실제 처리 용량에 맞춰 일정 인원씩만 입장시키는 문지기 역할을 수행한다. 이는 혼돈스러운 수직적 트래픽 스파이크를, 시스템이 장시간 안정적으로 처리할 수 있는 수평적이고 관리 가능한 언덕으로 변환하는 트래픽 성형 기법이다.
Redis
수십만 명의 접속 순번을 관리하고 실시간으로 순위를 알려주기 위해 관계형 데이터베이스 관리 시스템을 사용하는 것은 치명적이다. 잦은 읽기/쓰기로 인해 디스크 I/O 병목이 발생하고 시스템 전체가 느려지기 때문이다. 따라서 가상 대기열의 상태 저장소로는 1밀리초 이하 수준의 처리 속도를 보장하는 인메모리(in-memory) 데이터 스토어인 레디스(Redis)가 업계 표준으로 활용된다. 클라우드 네이티브 환경에서는 Redis Enterprise Operator나 Upstash와 같은 서버리스 레디스 솔루션을 활용하여, 상태 저장 앱의 프로비저닝 복잡도를 낮추고 쿠버네티스 네이티브한 환경을 구축할 수 있다.
그림 23. Redis의 아키텍처.
Redis를 활용한 대기열 구현의 핵심은 Sorted Set(ZSET) 자료구조를 응용하는 것이다. ZSET은 저장되는 각 멤버에 고유한 점수를 부여하여 저장과 동시에 내부적으로 정렬을 수행함으로써, 순위 검색 시 수준의 성능을 제공한다. 일반적인 데이터베이스로는 달성하기 어려운 공정함을 유지하는 대기열 시스템의 동작 흐름은 다음과 같다.
그림 24. 가상 대기열 시스템의 전체 동작 흐름.
- 대기열 진입(enqueueing): 이벤트가 오픈되고 사용자가 플랫폼에 접근하면, API 게이트웨이나 엣지 워커는 사용자를 즉각 통과시키지 않고 대기열 할당 시스템으로 우회시킨다. 시스템은 각 사용자를 고유한 토큰(UUID)으로 캡슐화한 뒤, Redis ZSET에
ZADD waiting_queue <Unix_Timestamp> <UUID>명령을 통해 진입시킨다. 이때 도착한 시점의 마이크로초 단위 타임스탬프를 점수(score)로 사용함으로써 완벽한 선입선출 정렬 기준을 확립한다. - 순번 실시간 확인(polling/WebSockets): 대기실 화면에 갇힌 사용자의 클라이언트 앱은 주기적으로 폴링을 수행하여 자신의 위치를 묻는다. 백엔드는 Redis의
ZRANK waiting_queue <UUID>명령어를 단 1회 실행하여 현재 대기열 내에서 해당 사용자의 정확한 대기 순번을 도출하여 반환한다. 이를 바탕으로 클라이언트는 "앞에 54,321명이 있습니다"라는 UI를 사용자에게 제공하여 이탈률을 방지한다. - 허가 및 상태 전이(dequeue & token activation): HPA에 의해 백엔드 파드가 충분히 확장되었거나 시스템 모니터링 툴(예: Prometheus)을 통해 백엔드 데이터베이스의 부하가 정상 범주로 확인되면, 시스템 관리자(또는 자동화된 수용량 스케줄러)는 게이트를 개방한다. 스케줄러는
ZPOPMIN waiting_queue <N>명령어를 통해 ZSET에서 가장 타임스탬프가 앞선 N명의 사용자를 일괄적으로 추출하고, 이들의 토큰을 '활성화 큐(active token pool)'로 전이시킨다. - 보호 구역 진입 제어(CQRS4 및 edge validation): 사용자의 브라우저가 자신의 토큰이 활성화되었음을 감지하면 비로소 실제 트래픽을 발생시키는 예매 API를 호출한다. 쿠버네티스의 Ingress Controller나 Envoy Proxy 단에서는 요청 헤더에 삽입된 토큰을 검증하여, 활성 풀에 등록되지 않은 비정상 접근이나 새치기 요청은
403 Forbidden처리하여 즉시 차단함으로써 백엔드의 비즈니스 로직을 보호한다.
이러한 가상 대기열 방식은 HPA가 새로운 노드를 클러스터에 합류시키고 수백 개의 파드를 부팅하는 긴 지연 시간 동안, 시스템이 완전히 다운되는 것을 막아 준다.
엣지 인프라 최적화
메시지 큐와 대기열 시스템이 백엔드를 보호하는 거시적 방파제라면, 개별 요청의 처리 비용을 0으로 수렴하게 만들고 노드 확장 지연을 기술적으로 극복하는 것은 미시적인 최적화의 영역이다. 클러스터 외부의 엣지 네트워크에서 부하를 상쇄하고, 1초의 지연도 용납하지 않도록 시스템을 구성해야 한다.
CDN 캐싱과 속도 제한을 통한 프론트라인 방어
서버 트래픽 폭주 시 애플리케이션 파드의 CPU를 가장 허무하게 낭비하는 것은 정적 자원의 서빙이다. 티켓팅 페이지를 렌더링하기 위한 대용량 HTML, 아티스트 포스터 이미지, 좌석 배치도 스크립트 파일이 쿠버네티스 내부 리소스를 소비하게 두어서는 안 된다. 트래픽의 80% 이상을 구성하는 이러한 정적 자산은 AWS CloudFront, Cloudflare, Fastly와 같은 CDN(Content Delivery Network, 콘텐츠 전송 네트워크)을 활용하여, 클라이언트와 물리적으로 가장 가까운 글로벌 엣지 서버에서 즉각 반환되도록 조치해야 한다.
또한 사용자 인증 등 약간의 동적 처리가 가미된 데이터라 할지라도, 모든 사용자에게 동일하게 보여지는 공지사항 데이터 등은 인그레스 컨트롤러 수준에서 짧은 TTL(Time-To-Live)을 가진 HTTP 응답 캐싱을 적용해야 한다. 이를 통해 백엔드 파드와 데이터베이스로 인입되는 쿼리의 양을 기하급수적으로 줄일 수 있다.
동시에 시스템 인프라를 위협하는 악의적 요청이나 비정상적인 새로고침 매크로를 통제하기 위해, API 게이트웨이 수준에서의 속도 제한과 트래픽 셰이핑이 수반되어야 한다. 인그레스 단에서 특정 IP나 토큰당 초과 요청을 즉각 식별하여 429 Too Many Requests 상태 코드를 반환함으로써, 쿠버네티스 워크로드 리소스를 소모하기 전에 요청을 폐기한다. 선진적인 환경에서는 Retry-After 헤더를 포함하여 지수적 백오프 방식으로 클라이언트의 자동 재시도를 유도하여 트래픽 쏠림을 완화한다.
캐시 쇄도 현상 극복하기
캐시 쇄도 현상을 극복하기 위해 실무 애플리케이션 계층에서는 두 가지 동시성 제어 기법을 도입한다.
분산 락
그림 25. 분산 락으로 데이터베이스 접근을 직렬화하는 구조.
레디스의 SET NX(Not eXists) 등 원자적 명령어를 활용하여 락을 구현한다. 캐시가 증발한 것을 목격한 수많은 스레드 중 선착순 단 1개의 스레드만이 cache-key-LOCK 획득에 성공하게 만든다. 락을 획득한 스레드만이 데이터베이스에 접근하여 결과를 계산한 후 캐시를 갱신하고 알림을 발행한다. 락을 획득하지 못한 나머지 수만 개의 스레드들은 폴링을 멈추고 캐시가 갱신될 때까지 안전하게 대기하거나, 이전 상태의 만료된 데이터를 잠시 반환(stale-while-revalidate)5하도록 구성한다.
비동기 캐시 선행 로딩
그림 26. 백그라운드에서 캐시를 미리 채워 두는 비동기 캐시 선행 로딩 구조.
클라이언트 요청 빈도와 무관하게 시스템 백그라운드 스레드가 동작하는 방식이다. 자바 진영의 Caffeine 캐시 라이브러리의 buildAsync() 기능 등을 이용하면, 여러 스레드가 동시에 같은 키를 요청하더라도 내부적으로 단 한 번의 비동기 계산만 실행되도록 스레드를 통제한다. 더 적극적으로는 캐시가 만료되기 전에 주기적인 CronJob을 돌려 데이터베이스에서 웜 캐시를 계속해서 덮어씌움으로써, 사용자는 결코 데이터베이스 쿼리 대기열에 합류하지 않고 영구적인 캐시 히트만 경험하도록 아키텍처를 설계한다.
Dummy Pods
HPA와 KEDA가 지표를 신속하게 인지하고 파드 스케일 아웃 명령을 내리더라도, 해당 파드를 배치할 워커 노드의 컴퓨팅 공간이 꽉 차 있다면 무용지물이 된다. Cluster Autoscaler(CA)나 Karpenter가 새로운 노드 프로비저닝을 클라우드 API에 요청하고, 해당 노드가 부팅되어 클러스터에 등록되기까지 소요되는 최소 1~3분의 공백은 티켓팅 도중 발생하는 최악의 병목이다.
그림 27. 더미 파드를 이용한 오버프로비저닝.
이러한 물리적 한계 시간을 아키텍처 수준에서 완전히 제거하는 고급 기법이 바로 '더미 파드(dummy pods)'를 활용한 오버프로비저닝(over-provisioning)이다.
- 시스템 관리자는 서비스 오픈 전, 내부 로직이 전혀 없이 그저 컴퓨팅 리소스만 크게 차지하는 가벼운
pause컨테이너6(더미 파드)를 다수 생성하여 클러스터에 배포한다. 이때 쿠버네티스의 스케줄링 우선순위를 결정하는PriorityClass를 활용하여, 실제 비즈니스 애플리케이션 파드에는 최상위 우선순위를 부여하고 더미 파드에는 클러스터 내에서 가장 낮은 마이너스 단위의 우선순위를 부여한다. - 100만 유저가 접속하여 KEDA가 수백 개의 실제 애플리케이션 파드 생성을 트리거하면, 쿠버네티스 스케줄러(
kube-scheduler)는 새 파드를 배치할 노드를 탐색한다. 노드의 남은 용량이 부족하다는 것을 인지하지만, 스케줄러는 클라우드 공급자가 새 노드를 띄워줄 때까지 대기하지 않는다. 대신 현재 노드의 공간을 차지하고 있는 가장 우선순위가 낮은 더미 파드들을 가차 없이 즉각 쫓아내고, 빈자리에 실제 애플리케이션 파드를 스케줄링한다. - 쫓겨난 수십 개의 더미 파드들은 갈 곳을 잃어 Pending 상태로 전환되며, 이때서야 백그라운드의 Karpenter나 CA가 천천히 새로운 노드를 프로비저닝하여 빈 더미 파드들을 채워 넣는다. 결과적으로 사용자의 결제 트래픽은 노드 생성이라는 지루한 지연 시간에 전혀 얽매이지 않고 즉각적인 확장의 혜택을 온전히 누릴 수 있게 된다. 약간의 유휴 클라우드 비용을 지불하더라도, 대규모 이벤트 상황에서 무중단 확장을 이끌어내는 가장 확실하고 강력한 투자 전략이다.
결론
HPA는 "지표를 보고 파드 수를 맞춘다"는 우아한 컨트롤 루프지만, 그 지표가 후행적이라는 사실이 모든 문제의 출발점이다. CPU는 버티다가 늦게 신호를 주고, 메모리는 신호를 주기도 전에 파드를 죽이며, 메트릭 수집·스케일 제한·노드 프로비저닝·파드 기동의 지연이 겹겹이 쌓이는 동안 캐시 쇄도가 클러스터를 연쇄 붕괴시킨다. 그래서 초급증 트래픽에 대한 답은 더 좋은 오토스케일러 하나가 아니라 계층 방어다. KEDA로 큐 길이 같은 선행 지표를 스케일링에 연결하고, 메시지 브로커로 트래픽 파도를 완만한 언덕으로 바꾸고, 동기적 상호작용이 필요한 곳에는 가상 대기열을 세우고, 엣지에서 정적 트래픽과 악성 요청을 걸러내며, 더미 파드로 노드 프로비저닝 지연까지 선지불해 둔다. HPA의 수식은 이 방어선들 뒤에서 비로소 제 역할을 한다.
출처
- google/cadvisor (no date) Analyzes resource usage and performance characteristics of running containers. GitHub. Available at: https://github.com/google/cadvisor (Accessed: 26 August 2026).
- KEDA Authors (no date) Getting Started. KEDA documentation, v2.20. Available at: https://keda.sh/docs/ (Accessed: 26 August 2026).
- Kubernetes (2026) Horizontal Pod Autoscaling. Available at: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ (Accessed: 26 August 2026).
- Kubernetes (2026) Node metrics data. Available at: https://kubernetes.io/docs/reference/instrumentation/node-metrics/ (Accessed: 26 August 2026).
- Kubernetes (2026) Resource metrics pipeline. Available at: https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/ (Accessed: 26 August 2026).
- Kubernetes (no date) Cluster Architecture. Available at: https://kubernetes.io/docs/concepts/architecture/#kubelet (Accessed: 26 August 2026).
Footnotes
-
압축 불가능하다는 것은 할당된 리소스의 양을 줄일 수 없다는 뜻이다. ↩
-
OOM(Out-Of-Memory) 킬러는 시스템의 가용 메모리가 고갈되었을 때 리눅스 커널이 전체 시스템을 보호하기 위해 특정 프로세스를 골라 강제 종료하는 메커니즘이다. 컨테이너 환경에서는 메모리 limit을 초과한 컨테이너가 우선 대상이 되며, 이렇게 종료된 파드는 OOMKilled 상태로 표시된다. ↩
-
Readiness Probe는 쿠버네티스가 파드가 트래픽을 받을 준비가 되었는지 주기적으로 검사하는 장치다. 이 검사를 통과하기 전까지 파드는 서비스의 로드밸런싱 대상에 포함되지 않는다. ↩
-
CQRS(Command Query Responsibility Segregation)는 데이터를 변경하는 명령(command)과 조회하는 질의(query)의 책임을 분리하는 아키텍처 패턴이다. 대기열 시스템에서는 토큰 발급·전이(명령)와 순번 조회(질의)의 경로를 분리해 각각을 독립적으로 확장할 수 있다. ↩
-
stale-while-revalidate는 캐시가 만료되었을 때 우선 만료된(stale) 값을 즉시 반환하고, 백그라운드에서 새 값을 다시 계산(revalidate)해 캐시를 갱신하는 전략이다. 사용자는 응답 지연 없이 데이터를 받고, 캐시는 곧 최신화된다. ↩
-
pause컨테이너는 쿠버네티스에서 파드의 네트워크 네임스페이스를 붙잡아 두는 용도로 쓰이는, 사실상 아무 일도 하지 않는 초경량 컨테이너다. 오버프로비저닝에서는 이 컨테이너에 리소스 요청량만 크게 설정해 자리 차지용으로 활용한다. ↩