Published on

EKS HPA 폭주를 KEDA 큐기반 오토스케일링으로 안정화

Authors

서버리스/마이크로서비스 환경에서 오토스케일링은 비용과 안정성을 동시에 좌우합니다. 특히 EKS에서 HPA(Horizontal Pod Autoscaler) 를 CPU/메모리 같은 리소스 지표로만 걸어두면, 트래픽이 ‘요청-대기열-워커’ 구조로 흘러가는 워크로드(비동기 작업 처리, 이벤트 소비, 배치/ETL, 메시지 브로커 컨슈머 등)에서는 스케일이 폭주(thrashing) 하거나 반대로 늦게 따라가서 지연이 누적되는 일이 흔합니다.

이 글에서는 HPA 폭주의 전형적인 원인을 짚고, KEDA(Kubernetes Event-driven Autoscaling) 를 이용해 큐 기반(Queue-based) 오토스케일링으로 전환/병행하여 스케일링을 안정화하는 방법을 EKS 관점에서 정리합니다.

HPA가 “폭주”하는 전형적인 시나리오

1) CPU/메모리 지표가 작업량을 대변하지 못함

비동기 워커는 보통 다음 흐름입니다.

  • API/Producer가 메시지를 큐(SQS/Kafka/RabbitMQ 등)에 적재
  • 워커(Consumer)가 큐에서 가져와 처리

이때 실제로 사용자가 체감하는 병목은 큐 적체(백로그) 인데, HPA는 기본적으로 CPU/메모리 사용률만 보고 결정합니다.

  • 메시지가 쌓이는데도 워커가 I/O 대기(외부 API, DB, S3 등)로 CPU 사용률이 낮으면 → HPA는 “여유 있음”으로 판단하여 스케일을 안 올림
  • 반대로 특정 순간에 GC, 압축/암호화, JSON 파싱 등으로 CPU가 튀면 → HPA가 급격히 스케일 아웃 → 잠시 후 부하가 떨어지면 다시 스케일 인 → 반복(폭주)

2) 지표 수집/반영 지연 + 쿨다운 부재

HPA는 metrics-server/Prometheus Adapter 등에서 지표를 가져오고, 일정 주기마다 평가합니다. 지표가 늦게 반영되거나(수집 지연), 워커가 뜨는 시간이 길면(이미지 pull, init, JIT warm-up) “늦게 과하게” 반응합니다.

3) 워커의 동시성(concurrency)과 처리시간 분산

메시지 처리시간이 분산이 크면(롱테일), CPU 기반 스케일은 더 흔들립니다.

  • 짧은 작업이 몰릴 때 CPU 급등 → 과도한 스케일 아웃
  • 긴 작업이 몰릴 때 CPU는 낮아도 처리량 부족 → 큐 적체 증가

4) 스케일 인이 너무 빨라 “처리 중인 작업”을 죽임

HPA는 스케일 인 시점에 파드를 줄입니다. 워커가 graceful drain(큐에서 더 이상 가져오지 않기, in-flight 처리 완료 후 종료)을 제대로 구현하지 않으면, 처리 중이던 작업이 중단되어 재시도/중복처리로 이어지고, 그 자체가 다시 부하를 만들며 스케일이 더 불안정해집니다.

> 워커 종료/재기동 문제까지 함께 얽히면 장애가 장기화됩니다. 파드가 반복 재기동하는 상황은 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅 체크리스트도 같이 보시면 원인 분리가 빠릅니다.

왜 KEDA가 큐 기반 워크로드에 유리한가

KEDA는 “이벤트 소스”를 기준으로 스케일링합니다. 즉, 큐 길이/레이트/라그(lag) 같은 업무량 지표를 스케일 트리거로 사용합니다.

  • SQS: ApproximateNumberOfMessagesVisible, 메시지 age 등
  • Kafka: consumer lag
  • RabbitMQ: queue depth
  • Prometheus: 사용자 정의 지표(예: backlog, processing latency)

