ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • KEDA 트러블슈팅: RabbitMQ 기반 이벤트 드리븐 오토스케일링
    이코에코(Eco²)/Troubleshooting 2025. 12. 26. 19:43

    KEDA HTTP Add-on을 추가할 당시, KEDA 개발자가 포스팅한 디자인 아키텍처다. KEDA의 내부 통신(gRPC 파트)을 이해하는데 도움이 된다.

     

    본 문서는 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 management
    • 4f33e9ef 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 디버깅 체크리스트

    1. kubectl describe hpa - 메트릭 값 확인
    2. kubectl get scaledobject -o wide - ACTIVE 상태 확인
    3. kubectl logs -n keda deployment/keda-operator - Scaler 로그
    4. kubectl logs -n keda deployment/keda-operator-metrics-apiserver - 메트릭 서버 로그
    5. 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. 참고 자료

     

    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

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango