IT/Kubernets

KEDA SQS scaler에서 새벽에 만난 함정

gfrog 2026. 5. 11. 09:41
반응형

KEDA SQS scaler에서 새벽에 만난 함정

지난주 토요일 새벽 3시쯤이었다. 핸드폰 진동 한 번에 눈이 떠졌다. SQS DLQ 누적 알림. 큐 메시지가 12,000개 쌓여 있었고, 컨슈머 파드는 정확히 한 개. 분명 KEDA로 ScaledObject 걸어놨고, 두 달 동안 잘 돌던 워크로드인데 왜 안 늘어났을까. 멘탈이 좀 흔들렸다.

그래서 뭐가 문제였나

상황부터 정리하자. 우리 팀이 운영하는 컨슈머는 이미지 후처리(리사이즈 + 메타데이터 추출)를 하는 파이썬 워커다. SQS에서 메시지 꺼내서 S3 거쳐 DynamoDB 업데이트하는 평범한 패턴. KEDA 2.16으로 SQS scaler 붙여놨고 설정은 이랬다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: image-worker
spec:
  scaleTargetRef:
    name: image-worker
  minReplicaCount: 0
  maxReplicaCount: 30
  cooldownPeriod: 300
  triggers:
  - type: aws-sqs-queue
    metadata:
      queueURL: https://sqs.ap-northeast-2.amazonaws.com/.../image-jobs
      queueLength: "10"
      activationQueueLength: "5"
      awsRegion: ap-northeast-2
      identityOwner: operator

minReplicaCount: 0. 그래, scale-to-zero를 켜놨다. 비용 아끼겠다고 욕심 좀 부렸다.

근데 새벽에 갑자기 트래픽이 몰리면서 큐가 5,000개를 넘어가는데도 파드가 1개에서 멈춰 있었다. HPA는 이미 있고, ScaledObject도 멀쩡하고, KEDA operator 로그도 평소대로 흘러가는데.

원인을 찾기까지

처음엔 IAM 권한 문제인 줄 알았다. 작년에 한 번 IRSA 토큰 만료로 같은 증상 본 적 있어서. 그래서 KEDA operator 파드 로그부터 열었는데 sqs:GetQueueAttributes 호출은 정상이었다. CloudTrail까지 봤다. 멀쩡했다.

다음 의심은 메트릭 서버. external metrics API가 죽었나 싶어서 kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" 때려봤다. 잘 나왔다. KEDA가 보여주는 큐 길이도 정확했다. 5,000-something.

여기서 좀 멍해졌다. 큐는 5,000개라고 KEDA가 잘 알고 있는데 왜 파드는 안 늘지?

답은 ScaledObject status에 있었다. 자세히 봤더니 Active 상태가 False였다. 이게 무슨 소리야. 그 다음 줄을 봤다. LastActiveTime 필드 옆에 메시지가 쓰여 있었다 — 정확한 워딩은 기억 안 나는데, 요지는 "activation threshold 도달 안 함".

함정의 진짜 정체

여기서 KEDA의 활성화 로직을 다시 짚고 가야 한다. KEDA에는 두 개의 임계값이 있다.

첫 번째는 queueLength. 이건 얼마나 빠르게 늘릴지 결정하는 값이다. 메시지 10개당 파드 1개라고 알려주는 비율. HPA 식으로 풀면 target value다.

두 번째는 activationQueueLength. 이게 함정이다. scale-from-zero를 트리거하는 임계값. minReplicaCount가 0일 때만 의미 있다. 큐 길이가 이 값 미만이면 파드가 0개에 머문다. 이 값 이상이면 깨어난다.

여기까지는 문서대로다. 그런데 우리 케이스는 큐가 5,000개였는데도 1개에서 멈췄다. 0개도 아니고 1개. activation은 이미 진작에 통과한 상태였다는 뜻이다.

진짜 원인은 좀 더 미묘했다. SQS scaler가 큐 길이를 가져올 때 기본적으로 ApproximateNumberOfMessagesApproximateNumberOfMessagesNotVisible을 합쳐서 계산한다. 그런데 누가 KEDA 2.13쯤 도입된 scaleOnInFlight: false 옵션을 우리 ScaledObject에 박아둔 거였다. git blame으로 추적해보니 6주 전 다른 팀원이 in-flight 메시지가 처리 중인 걸로 잡혀서 스케일 다운 안 된다고 PR 올렸던 흔적이 있었다.

metadata:
  scaleOnInFlight: "false"

이 한 줄 때문에, KEDA는 큐 길이를 계산할 때 visible 메시지만 봤다. 우리 워커는 SQS visibility timeout이 5분이다. 새벽에 첫 파드가 한꺼번에 receiveMessage로 10개씩 잡아채고 있었고, 그 메시지들은 전부 in-flight 상태였다. KEDA 입장에서 보이는 큐 길이는 0에 가까웠다. "어, 큐 비어가네?" 그래서 안 늘린 거다.

어떻게 풀었나

일단 응급조치로 scaleOnInFlight: "true"로 되돌렸다. 1분 안에 파드가 12개까지 올라왔고 큐가 빠지기 시작했다. 새벽 3시 40분쯤에 그래프가 정상으로 돌아왔다.

문제는 그 옵션을 끈 이유였다. in-flight를 포함시켰을 때 단점이 있다. 워커가 메시지 처리 중에 죽으면 visibility timeout 만료될 때까지 큐 길이 카운트가 부풀려진다. 그래서 scale down이 느려진다. 이걸 피하려고 끈 거였는데, scale up이 망가지는 부작용이 훨씬 컸다.

지금은 절충안으로 가고 있다.

  • scaleOnInFlight: "true" 유지 (scale up 우선)
  • 워커에 graceful shutdown 30초 + SIGTERM 받으면 메시지 즉시 ChangeMessageVisibility로 visibility를 0으로 리셋. SQS가 바로 메시지 재배포하도록 함
  • visibility timeout을 5분 → 2분으로 줄임. 처리 시간 분포 보고 P99가 90초였어서 충분

이렇게 하니 in-flight가 비정상적으로 누적되는 케이스가 거의 사라졌다.

끝나고 든 생각

KEDA 옵션은 한 번 정해놓으면 무심코 지나치게 된다. 특히 ScaledObject YAML은 다들 "한 번 짜놓으면 끝"이라고 생각하는데, 옵션 하나 바꿨을 때 어떤 시나리오에서 어떻게 동작하는지 시뮬레이션 안 하면 이런 함정에 빠진다. PR 리뷰할 때도 "in-flight 빼는 거 OK"라고 두 명이 approve했다. 그 PR 본문에는 scale-down 개선이라고만 적혀 있었지, scale-up이 어떻게 변하는지는 한 줄도 없었다.

요새 우리 팀은 ScaledObject 변경 PR 템플릿에 "이 변경이 scale-up 동작에 어떤 영향을 주는가", "scale-down 동작에 어떤 영향을 주는가" 두 줄을 강제로 채우게 만들었다. 좀 빡빡하긴 한데 비슷한 사고가 한 번 더 나는 것보단 낫다.

혹시 비슷한 새벽 호출 받아본 분 있으면 어떻게 푸셨는지 궁금하다. 우리 팀 방식이 최선이라는 확신은 아직 없다.

반응형