KEDA Kafka 스케일러, 운영하면서 챙겨야 하는 설정 가이드
KEDA로 Kafka consumer를 오토스케일링하는 건 ScaledObject 하나 적용하면 끝나는 것처럼 보인다. 실제로 키워드만 보면 그렇다. 근데 트래픽 패턴이 살짝만 비대칭이거나 consumer가 commit을 게으르게 하는 순간 스케일러가 엉뚱한 방향으로 움직인다. 우리 팀에서 지난 분기에 partition 24개짜리 topic 두 개를 KEDA 기반으로 옮기면서 정리한 내용을 가이드 형태로 풀어둔다. 최근 KEDA 2.19 기준이다.
0. 기본 ScaledObject부터
Sarama 기반 기본 스케일러는 deprecated 경고가 종종 뜨므로, 신규 도입이면 apache-kafka 트리거(Go 클라이언트 기반) 쪽을 권장한다. 가장 단순한 형태는 이렇다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
namespace: orders
spec:
scaleTargetRef:
name: order-consumer
minReplicaCount: 2
maxReplicaCount: 24
pollingInterval: 15
cooldownPeriod: 120
triggers:
- type: apache-kafka
metadata:
bootstrapServers: kafka-bootstrap.kafka:9092
consumerGroup: order-consumer
topic: orders.events.v1
lagThreshold: "2000"
offsetResetPolicy: latest
이 YAML로 일단 동작은 한다. 하지만 운영에 그대로 올리면 며칠 안에 한두 번씩 이상 거동이 보인다. 아래 항목들을 차례대로 잡아야 한다.
1. lagThreshold는 "파티션당 목표 lag"이다
가장 많이 헷갈리는 부분이다. lagThreshold는 토픽 전체 합산 lag이 아니라 HPA가 목표로 삼는 파티션당 평균 lag이다. KEDA가 내부적으로 desiredReplicas를 계산할 때 totalLag / lagThreshold 식이 쓰인다.
파티션 24개 토픽에서 lagThreshold를 100으로 잡으면, totalLag가 100을 넘는 순간 desired가 1보다 커지고, 2,400을 넘으면 desired가 24가 된다. 즉 lagThreshold가 작을수록 더 공격적으로 스케일업한다.
처음에는 이걸 모르고 lagThreshold를 50으로 박았다가, 평상시 lag 200~300대에서도 maxReplica까지 튀어오르는 걸 보고 알아챘다. 결국은 이렇게 잡았다.
- 메시지 처리 latency가 100ms 이하인 가벼운 consumer: lagThreshold 2000~5000
- 처리에 500ms 이상 걸리는 무거운 consumer: lagThreshold 200~500
기준은 "이 정도 lag이면 1대가 처리할 만하다"는 수치다. SLO에서 거꾸로 계산하면 편하다. P95 처리시간이 80ms고 lag을 30초 안에 해소하고 싶다면 한 대가 30s/80ms = 약 375개를 처리할 수 있으니 lagThreshold 300~400 정도가 합리적이다.
2. excludePersistentLag가 진짜 필요한 순간
이 옵션은 "직전 폴링 사이클과 비교해서 current offset이 안 움직인 파티션의 lag은 무시한다"는 뜻이다. 처음 봤을 때는 "왜 무시를 해? 더 빨리 스케일업해야 하는 거 아닌가?" 싶었는데 한번 데여본 후에는 무조건 켜둔다.
시나리오는 이렇다. Kafka transactional producer가 보낸 메시지에는 commit marker가 끼어 있어서 consumer가 그 메시지를 받자마자 offset이 한 칸씩 점프한다. 그런데 transactional producer가 commit을 안 한 채로 트랜잭션을 들고 있으면 consumer는 그 idx에서 멈춰 있게 되고, lag은 계속 증가한다. 이때 consumer는 정상이고, 단지 producer쪽 트랜잭션이 늦어진 것뿐인데 스케일러는 lag만 보고 무지성으로 파드를 늘린다.
metadata:
excludePersistentLag: "true"
이거 한 줄이 의외로 큰 차이를 만든다. transactional producer를 안 쓰더라도, 특정 키 한쪽으로 트래픽이 쏠려서 파티션 하나에 lag이 잔뜩 쌓이고 그 consumer가 처리에 시간이 걸리는 경우에도 효과가 있다. 어차피 그 파티션은 컨슈머 하나만 잡고 있고, 새 파드를 띄워봐야 다른 파티션을 잡을 텐데 거기는 lag이 없으니 idle로 놀게 된다. 그럴 거면 안 띄우는 게 낫다.
다만 KEDA #5274 이슈처럼 offset이 -1인 신규 파티션에서는 이 동작이 좀 어색하니, 새 파티션 추가 직후엔 메트릭을 한번 확인하는 게 좋다.
3. partitionLimitation과 allowIdleConsumers
maxReplicaCount를 파티션 수보다 크게 잡으면 KEDA는 알아서 파티션 수까지만 늘린다. 한 파티션은 같은 consumer group 내에서 한 컨슈머만 잡을 수 있으니까 합리적인 동작이다. 그런데 allowIdleConsumers: "true"를 켜면 이 안전장치를 끄고 그 이상으로도 스케일업한다.
언제 켜는가? 보통은 안 켠다. 켜고 싶다면 두 가지 케이스다.
첫째, rolling 배포 중에 새 파드와 기존 파드가 잠깐 같이 떠있어야 하는 상황. Deployment의 maxSurge가 25%인데 KEDA가 파티션 수에 딱 맞춰서 묶어두면 새 파드가 Ready인데 파티션을 못 잡고 기존 파드도 못 죽는 어색한 순간이 생긴다. 이때 살짝 여유를 두면 배포가 부드러워진다.
둘째, consumer 코드 안에서 Kafka 외에도 다른 작업을 같이 하는 경우. 예를 들어 Kafka 메시지를 받아 처리한 결과를 다시 HTTP로 외부에 보낸다거나 할 때, partition 수와 무관하게 처리 throughput을 더 끌어올려야 할 때가 있다. 이 경우는 그냥 두 파드가 같은 파티션을 들고 있어도 무방한 별도 작업이 있다는 뜻이니 가능하다.
partitionLimitation은 특정 파티션만 모니터링하고 싶을 때 쓴다. 우리는 partition별 트래픽 편차가 크게 나는 토픽에서 hot partition만 추적하려고 한번 써봤는데, 결국 트래픽 균등화 문제는 producer 쪽에서 해결하는 게 맞다는 결론이라 거두어들였다.
4. scale-to-zero의 함정
KEDA의 가장 매력적인 기능 중 하나가 minReplicaCount: 0이다. 야간에 트래픽이 거의 없는 topic이라면 컨슈머를 다 죽이고 비용을 아낄 수 있다. 그런데 production에서 이걸 켤 때는 두 번쯤 더 생각해보길 권한다.
먼저, 0에서 1로 올라올 때 cold start cost가 생각보다 크다. consumer group join, rebalance, partition assignment를 다 새로 거친다. 우리 환경에서는 6초 정도 걸렸다. 그 사이에 들어오는 메시지는 누적되고, 막상 1대 띄워서 처리 시작하면 lag 1만 가까이 쌓여 있어서 스케일러가 즉시 max로 튀어오른다. 그 다음 메시지가 또 잠시 없으면 다시 0으로 떨어지고… 이걸 몇 번 반복하면 한 시간 만에 알람이 다섯 개 정도 온다.
이걸 막으려면 cooldownPeriod를 후하게 준다. 기본 300초인데, 새벽 트래픽이 산발적으로 들어오는 우리 토픽은 900초로 늘렸다. 그리고 idleReplicaCount를 1로 설정해서 "0까지는 가지 말고 1까지만"으로 타협하는 것도 방법이다. 비용은 좀 들지만 알람은 안 온다.
spec:
idleReplicaCount: 1
minReplicaCount: 1
maxReplicaCount: 24
cooldownPeriod: 900
그리고 한 가지 더. scale-to-zero가 동작하려면 KEDA의 admission webhook이 정상이어야 한다. webhook이 죽으면 메트릭은 안 갱신되는데 ScaledObject는 그대로 유지돼서 마지막 상태에 박혀버린다. webhook 가용성에 대한 알람은 따로 걸어두는 걸 추천한다.
마무리
KEDA 자체는 야무지게 만들어진 도구다. 다만 Kafka 트리거는 "lag이라는 지표가 가진 비대칭성"을 그대로 안고 가기 때문에, 토픽 특성과 consumer 패턴을 좀 들여다보고 lagThreshold/excludePersistentLag/cooldownPeriod 세 개는 반드시 손보는 게 좋다. 우리는 위 설정 적용 후 alarm 빈도가 70% 정도 줄었고, 야간 자원 사용량은 40% 가까이 떨어졌다.
이 글에서 다 못 다룬 게 SASL 인증, multi-cluster Kafka, KEDA HTTP add-on과의 조합 같은 것들인데, 별도 글에서 정리해볼 생각이다.