핵심은 다음입니다.

  1. 스케일 기준이 ‘해야 할 일의 양’ 이다 → CPU가 낮아도 큐가 쌓이면 스케일 아웃
  2. 스케일 0(필요 시)까지 지원 → 야간/무트래픽 비용 최적화
  3. HPA와 결합 가능(내부적으로 HPA를 생성/관리) → 운영 모델이 쿠버네티스 표준에 가깝다

목표 아키텍처: HPA 단독 → KEDA 중심(또는 병행)

권장 패턴은 크게 2가지입니다.

패턴 A) 워커는 KEDA(큐 기반)로만 스케일

  • 워커 Deployment에는 CPU 기반 HPA를 걸지 않음
  • KEDA ScaledObject/ScaledJob로 큐 깊이에 따라 레플리카 조절

패턴 B) 워커는 KEDA + HPA(리소스 안전장치) 병행

  • 큐 기반으로 스케일하되,
  • CPU/메모리 기반 HPA는 “폭주 방지용 상한/완충”으로 제한적으로 사용

실무에서는 패턴 A가 단순하고 안정적이며, 워커가 CPU 바운드인 특수 케이스만 패턴 B를 고려합니다.

EKS에 KEDA 설치 (Helm)

EKS에서 KEDA는 보통 Helm으로 설치합니다.

helm repo add kedacore https://kedacore.github.io/charts
helm repo update

kubectl create namespace keda

helm install keda kedacore/keda \
  --namespace keda \
  --set crds.install=true

설치 확인:

kubectl get pods -n keda
kubectl get crd | grep keda

예제 1) SQS 큐 길이 기반 워커 스케일링 (ScaledObject)

1) 워커 Deployment (graceful shutdown 포함)

워커는 SIGTERM 처리, terminationGracePeriodSeconds, preStop 등을 통해 스케일 인 시 작업 유실을 줄여야 합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sqs-worker
  namespace: workers
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sqs-worker
  template:
    metadata:
      labels:
        app: sqs-worker
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: worker
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/sqs-worker:1.0.0
          env:
            - name: QUEUE_URL
              value: https://sqs.ap-northeast-2.amazonaws.com/123456789012/my-queue
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "echo 'draining...' && sleep 20"]
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"
            limits:
              cpu: "1000m"
              memory: "512Mi"

preStop은 예시입니다. 실제로는 애플리케이션 레벨에서 “더 이상 메시지 poll 하지 않기 + in-flight 처리 완료 후 종료”를 구현하는 것이 가장 안전합니다.

2) AWS 인증: IRSA 권장

KEDA가 SQS를 읽으려면 AWS API 호출 권한이 필요합니다. EKS에서는 IRSA(IAM Roles for Service Accounts) 로 권한을 부여하는 것이 정석입니다.

  • KEDA operator/trigger-auth가 사용할 ServiceAccount에 IAM Role 연동
  • 해당 Role에 sqs:GetQueueAttributes, sqs:GetQueueUrl 등 최소 권한 부여

IRSA 구성에서 STS AccessDenied가 자주 발생하므로, 막히면 EKS IRSA 설정했는데 STS AccessDenied 뜰 때 체크 포인트대로 trust policy/oidc/sub 조건을 먼저 확인하세요.

3) TriggerAuthentication + ScaledObject

apiVersion: v1
kind: ServiceAccount
metadata:
  name: keda-sqs-sa
  namespace: workers
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/keda-sqs-reader
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: keda-sqs-auth
  namespace: workers
spec:
  podIdentity:
    provider: aws-eks
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: sqs-worker-scaledobject
  namespace: workers
spec:
  scaleTargetRef:
    name: sqs-worker
  pollingInterval: 15          # 큐 지표 폴링 주기(초)
  cooldownPeriod: 120          # 스케일 인까지 대기(초)
  minReplicaCount: 0
  maxReplicaCount: 50
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 180
          policies:
            - type: Percent
              value: 20
              periodSeconds: 60
        scaleUp:
          stabilizationWindowSeconds: 0
          policies:
            - type: Percent
              value: 100
              periodSeconds: 30
  triggers:
    - type: aws-sqs-queue
      authenticationRef:
        name: keda-sqs-auth
      metadata:
        queueURL: https://sqs.ap-northeast-2.amazonaws.com/123456789012/my-queue
        queueLength: "50"       # 메시지 50개당 1 replica 수준의 의미(트리거 기준)
        awsRegion: ap-northeast-2

파라미터 해석 팁

  • queueLength: “이 값 이상이면 스케일”이 아니라, HPA 메트릭 목표치처럼 동작합니다. 대략적으로 desiredReplicas ≈ currentQueueLength / queueLength에 가까운 방향으로 조정됩니다(정확한 동작은 트리거 구현에 따라 다를 수 있음).
  • cooldownPeriodstabilizationWindowSeconds는 폭주 방지에 매우 중요합니다.
    • 큐가 순간적으로 줄었다고 바로 스케일 인하지 않게 만들면, 워커 재기동/드레인 비용을 줄이고 처리량 변동을 완화합니다.

예제 2) Kafka consumer lag 기반 스케일링 (개념)

Kafka는 “큐 길이”보다 consumer lag가 핵심 지표입니다. KEDA Kafka 트리거를 사용하면 lag가 특정 임계치를 넘을 때 컨슈머 수를 늘릴 수 있습니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-worker
  namespace: workers
spec:
  scaleTargetRef:
    name: kafka-consumer
  minReplicaCount: 1
  maxReplicaCount: 100
  pollingInterval: 10
  cooldownPeriod: 180
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: my-kafka:9092
        consumerGroup: my-group
        topic: my-topic
        lagThreshold: "5000"

운영 포인트는 “파티션 수”와 “컨슈머 수”의 상한입니다. 파티션보다 컨슈머가 많아도 처리량이 늘지 않으므로, maxReplicaCount는 토픽 파티션 수/처리 모델에 맞춰 제한하는 것이 좋습니다.

HPA 폭주를 줄이는 KEDA 튜닝 체크리스트

1) 스케일 인을 느리게, 스케일 아웃은 빠르게

  • scale out: backlog가 쌓이면 빠르게 늘려 지연을 줄임
  • scale in: backlog가 잠깐 줄어도 천천히 줄여 안정화

위에서 사용한 cooldownPeriod, stabilizationWindowSeconds, scaleDown policies가 핵심입니다.

2) 워커 1개당 처리량을 먼저 측정하고 목표를 역산

큐 기반 스케일링은 “큐 길이 → 레플리카” 매핑이 전부입니다.

  • 워커 1개가 초당 처리 가능한 메시지 수(steady state)
  • 평균 처리시간/95p 처리시간
  • 외부 의존성(DB/API)의 QPS 제한

예를 들어 워커 1개가 평균 10 msg/s 처리 가능하고, “최대 2분 내 backlog 해소”가 목표라면:

  • backlog 12,000개일 때 필요한 워커 수 ≈ 12,000 / (10*120) = 10개
  • 이를 기준으로 queueLength를 대략 1,200 정도로 잡고, 실제 운영에서 조정합니다.

3) 외부 시스템이 병목이면 ‘스케일 아웃’이 오히려 장애를 키움

큐가 쌓인다고 무조건 워커를 늘리면, DB 커넥션/외부 API rate limit에 걸려 실패/재시도가 증가하면서 backlog가 더 늘 수 있습니다.

  • 워커 concurrency 제한(스레드/async 동시 처리 수)
  • DB pool 상한
  • 외부 API 호출 rate limit

네트워크/프록시 계층에서 gRPC/HTTP2 오류가 증가한다면, 단순 스케일 문제인지도 같이 봐야 합니다. 관련해서는 Kubernetes gRPC UNAVAILABLE·RST_STREAM 원인과 Envoy·NGINX 대응도 참고할 만합니다.

