Kafka consumer를 K8s에서 돌리다 보면 결국 한 번은 마주치는 질문이 있다. "왜 lag이 쌓이는데 파드는 안 늘어나지?" HPA에 CPU 기준 스케일링을 걸어봐야 소용없다. consumer는 대부분 I/O bound라 lag이 폭증해도 CPU는 40%대에서 안정적일 수 있다.
이 글은 KEDA(Kubernetes Event-Driven Autoscaler)를 실제 운영 환경에 붙일 때 알아야 할 것들을 정리한 가이드다. 공식 문서에 잘 나온 기본 설치법이 아니라, 실무에서 자주 밟는 함정 중심으로 썼다. KEDA 2.15 기준.
HPA 대신 KEDA를 쓰는 이유
기본 HPA는 CPU/메모리, 그리고 custom metrics API를 통한 임의 지표까지 지원한다. 그럼 왜 굳이 KEDA를 별도로 붙여야 할까?
솔직히 말하면 custom metrics adapter를 직접 붙여서 Kafka lag 지표를 노출하는 것도 가능하다. Prometheus adapter를 세팅하고, kafka-lag-exporter로 lag을 뽑아서, HPA의 external metrics에 물릴 수 있다. 예전엔 다들 그렇게 했다. 근데 파이프라인이 길다. exporter → Prometheus → adapter → HPA 이 흐름 어딘가에서 문제가 생기면 스케일링이 멈춘다. 어디서 끊겼는지 추적하는 것도 일이다.
KEDA는 이 파이프라인을 통째로 걷어낸다. ScaledObject CR을 하나 만들면, KEDA operator가 Kafka broker에 직접 붙어서 lag을 폴링하고, external metrics server 역할까지 겸한다. HPA는 그 external metric을 그대로 소비한다. 컴포넌트가 하나로 압축되니 디버깅 지점도 줄어든다.
또 하나 중요한 게 scale-to-zero다. 기본 HPA는 minReplicas가 1 이상이어야 한다. 새벽에 트래픽이 거의 없는 이벤트 처리 워크로드라면 파드를 0으로 내리고 싶은데, HPA만으로는 안 된다. KEDA는 이 부분을 처리한다. lag이 0이면 파드를 0으로 내리고, 새 메시지가 들어오면 다시 띄운다.
최소 설정으로 시작하기
먼저 KEDA를 설치한다. Helm으로 붙이는 게 가장 편하다.
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda \
--namespace keda --create-namespace \
--version 2.15.1
설치가 끝나면 keda-operator, keda-operator-metrics-apiserver, keda-admission-webhooks 세 파드가 뜬다. metrics-apiserver가 HPA에게 external metrics를 노출하는 컴포넌트다. 이게 안 뜨면 스케일링이 아예 안 되니 로그부터 확인하자.
ScaledObject는 이렇게 생겼다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
namespace: prod
spec:
scaleTargetRef:
name: order-consumer
minReplicaCount: 1
maxReplicaCount: 20
pollingInterval: 30
cooldownPeriod: 300
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-broker.kafka.svc:9092
consumerGroup: order-consumer
topic: orders
lagThreshold: "500"
offsetResetPolicy: latest
lagThreshold: 500은 파드 한 대당 처리 여유가 lag 500까지라는 뜻이다. lag이 5000까지 쌓이면 파드 10대가 목표가 된다. 이게 헷갈리는 부분인데, HPA의 targetValue 개념이 그대로 온다고 보면 된다.
lagThreshold를 정하는 방법
가이드마다 "적절한 값을 넣으세요"라고만 되어 있다. 근데 이게 실무에서 제일 애매하다. 우리 팀은 결국 이렇게 정한다.
파드 하나가 초당 몇 개의 메시지를 처리할 수 있는지 먼저 측정한다. 벤치마크가 아니라 프로덕션 P95 처리량이다. 예를 들어 파드 하나가 초당 100 msg/s를 처리하고, 지연 목표가 5초라면 lagThreshold는 500이 된다. 100 msg/s × 5s = 500.
그런데 여기서 실수하기 쉽다. Kafka lag은 파티션별로 계산되고 파드 수는 파티션 수를 넘을 수 없다. 파티션이 12개인 토픽에 maxReplicaCount를 50으로 걸어봐야 소용없다. 파드 12대 이상은 컨슈머 그룹에서 idle 상태로 남는다. 실제로 이걸 놓쳐서 "왜 파드 30대 떴는데 lag은 안 줄지?" 하고 헤매는 경우가 흔하다.
파티션 개수 = maxReplicaCount의 상한선이라고 생각하고 설계하자. 트래픽이 폭증할 걸 예상한다면 애초에 토픽을 만들 때 파티션을 넉넉하게 잡아야 한다.
cooldown과 stabilization 튜닝
cooldownPeriod: 300은 마지막 트리거가 발생한 후 5분 동안 lag이 임계값 아래면 파드를 minReplica로 내린다는 뜻이다. 이걸 너무 짧게 잡으면 flapping이 생긴다. 30초마다 파드가 3대 → 8대 → 3대 → 10대로 요동친다.
우리는 처음에 60초로 시작했다가 파드가 계속 재시작되면서 rebalance가 폭발했다. Kafka consumer group rebalance는 파드가 들락날락할 때마다 발생하고, rebalance 중에는 모든 파드가 메시지 처리를 멈춘다. 결국 스케일링이 오히려 지연을 늘리는 역설적 상황이었다.
cooldownPeriod는 5분 이상 잡는 걸 추천한다. 그리고 scale-up 쪽은 HPA behavior 필드로 별도 제어할 수 있다.
spec:
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp은 빠르게, scaleDown은 천천히. 이 조합이 대부분의 consumer 워크로드에서 잘 맞는다.
activationLagThreshold와 scale-to-zero
scale-to-zero를 쓰려면 minReplicaCount: 0으로 두고 activationLagThreshold를 별도로 지정해야 한다. 이 두 값을 헷갈리는 사람이 정말 많다.
lagThreshold: 파드가 이미 떠 있을 때, 몇 개의 파드를 유지할지 결정하는 값activationLagThreshold: 파드가 0일 때, 새로 띄울지 말지 결정하는 값
activationLagThreshold가 없으면 lag이 1만 있어도 파드가 튀어오른다. 이러면 하루에 수십 번 파드가 뜨고 내리는 상황이 벌어진다. 우리 팀은 이 값을 lagThreshold의 20% 정도로 잡는다. 예를 들어 lagThreshold가 500이면 activationLagThreshold는 100. 어느 정도 배치가 쌓여야 파드를 띄운다.
triggers:
- type: kafka
metadata:
lagThreshold: "500"
activationLagThreshold: "100"
인증 처리
SASL/SCRAM이나 mTLS를 쓰는 프로덕션 클러스터에서는 TriggerAuthentication을 별도로 만들어야 한다. Secret에 credential을 넣고 ScaledObject가 참조한다.
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: kafka-auth
spec:
secretTargetRef:
- parameter: sasl
name: kafka-secret
key: sasl
- parameter: username
name: kafka-secret
key: username
- parameter: password
name: kafka-secret
key: password
여기서 자주 겪는 문제 하나. KEDA operator가 Kafka broker에 붙지 못하면 스케일링이 조용히 멈춘다. 에러가 파드 이벤트로 안 나오는 경우가 많다. kubectl describe scaledobject와 KEDA operator 로그를 항상 같이 봐야 한다.
kubectl logs -n keda deploy/keda-operator | grep -i kafka
관측성
KEDA는 자체 Prometheus 메트릭을 노출한다. keda_scaler_metrics_value가 각 트리거의 현재 값을 보여주고, keda_scaled_object_errors가 문제가 있을 때 튄다. Grafana 대시보드는 KEDA 공식 저장소에 예제가 있으니 그대로 가져다 쓰면 된다.
우리 팀은 여기에 하나 더 추가했다. kafka_consumergroup_lag를 kafka-exporter로 뽑아서 KEDA 스케일링과 실제 lag을 겹쳐서 본다. KEDA가 판단하는 lag과 exporter가 보는 lag이 어긋나는 경우가 가끔 있는데, 이때가 진짜 디버깅이 필요한 시점이다.
마무리
KEDA 자체는 붙이기 쉽다. 어려운 건 lagThreshold, cooldown, activation을 워크로드에 맞게 튜닝하는 부분이다. 처음에는 보수적인 값으로 시작해서 실제 트래픽 패턴을 보면서 조정하는 게 안전하다.
한 가지 더. scale-to-zero가 매력적이라고 무조건 쓰지는 말자. 새 파드가 뜨는 데 걸리는 시간(image pull + JVM warm-up 등) 동안 lag은 계속 쌓인다. cold start가 30초 이상 걸리는 서비스라면 minReplica를 1이나 2로 유지하는 게 오히려 SLO에 유리하다.
다음 글에서는 KEDA ScaledJob으로 배치 작업 스케일링 하는 방법도 다뤄보려고 한다. ScaledObject랑 비슷하면서도 미묘하게 다른 게 많다.
'IT > Kubernets' 카테고리의 다른 글
| CoreDNS 죽지도 않았는데 DNS가 느려서 새벽에 깬 이야기 (0) | 2026.07.29 |
|---|---|
| HPA scale-down이 너무 공격적이라면, behavior 필드부터 보자 (0) | 2026.07.28 |
| kubectl debug --profile, 이거 모르면 매번 privileged 파드 만들고 있는 거다 (0) | 2026.07.20 |
| cert-manager 무한 재발급 루프에 걸린 새벽 이야기 (0) | 2026.07.19 |
| ValidatingAdmissionPolicy vs Kyverno, 우리 팀은 이렇게 선을 그었다 (0) | 2026.07.17 |