KEDA 트러블슈팅: RabbitMQ 기반 이벤트 드리븐 오토스케일링

본 문서는 Kubernetes 환경에서 KEDA(Kubernetes Event-driven Autoscaling)를 활용하여 RabbitMQ 큐 길이 기반 Worker Pod 오토스케일링을 구현하는 과정에서 발생한 이슈와 해결 방법을 기술합니다.
환경 정보
| 구성 요소 | 버전/사양 |
|---|---|
| Kubernetes | v1.28.4 (kubeadm) |
| KEDA | v2.16.0 |
| RabbitMQ | v3.13.x (RabbitMQ Operator) |
| ArgoCD | v2.13.x |
| CNI | Calico (NetworkPolicy 적용) |
목표
- CPU 기반 HPA의 한계를 극복하고 메시지 큐 길이 기반 오토스케일링 구현
- scan-worker의 동적 스케일링으로 부하 대응력 향상
- k6 부하 테스트 시 성공률 개선
1. 배경: CPU 기반 HPA의 한계
1.1 기존 구조
scan-worker는 Celery 기반 비동기 Worker로, RabbitMQ 큐에서 메시지를 소비하여 OpenAI API 호출 등 I/O 집약적 작업을 수행한다.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ scan-api │────►│ RabbitMQ │────►│ scan-worker │
│ (Producer) │ │ (Queue) │ │ (Consumer) │
└─────────────┘ └─────────────┘ └─────────────┘
│
scan.vision: 22
scan.answer: 16
scan.rule: 14
1.2 문제점
k6 부하 테스트(50 VUs, 3분) 결과:
| 지표 | 값 |
|---|---|
| 총 요청 | 643 |
| 성공 | 452 (70.3%) |
| 실패 | 128 (19.9%) |
| 부분 완료 | 63 (9.8%) |
CPU 기반 HPA가 효과적이지 않은 이유:
- OpenAI API 호출 대기 시간이 대부분 (30-50초)
- CPU 사용률은 낮게 유지 (10-15%)
- 큐에 메시지가 쌓여도 스케일업 트리거 안 됨
1.3 해결 방안: KEDA 도입
KEDA는 외부 이벤트 소스(RabbitMQ, Kafka, Prometheus 등)를 기반으로 HPA를 생성하여 이벤트 드리븐 스케일링을 지원한다.
2. 초기 구현
2.1 ScaledObject 정의
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: scan-worker-scaledobject
namespace: scan
spec:
scaleTargetRef:
name: scan-worker
kind: Deployment
minReplicaCount: 1
maxReplicaCount: 5
cooldownPeriod: 120
pollingInterval: 15
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: scan.vision
mode: QueueLength
value: '10'
authenticationRef:
name: rabbitmq-trigger-auth
2.2 TriggerAuthentication
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: rabbitmq-trigger-auth
namespace: scan
spec:
secretTargetRef:
- parameter: host
name: scan-secret
key: CELERY_BROKER_URL
3. Issue #1: 메트릭 수집 실패 (ScalerNotActive)
3.1 현상
ScaledObject 배포 후 ACTIVE 상태가 False로 유지:
$ kubectl get scaledobject -n scan -o wide
NAME SCALETARGETKIND SCALETARGETNAME MIN MAX READY ACTIVE
scan-worker-scaledobject apps/v1.Deployment scan-worker 1 5 True False
HPA 메트릭이 모두 0으로 표시:
$ kubectl describe hpa keda-hpa-scan-worker-scaledobject -n scan
Metrics:
"s0-rabbitmq-scan-vision" (target average value): 0 / 10
"s1-rabbitmq-scan-answer" (target average value): 0 / 10
"s2-rabbitmq-scan-rule" (target average value): 0 / 20
3.2 원인 분석
RabbitMQ 큐 상태를 직접 조회한 결과:
$ kubectl exec -n rabbitmq eco2-rabbitmq-server-0 -- \
rabbitmqctl list_queues name messages_ready messages_unacknowledged -p eco2
QUEUE READY UNACKED
scan.vision 0 22
scan.answer 0 16
scan.rule 0 14
scan.reward 0 9
핵심 발견: messages_ready=0, messages_unacknowledged=N
Celery Worker 설정:
# domains/_shared/celery/config.py
worker_prefetch_multiplier = 1 # greenlet당 1개 prefetch
task_acks_late = True
Worker는 gevent pool(100 greenlets)을 사용하며, 메시지를 prefetch하여 처리한다. 이로 인해 모든 메시지가 unacked 상태로 전환된다.
3.3 KEDA RabbitMQ Scaler 동작 분석
KEDA의 AMQP 프로토콜 스케일러 동작:
// KEDA 소스 코드 참고
func (s *rabbitMQScaler) GetQueueInfo() (int, error) {
// pika.BlockingConnection 사용
// channel.queue_declare(passive=True) 호출
// → messages_ready만 반환
}
| 프로토콜 | 측정 대상 | 사용 API |
|---|---|---|
| AMQP | messages_ready |
queue_declare (passive) |
| HTTP | messages (ready + unacked) |
Management API |
3.4 해결책: HTTP 프로토콜 전환
triggers:
- type: rabbitmq
metadata:
protocol: http
host: http://admin:***@eco2-rabbitmq.rabbitmq.svc.cluster.local:15672
queueName: scan.vision
vhostName: eco2
mode: QueueLength
value: '10'
HTTP Management API 응답 예시:
{
"name": "scan.vision",
"messages": 22,
"messages_ready": 0,
"messages_unacknowledged": 22,
"consumers": 1
}
3.5 결과
$ kubectl describe hpa keda-hpa-scan-worker-scaledobject -n scan
Metrics:
"s0-rabbitmq-scan-vision" (target average value): 22 / 10 ✓
"s1-rabbitmq-scan-answer" (target average value): 16 / 10 ✓
"s2-rabbitmq-scan-rule" (target average value): 14 / 20
커밋: c6245e99 fix(keda): switch to HTTP protocol for RabbitMQ metrics
4. Issue #2: KEDA 내부 gRPC 통신 타임아웃
4.1 현상
HTTP 프로토콜 전환 후에도 메트릭 수집이 간헐적으로 실패:
$ kubectl logs -n keda deployment/keda-operator-metrics-apiserver --tail=20
E1226 10:15:00 provider.go:90] "timeout"
"error"="timeout while waiting to establish gRPC connection to KEDA Metrics Service server"
"server"="keda-operator.keda.svc.cluster.local:9666"
W1226 10:15:02 logging.go:55] grpc: addrConn.createTransport failed to connect to
{Addr: "10.103.57.99:9666"...}
Err: dial tcp 10.103.57.99:9666: i/o timeout
4.2 KEDA 아키텍처
KEDA는 2개의 주요 컴포넌트로 구성된다:
┌─────────────────────────────────────────────────────────────────┐
│ KEDA Namespace │
│ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ keda-operator │◄──gRPC──│ keda-metrics-apiserver │ │
│ │ │ :9666 │ │ │
│ │ - ScaledObject 감시 │ │ - External Metrics API │ │
│ │ - Scaler 실행 │ │ - HPA 메트릭 제공 │ │
│ │ - RabbitMQ HTTP 호출 │ │ │ │
│ └───────────┬────────────┘ └────────────────────────┘ │
│ │ │
│ │ HTTP :15672 │
│ ▼ │
└──────────────┼──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RabbitMQ Namespace │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ eco2-rabbitmq:15672 (Management API) │ │
│ │ eco2-rabbitmq:5672 (AMQP) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
4.3 원인 분석
기존 NetworkPolicy 설정:
# allow-keda-egress.yaml (수정 전)
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: rabbitmq
ports:
- port: 5672 # AMQP
- port: 15672 # HTTP Management API
누락된 설정: KEDA 내부 컴포넌트 간 gRPC 통신(port 9666)
4.4 해결책
# allow-keda-egress.yaml (수정 후)
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# KEDA 내부 gRPC 통신
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: keda
ports:
- protocol: TCP
port: 9666 # gRPC metrics service
- protocol: TCP
port: 8080 # Prometheus metrics
# RabbitMQ 접근
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: rabbitmq
ports:
- protocol: TCP
port: 5672
- protocol: TCP
port: 15672
4.5 결과
$ kubectl get scaledobject -n scan -o wide
NAME SCALETARGETKIND SCALETARGETNAME MIN MAX READY ACTIVE
scan-worker-scaledobject apps/v1.Deployment scan-worker 1 5 True True ✓
커밋: 81c0adde fix(network-policy): allow KEDA internal gRPC communication (9666)
5. Issue #3: ArgoCD selfHeal과 KEDA 스케일링 충돌
5.1 현상
메트릭 수집 성공 후 스케일업이 발생하지만, 즉시 스케일다운:
$ kubectl get events -n scan --sort-by=.lastTimestamp | grep -i scaled
28s Normal SuccessfulRescale New size: 3; reason: s0-rabbitmq-scan-vision above target
28s Normal ScalingReplicaSet Scaled up replica set scan-worker to 3 from 1
27s Normal ScalingReplicaSet Scaled down replica set scan-worker to 1 from 3
스케일업 후 1초 만에 스케일다운 발생. HPA의 stabilizationWindowSeconds: 300 설정이 무시됨.
5.2 원인 분석
ArgoCD Application 설정 확인:
$ kubectl get application dev-scan-worker -n argocd -o jsonpath="{.spec.syncPolicy}" | jq
{
"automated": {
"prune": true,
"selfHeal": true
}
}
ArgoCD 동작 시퀀스:
Timeline
────────────────────────────────────────────────────────────────────
T+0s KEDA HPA: replicas 1 → 3 (메트릭 기반 스케일업)
T+1s ArgoCD: Drift 감지 (클러스터 상태 ≠ Git 상태)
- 클러스터: replicas=3
- Git: replicas=1 (workloads/domains/scan-worker/base/deployment.yaml)
T+1s ArgoCD: selfHeal=true → 자동 동기화 실행
T+2s Deployment: replicas 3 → 1 (Git 상태로 복원)
────────────────────────────────────────────────────────────────────
5.3 해결책: ignoreDifferences 설정
ArgoCD ApplicationSet에 ignoreDifferences 추가:
# clusters/dev/apps/41-workers-appset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
spec:
template:
spec:
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
5.4 주의사항
ApplicationSet은 기존 Application을 자동으로 업데이트하지 않는다. 기존 Application에 수동 패치 필요:
kubectl patch application dev-scan-worker -n argocd --type=json \
-p '[{
"op": "add",
"path": "/spec/ignoreDifferences",
"value": [{
"group": "apps",
"kind": "Deployment",
"jsonPointers": ["/spec/replicas"]
}]
}]'
5.5 결과
$ kubectl get pods -n scan -l app=scan-worker
NAME READY STATUS RESTARTS AGE
scan-worker-69ff8ccc9d-djfps 2/2 Running 0 87s
scan-worker-69ff8ccc9d-jnj9z 2/2 Running 0 34m
scan-worker-69ff8ccc9d-mshx4 2/2 Running 0 87s
$ kubectl exec -n rabbitmq eco2-rabbitmq-server-0 -- \
rabbitmqctl list_queues name consumers -p eco2 | grep scan
scan.vision 3
scan.answer 3
scan.rule 3
scan.reward 3
3개의 Worker Pod가 안정적으로 유지되며, 각 큐에 3개의 Consumer가 연결됨.
커밋:
40824966 fix(argocd): ignore replicas field for KEDA/HPA management4f33e9ef fix(argocd): ignore replicas for worker deployments (KEDA/HPA)
6. 최종 구성
6.1 ScaledObject 최종 설정
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: scan-worker-scaledobject
namespace: scan
spec:
scaleTargetRef:
name: scan-worker
kind: Deployment
minReplicaCount: 2
maxReplicaCount: 5
cooldownPeriod: 300
pollingInterval: 30
fallback:
failureThreshold: 3
replicas: 2
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 2
periodSeconds: 30
triggers:
- type: rabbitmq
metadata:
protocol: http
host: http://admin:***@eco2-rabbitmq.rabbitmq.svc.cluster.local:15672
queueName: scan.vision
vhostName: eco2
mode: QueueLength
value: '10'
6.2 설정값 설명
| 파라미터 | 값 | 설명 |
|---|---|---|
| minReplicaCount | 2 | 최소 Worker 수 (baseline) |
| maxReplicaCount | 5 | 최대 Worker 수 |
| cooldownPeriod | 300s | 스케일다운 전 대기 시간 |
| pollingInterval | 30s | 메트릭 수집 주기 |
| fallback.replicas | 2 | 메트릭 수집 실패 시 유지할 replicas |
| stabilizationWindowSeconds | 300s | 스케일다운 안정화 기간 |
| value | 10 | Pod당 처리할 메시지 수 기준 |
7. 검증 결과
7.1 스케일링 동작 확인
부하 발생 시:
$ kubectl get hpa -n scan -o wide
NAME REFERENCE TARGETS REPLICAS
keda-hpa-scan-worker-scaledobject Deployment/scan-worker 22/10, 16/10, 14/20 3
7.2 개선 지표
| 항목 | 수정 전 | 수정 후 |
|---|---|---|
| Worker replicas | 1 (고정) | 2-5 (동적) |
| Consumer/queue | 1 | 3+ |
| 메트릭 수집 | 실패 | 정상 (실시간) |
| 스케일업 유지 | 1초 만에 롤백 | 5분 안정화 |
| HPA 트리거 | CPU (비효율적) | Queue Length (효율적) |
8. 핵심 교훈
8.1 KEDA RabbitMQ Scaler
- AMQP 프로토콜은
messages_ready만 측정하므로 prefetch 환경에서는 HTTP 프로토콜 필수 mode: QueueLength는 프로토콜에 따라 측정 대상이 다름- TriggerAuthentication에서 vhost 설정 주의 (vhostName 파라미터)
8.2 NetworkPolicy
- KEDA는 operator와 metrics-apiserver 2개 컴포넌트로 구성
- 내부 gRPC 통신(port 9666)이 필수이며, 이에 대한 egress 정책 필요
- Egress 정책에서 자기 자신(namespace) 통신도 명시적으로 허용 필요
8.3 ArgoCD와 HPA/KEDA 공존
selfHeal: true는 HPA/KEDA의 replicas 변경을 되돌림ignoreDifferences로/spec/replicas필드를 동기화에서 제외- ApplicationSet 변경 시 기존 Application은 자동 업데이트되지 않음
8.4 디버깅 체크리스트
kubectl describe hpa- 메트릭 값 확인kubectl get scaledobject -o wide- ACTIVE 상태 확인kubectl logs -n keda deployment/keda-operator- Scaler 로그kubectl logs -n keda deployment/keda-operator-metrics-apiserver- 메트릭 서버 로그kubectl get events --sort-by=.lastTimestamp- 스케일링 이벤트 추적
9. 관련 리소스
9.1 변경 파일
| 파일 | 설명 |
|---|---|
workloads/scaling/base/scan-worker-scaledobject.yaml |
KEDA ScaledObject |
workloads/network-policies/base/allow-keda-egress.yaml |
NetworkPolicy |
clusters/dev/apps/41-workers-appset.yaml |
ArgoCD ApplicationSet |
9.2 관련 커밋
| 커밋 해시 | 설명 |
|---|---|
c6245e99 |
AMQP → HTTP 프로토콜 전환 |
81c0adde |
KEDA 내부 gRPC 통신 허용 (NetworkPolicy) |
227231f6 |
KEDA 안정화 설정 (fallback, cooldown) |
40824966 |
ArgoCD ignoreDifferences (APIs) |
4f33e9ef |
ArgoCD ignoreDifferences (Workers) |
10. 참고 자료
- KEDA Documentation - RabbitMQ Scaler
- AWS Blog - Autoscaling Kubernetes workloads with KEDA
- 여기어때 기술블로그 - KEDA 도입기
- ArgoCD Documentation - Diffing Customization
- Kubernetes HPA Behavior
- https://hackmd.io/@arschles/kedahttp
Adding HTTP Scaling & Ingress Functionality to KEDA - HackMD
# Adding HTTP Scaling & Ingress Functionality to KEDA >This is a proposal to add HTTP functionalit
hackmd.io