4) 메모리 누수/GC 폭주가 섞이면 스케일링이 더 요동침

스케일이 흔들리는 와중에 OOMKilled가 나면 재시도/재처리로 큐가 더 쌓이고, KEDA가 더 스케일 아웃하는 악순환이 생깁니다. 워커가 자주 죽는다면 먼저 메모리/누수 여부를 분리 진단하세요: Kubernetes OOMKilled 진단과 메모리 누수 추적 실전

운영에서 자주 겪는 함정과 해결 방향

함정 1) minReplicaCount=0에서 콜드스타트가 SLA를 깨뜨림

스케일 0은 비용 최적화에 좋지만, 워커 기동(이미지 pull, JIT, 커넥션 워밍업) 시간이 길면 backlog가 생긴 뒤 첫 처리까지 지연이 커집니다.

  • SLA가 빡빡하면 minReplicaCount: 1~2로 두고
  • 야간에만 0으로 내리는 스케줄링(별도 CronJob/자동화)도 고려합니다.

함정 2) 큐 지표가 “근사치”라서 튄다

SQS의 ApproximateNumberOfMessagesVisible은 말 그대로 근사치입니다. 폴링 주기/쿨다운/안정화 창을 통해 튐을 흡수해야 합니다.

함정 3) 스케일 아웃은 됐는데 처리량이 안 오른다

대부분 아래 중 하나입니다.

  • 파티션/샤드 한계(Kafka 파티션 수)
  • 외부 의존성 병목(DB, API)
  • 워커 내부 락/싱글스레드 처리
  • 네트워크/NAT/SNAT 포화(EKS VPC CNI 구성)

이 경우 “스케일링 정책”이 아니라 “처리 파이프라인 설계/리소스 병목” 문제이므로, 지표를 나눠서 봐야 합니다.

마이그레이션 전략: HPA 폭주 환경에서 안전하게 KEDA 도입하기

  1. 현재 HPA 이벤트/레플리카 변동 로그를 수집
    • kubectl describe hpa ...
    • 스케일 아웃/인 빈도, 최대치, 지표 튐 확인
  2. 워커의 처리량(throughput)과 실패율, 외부 의존성 QPS 측정
  3. KEDA를 먼저 낮은 maxReplicaCount로 제한해 canary 적용
  4. 안정화되면 HPA 제거 또는 CPU HPA를 “상한선/안전장치”로만 유지
  5. 스케일 인 시 작업 유실이 없도록 graceful drain을 반드시 구현

결론

EKS에서 HPA가 폭주하는 가장 흔한 이유는, HPA가 보는 지표(CPU/메모리)가 실제 업무량(큐 적체, lag, 메시지 age)과 어긋나기 때문입니다. KEDA는 이 간극을 메워 큐/이벤트 기반으로 스케일 결정을 내리게 해주며, cooldownPeriod와 HPA behavior(안정화 창/스케일 다운 정책)를 적절히 조합하면 “늘었다 줄었다”를 크게 줄일 수 있습니다.

정리하면 다음 3가지만 지키면 성공 확률이 높습니다.

  • 스케일 기준을 CPU가 아니라 backlog/lag로 바꾼다(KEDA)
  • 스케일 인을 느리게 해서 드레인/재시도 비용을 흡수한다
  • 워커 처리량과 외부 병목을 측정해 maxReplicaCount와 concurrency를 상한으로 둔다

원하시면 사용 중인 큐(SQS/Kafka/RabbitMQ), 워커 언어/프레임워크, 목표 SLA(최대 지연/처리시간), 현재 HPA 설정(yaml)을 알려주시면 그 조건에 맞춘 queueLength/lagThreshold, 쿨다운/안정화 창, graceful shutdown 패턴까지 더 구체적으로 튜닝 예시를 만들어 드릴게요.