KEDA SQS scaler 도입했다가 thrashing에 한참 데인 이야기
지난달에 SQS 기반 워커 파드를 KEDA로 옮겼다. HPA의 CPU 메트릭만으로는 큐가 쌓일 때 늦게 반응하는 게 계속 거슬려서, 큐 길이로 직접 스케일하는 게 자연스러워 보였다. KEDA는 2.19가 최근에 떨어졌고(2026-02), SQS scaler에 scaleOnDelayed 같은 옵션도 정리돼 있어서 큰 고민 없이 시작했는데, 정작 일주일 동안 새벽에 두 번 호출되고 나서야 정신을 차렸다. 그 과정 정리.
시작은 정상이었다
워크로드는 단순하다. 외부 이벤트 → SQS → 워커 파드(Go 단일 바이너리)가 메시지 하나씩 받아 처리. 평소엔 큐가 비어 있고, 1시간 단위로 큐가 수만 건씩 쌓이는 burst 패턴이다. 한 메시지 처리에 평균 2초, P99 8초.
처음 달았던 ScaledObject는 거의 문서 그대로였다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: sqs-worker
spec:
scaleTargetRef:
name: sqs-worker
minReplicaCount: 0
maxReplicaCount: 50
pollingInterval: 15
cooldownPeriod: 60
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.ap-northeast-2.amazonaws.com/.../events
queueLength: "10"
awsRegion: ap-northeast-2
queueLength: 10은 "파드 하나가 10개 처리 가능한 단위" 라고 막연하게 잡았다. 이게 첫 번째 함정이었다는 건 한참 뒤에 알게 된다.
stage에서 부하 줘보고, 큐 1만 건 넣고 25-30파드까지 떠서 5분 안에 다 비우는 걸 확인하고 본 환경 적용. 그날 저녁까지는 깔끔하게 잘 돌았다.
새벽 3시, 워커가 출렁이기 시작했다
그날 새벽 burst 트래픽이 들어왔는데, 모니터링 대시보드를 다음날 보니 그래프가 사인파였다. 5분 단위로 파드가 50개 → 8개 → 50개 → 12개 → ... 를 반복. 처리량이 일정하지 않으니 큐도 비워졌다 다시 쌓이고를 반복했다. 결과적으로 30분이면 끝날 일이 1시간 넘게 걸렸다.
처음 의심했던 건 KEDA polling이 너무 자주 돌아서 그런가 싶어서 pollingInterval을 30초로 늘렸는데 차이가 거의 없었다. 큐 깊이가 잘못 보고되는 건 아닐까 싶어 CloudWatch 메트릭이랑 KEDA 메트릭 서버 응답을 비교했는데 그것도 일치했다.
$ kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/s0-aws-sqs-queue-events" | jq
{
"value": "8423"
}
CloudWatch 콘솔이랑 같음. 그러니 KEDA 자체는 거짓말 안 하는 거였다. 문제는 다른 데 있었다.
진짜 원인: HPA의 평균값과 ApproximateNumberOfMessagesNotVisible
KEDA의 SQS scaler는 내부적으로 ApproximateNumberOfMessages + ApproximateNumberOfMessagesNotVisible을 합쳐서 큐 깊이로 본다. 즉 워커가 receive해서 visibility timeout 동안 들고 있는 메시지도 "남은 일감"으로 카운트한다는 뜻이다.
문제는 우리 워커의 receive 패턴이었다. 한 워커가 한 번에 10개씩 long-poll로 받아서 (처리 시간 평균 2초니까) 20초 정도 들고 있었다. 그러니 50파드가 떠 있으면 한순간에 500개까지 NotVisible로 잡혔다. 그게 큐 깊이로 카운트되니, 큐가 거의 비었는데도 KEDA는 "아직 일감 500개 남았다" 고 보고 파드를 안 줄였다.
그런데 워커가 메시지 처리 끝내고 ack(delete)하는 순간 NotVisible이 한 번에 떨어지면서 큐 깊이도 급격히 줄었다. HPA는 그걸 보고 "지금 평균 큐 깊이가 너무 낮다" 고 판단해서 한꺼번에 파드를 절반 가까이 줄였다. 줄어든 파드가 메시지 못 받으니 다시 큐가 쌓이고 → 다시 50으로 올라가고. 이게 사인파의 정체였다.
내부적으로는 이런 흐름이다:
큐에 메시지 N개 → KEDA가 N/queueLength 비율 계산 → HPA가 desiredReplicas 계산
↑
ApproximateNumberOfMessagesNotVisible 포함됨
그러니까 워커가 빠르게 일하면 일할수록 NotVisible이 출렁이고, 출렁이는 만큼 파드 수도 출렁인다.
두 개의 노브를 만졌다
해결한 방법은 두 가지를 같이 적용한 거다.
첫째, KEDA 2.16부터 들어온 excludeDelayed/scaleOnDelayed/scaleOnInFlight 옵션 중 scaleOnInFlight: false를 켜서 NotVisible 메시지를 빼고 가시 메시지(ApproximateNumberOfMessages)만 보고 스케일하게 했다.
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.ap-northeast-2.amazonaws.com/.../events
queueLength: "10"
scaleOnInFlight: "false"
awsRegion: ap-northeast-2
이 한 줄로 사인파의 진폭이 1/3 수준으로 줄었다.
둘째, HPA behavior로 scale-down을 보수적으로 깔았다. KEDA는 advanced.horizontalPodAutoscalerConfig.behavior 로 HPA의 behavior 섹션을 그대로 노출한다.
spec:
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 180
policies:
- type: Percent
value: 25
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 20
periodSeconds: 30
selectPolicy: Max
scale-up은 공격적으로 (100%/30s 또는 20파드/30s 중 큰 거), scale-down은 3분 stabilization 걸고 60초당 25%만. 평소엔 minReplicaCount: 0 → 메시지 들어오면 0 → 활성으로 가는 단계가 있어서 cold start가 좀 있는데, scale-up이 빠르니까 큐 적체로 바로 안 가더라.
queueLength 값을 수치로 잡는 법
원래 막연히 "파드 하나가 10개 처리" 라고 잡았던 queueLength도 다시 봤다. 사실 이 값이 "한 파드가 동시에 들고 있을 메시지 수의 목표치" 인데, 우리 워커는 receive batch size가 10이고 처리 시간이 평균 2초니까 정상 상태에서 한 파드가 들고 있는 메시지 수는 10개 정도가 맞긴 하다.
근데 우리는 scaleOnInFlight: false로 NotVisible을 뺐기 때문에 "보이는 메시지만 본다" 가 됐고, 이 경우엔 queueLength가 "한 파드가 1초에 처리할 수 있는 메시지 수" 에 가깝게 잡혀야 한다. 평균 처리 시간 2초 → 1초당 0.5건 → 그런데 이걸 정수로 잡아야 하니까 queueLength: 5 정도로 내렸다 (5초치 작업량). 이러면 파드 하나가 5초 안에 보이는 메시지를 다 흡수할 수 있는 만큼만 떠 있게 된다.
이 값은 워크로드마다 다르니까 한 번에 정답이 안 나온다. 우리는 큐 깊이별 처리 시간 그래프 그려놓고 며칠 동안 슬슬 내려가면서 안정점 찾았다.
그래서 결론
| 증상 | 원인 | 처방 |
|---|---|---|
| 파드 수 사인파 | NotVisible 메시지가 큐 깊이에 포함됨 | scaleOnInFlight: false |
| Scale-down 너무 공격적 | HPA 기본 behavior가 빠르게 줄임 | scaleDown stabilizationWindow + Percent 25% |
queueLength가 막연 |
처리 시간 기준이 모호함 | NotVisible 빼면 "초당 처리량" 기준으로 재계산 |
지금은 깔끔하다. 같은 burst 패턴인데 파드 수 그래프가 톱니파에서 부드러운 종 모양으로 바뀌었다. 처리 시간도 30분 안쪽으로 안정. 새벽에 안 깨도 된다.
KEDA 자체는 좋은 도구다. 다만 SQS 같은 외부 큐 메트릭은 "내부 동작에 어떻게 매핑되는지" 한 번 더 생각하고 써야 한다는 게 이번 교훈이었다. ApproximateNumberOfMessagesNotVisible이 NotVisible이라고 해서 "안 보이니까 안 셀 거야" 라고 막연히 생각하면 우리 같은 함정에 빠진다.
비슷한 패턴 운영하시는 분들 중에 다른 노브 만지신 분 있으면 댓글로 알려주세요. 아직 우리도 큐 깊이 모니터링은 좀 더 손봐야 할 부분이라.