
요약
안녕하세요. SRE팀 클러스터 파트에서 근무하고 있는 Ssup(썹)이에요. 저희 클러스터 파트는 AWS EKS 클러스터 운영부터 Istio 기반의 서비스 메쉬 네트워크 구성, Prometheus, Loki와 같은 모니터링 컴포넌트 관리까지 다양한 업무를 담당하고 있어요.
당근의 대부분의 워크로드는 AWS EKS 클러스터 위에서 실행되고 있어요. 워크로드는 다양한 기준으로 분류할 수 있는데, 그 중 대표적인 유형 중 하나가 Job이에요. Job 워크로드는 Server 워크로드와 달리 시작과 끝이 명확하게 존재하는 워크로드를 의미해요.
Job 워크로드의 중요한 특징 중 하나는 한번 시작되면 중단하기 어렵다는 점이에요. 대부분의 Job 워크로드는 중단되면 처음부터 동일한 작업을 다시 수행해야 하기 때문이에요. 특히 1시간 이상 장시간 수행되는 Job 워크로드는 중단 시 재실행에 상당한 시간이 소요되기 때문에 더욱 중단하기 어려워요.
이처럼 중단하기 어려운 Job 워크로드의 특성은 EKS Node Group의 오토스케일링을 어렵게 만드는 대표적인 원인 중 하나예요. 이번 글에서는 이러한 Job 워크로드의 제약을 당근에서는 어떻게 우회하여, Job 워크로드가 실행되는 EKS Node Group에 오토스케일링을 적용하고 있는지를 소개하려고 해요.
이전의 Job 워크로드 운영방식
앞서 언급한 것처럼 Job 워크로드는 중단하기 어렵기 때문에, Job 워크로드가 실행 중인 Node는 오토스케일링의 Scale-in 과정에서 제거될 수 없어요. 이는 Job 워크로드가 다수의 Node에 고르게 분포될수록 오토스케일링을 방해하는 요소로 작용한다는 것을 의미해요. 이러한 이유로 당근에서는 Job 워크로드를 위한 전용 Node Group을 별도로 구성하여 운영하고 있어요.

위 그림은 Server 워크로드용 Node Group과 Job 워크로드용 Node Group을 분리하는 예시를 나타내고 있어요. 분리 전에는 Job 워크로드가 모든 Node에서 실행되고 있었기 때문에, Job 워크로드가 종료되기 전까지는 Node Scale-in을 수행할 수 없었어요. 반면 분리 후에는 Job 워크로드의 영향 없이 Server 워크로드의 Node Scale-in을 수행할 수 있게 돼요.
하지만 여전히 Job 워크로드용 Node Group은 고정된 수의 Node로 운영된다는 점이 문제였어요. Node 수가 고정되어 있다 보니, Job 워크로드가 적을 때는 불필요한 비용이 발생했고, 반대로 예상보다 많은 Job 워크로드가 몰릴 때는 처리가 밀려 제때 시작하지 못하는 문제가 생겼어요.
처리가 밀리는 상황을 대비하려면 Node 수를 여유 있게 설정할 수밖에 없었는데, 이는 곧 불필요한 비용으로 이어졌어요. 결국 비용과 안정성 사이에서 적절한 Node 수를 설정하고 유지하는 일 자체가 큰 운영 부담이었어요. 이러한 문제들을 해결하기 위해, Job 워크로드용 Node Group에도 오토스케일링을 도입할 수 있을지 검토하게 되었어요.
Job 워크로드의 Node Group에 오토스케일링 적용하기
Job 워크로드의 Node Group에 오토스케일링을 효과적으로 적용하기 위해 두 가지 문제를 해결해야 했어요. 하나는 원활한 Scale-in을 위한 Bin-packing 적용이고, 다른 하나는 Scale-in 과정에서 실행 중인 Job 워크로드의 강제 중단 방지예요.
Job 워크로드에 Bin-packing 적용하기
Job 워크로드는 한번 시작되면 중단이 어렵기 때문에, 여러 Node에 고르게 분산되는 것보다 가능한 한 소수의 Node에 집중되어 실행되어야 나중에 원활한 Scale-in이 가능해요. 즉, Job 워크로드는 일반적인 Server 워크로드와 달리 Bin-packing 방식으로 스케줄링되어야 해요.

위 그림은 Bin-packing이 적용되지 않았을 때와 적용되었을 때의 Pod 스케줄링 예시를 나타내고 있어요. Bin-packing이 적용되지 않은 경우에는 Job 워크로드가 여러 Node에 최대한 고르게 분산되어 스케줄링되는 것을 확인할 수 있어요. 반면 Bin-packing이 적용된 경우에는 소수의 Node를 가득 채우도록 스케줄링되는 것을 확인할 수 있어요. 즉, Bin-packing을 적용하면 빈 Node가 생길 가능성이 높아지기 때문에, Node Scale-in을 더 효과적으로 수행할 수 있게 돼요.
Pod를 Node에 배치하는 Kubernetes 기본 스케줄러는 Pod를 가능한 한 여러 Node에 분산하는 전략을 사용하기 때문에, 기본적으로 Bin-packing을 수행하지 않아요. 따라서 Bin-packing을 적용하기 위해 몇 가지 방법을 검토했어요. 크게는 Job 워크로드를 위한 별도의 스케줄러를 띄우는 방법과 PodAffinity를 활용하는 방법을 고려했어요.
별도의 스케줄러로는 잘 알려진 Volcano, Yunikorn의 Bin-packing 기능을 검토했고, Kubernetes 기본 스케줄러가 제공하는 MostAllocated 전략도 살펴봤어요. 하지만 어떤 방법이든 별도의 스케줄러를 띄워 운영해야 한다는 부담이 있었기 때문에, 별도의 스케줄러를 도입하는 방향은 선택하지 않았어요. 대신 PodAffinity를 활용하는 방법을 선택했어요.
PodAffinity는 특정 Pod가 실행 중인 Node에 새로운 Pod도 함께 배치되도록 유도하는 기능이에요. 예를 들어 네트워크 지연에 민감한 두 서비스가 있다면, PodAffinity를 활용해 두 서비스의 Pod를 항상 동일한 Node에 배치하여 통신 비용을 줄일 수 있어요. 이를 조금 다르게 활용하면 Job 워크로드 Pod끼리 서로 같은 Node에 배치되도록 유도할 수 있기 때문에, 자연스럽게 Bin-packing 효과를 얻을 수 있어요.
심지어 PodAffinity 코드의 주석을 보면, 단순히 같은 Node에 Pod를 붙여 배치하는 것에 그치지 않는다는 것을 알 수 있어요. Affinity 조건과 매칭되는 Pod가 이미 많이 실행 중인 Node일수록 Weight 합산 값이 높아지고, 스케줄러는 이 값이 가장 높은 Node를 우선적으로 선택해요. 즉, Pod가 많이 실행 중인 Node일수록 새로운 Pod가 배치될 확률이 높아지는걸 의미해요.
apiVersion: batch/v1
kind: CronJob
metadata:
name: binpacking-test-${index}
spec:
jobTemplate:
spec:
ttlSecondsAfterFinished: 0
template:
metadata:
labels:
group: job
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
group: job
topologyKey: kubernetes.io/hostname
PodAffinity가 예상대로 동작하는지 확인하기 위해 위의 CronJob을 여러 개 생성하여 테스트를 진행했어요. 위 YAML 파일은 테스트에 사용한 CronJob YAML 파일이에요. Job 워크로드 Pod에 group:jobLabel을 붙이고, PodAffinity에는 group:jobLabel을 대상으로 Affinity가 설정되어 있는 것을 확인할 수 있어요. 즉, 자기 자신과 동일한 Label을 가진 Pod가 많이 실행 중인 Node일수록 새로운 Pod가 배치될 확률이 높아지도록 설정했어요.

위 그림은 PodAffinity가 적용되기 전의 Pod 배포 상태를 시각화한 내용이에요. kind 도구를 활용하여 1개의 Master Node(kind-control-plane)와 5개의 Worker Node(kind-worker[1-5])로 구성된 Kubernetes 클러스터를 구성하고, 다수의 CronJob을 생성했어요. 이후 kube-ops-view 도구를 통해 시각화했어요.
각 Node 안에 있는 초록 상자들은 Pod를 나타내요. 아래쪽 초록 상자들은 kube-system Namespace에 속한 Pod로, Kubernetes 클러스터 운영에 필요한 핵심 컴포넌트들이에요. 위쪽은 실제 서비스나 작업을 수행하는 일반 워크로드 Pod를 의미해요.
kind 도구로 구성한 클러스터에는 CronJob 외에 다른 워크로드가 없기 때문에, 빨간 네모칸 안의 모든 Pod가 CronJob의 Pod예요. Bin-packing이 적용되어 있지 않기 때문에, 5개의 Worker Node에 Job 워크로드 Pod가 고르게 분산되어 있는 것을 확인할 수 있어요.


위 그림은 PodAffinity가 적용된 이후의 Pod 배포 상태를 시각화한 내용이에요. Job 워크로드 Pod가 특정 Node에 집중되어 실행되는 것을 확인할 수 있어요. 처음에는 kind-worker2, kind-worker3 Node에 집중되었다가, 시간이 지나면서 kind-worker3, kind-worker5 Node에 집중되었어요. 즉, 의도한 대로 Bin-packing이 잘 동작하는 것을 확인할 수 있었어요.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-job-affinity
spec:
rules:
- name: add-job-label-and-affinity
match:
any:
- resources:
kinds:
- Pod
preconditions:
all:
- key: "{{ request.object.metadata.ownerReferences[0].kind }}"
operator: Equals
value: Workflow
mutate:
patchStrategicMerge:
metadata:
labels:
group: job
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
group: job
topologyKey: kubernetes.io/hostname
Job 워크로드 Pod에 대한 PodAffinity 설정은 현재 Kyverno를 활용하여 일괄로 적용하고 있어요. 위 YAML 파일은 Argo Workflow Pod에 PodAffinity를 설정하는 Kyverno 룰 예시예요. Pod의 ownerReference를 확인하여 해당 Pod가 Argo Workflow로부터 생성된 Pod인지 검증하고, 맞는 경우에만 group: jobLabel과 PodAffinity 설정을 추가해요.
Job 워크로드 Pod의 강제 종료 방지
Job 워크로드가 실행 중인 상태에서 Node Scale-in으로 인해 Node가 제거되면, 실행 중인 Job 워크로드가 강제로 중단될 수 있어요.
- Cluster Autoscaler : cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
- Karpenter : karpenter.sh/do-not-disrupt: "true"
이를 방지하기 위해 Node 오토스케일링을 담당하는 Cluster Autoscaler와 Karpenter는 특정 Annotation이 설정된 Pod가 실행 중인 Node를 Scale-in 대상에서 제외하는 기능을 제공하고 있어요.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-job-do-not-disrupt
spec:
rules:
- name: add-do-not-disrupt-annotation
match:
any:
- resources:
kinds:
- Pod
preconditions:
all:
- key: "{{ request.object.metadata.ownerReferences[0].kind }}"
operator: Equals
value: Workflow
mutate:
patchStrategicMerge:
metadata:
annotations:
karpenter.sh/do-not-disrupt: "true"
Job 워크로드 Pod에는 해당 Annotation을 설정하여 강제 종료를 방지하고 있어요. Annotation 설정 역시 PodAffinity 설정과 동일하게 Kyverno를 활용하여 일괄로 적용하고 있어요. 위 YAML 파일은 Argo Workflow Pod에 Karpenter의 강제 종료 방지 Annotation인 karpenter.sh/do-not-disrupt: "true"를 설정하는 Kyverno 룰 예시예요.
만약 Cluster Autoscaler와 Multi-AZ 구성의 ASG(Auto Scaling Group)를 함께 사용하고 있다면, ASG의 AZ Rebalancing 기능을 꺼야 한다는 점도 챙겨야 해요. ASG는 Job 워크로드 Pod의 상태를 알지 못하고, 단순히 AZ 간 Node 수의 균형만 유지하도록 동작하기 때문이에요.

위 그림은 AZ Rebalancing이 발생하는 과정을 나타내고 있어요. Scale-in 과정에서 AZ 간 Node 수의 균형이 깨질 수 있고, 이로 인해 AZ Rebalancing이 발생하게 돼요. AZ Rebalancing이 발생하면 Job 워크로드 Pod가 강제로 중단될 수 있어요.
오토스케일링 적용 후에 발생한 문제들
Job 워크로드 Pod에 Bin-packing과 강제 종료 방지를 적용한 후, Alpha 환경에서는 오토스케일링이 정상적으로 동작하는 것을 확인했어요. 하지만 실제 Production 환경에 적용했을 때는 예상치 못한 문제들이 발생했어요. 근본 원인은 Alpha 환경과 달리 Production 환경에서는 특정 시간에 너무 많은 Job 워크로드 Pod가 한꺼번에 생성되면서, Node에 일시적으로 과도한 부하가 집중된다는 점이었어요.

위 그래프는 시간대별로 실행 중인 Job 워크로드 Pod 수를 나타내고 있어요. 10분 단위로 Pod 수가 급증하는 것을 알 수 있고, 정시에는 더 많은 Pod가 실행되는 것을 확인할 수 있어요. Bin-packing 적용 전에는 Job 워크로드 Pod가 다수의 Node에 분산되었지만, Bin-packing 적용 이후에는 소수의 특정 Node에 Pod가 집중되면서 해당 Node들이 높은 부하를 받게 되었어요.
발생한 문제를 좀 더 세분화하면 kubelet 과부하, Image Pull 실패, EBS Volume Throttling, CNI Plugin IP 할당 지연으로 구분할 수 있어요. 각 문제마다 서로 다른 방식으로 해결했어요.
kubelet 과부하
kubelet은 Kubernetes 클러스터의 각 Node에 설치되어 해당 Node의 Pod를 관리하는 역할을 수행해요. 따라서 Node에서 실행되는 Pod 수가 많아질수록 가장 먼저 부하를 받는 컴포넌트 중 하나예요. 당근에서는 kubelet이 Pod를 나열(relist)하는 데 걸리는 시간을 나타내는 kubelet_pleg_relist_duration_seconds_bucket 메트릭을 기반으로 kubelet의 부하를 측정하고 있어요.
오토스케일링 적용 이후, 정시에 Bin-packing으로 인해 소수의 Node에 많은 수의 Job 워크로드 Pod가 한꺼번에 생성되면서 kubelet_pleg_relist_duration_seconds_bucket 메트릭 값이 일시적으로 크게 증가하는 것을 확인할 수 있었어요. 특히 대부분의 Job 워크로드 Pod가 실제로 사용하는 CPU보다 적은 양을 Pod의 CPU 사용량 예약값(Resource Request)에 명시하고 있었기 때문에, 예상보다 훨씬 많은 수의 Pod가 하나의 Node에 스케줄링되어 부하가 더욱 가중되었어요.
kubelet의 부하를 줄이기 위해 Bin-packing을 유지하면서도 특정 Node에 너무 많은 Job 워크로드 Pod가 집중되지 않도록 하는 방법을 찾았어요. 가장 이상적인 방법은 Job 워크로드 Pod가 실제로 필요한 CPU를 Resource Request에 정확히 명시하는 Pod Right-sizing이겠지만, 현실적으로 모든 Job 워크로드 Pod를 Right-sizing하기는 어렵다고 판단했어요.

다른 방법을 고민하던 중 가장 먼저 시도한 것은 kubelet의 kube-reserved Resource를 높이는 방법이었어요. kube-reserved는 Node에서 kubelet이 동작하는 데 필요한 Resource를 확보하기 위해 설정하는 값이에요. kube-reserved 값을 높이면 Pod에 할당 가능한 Resource(Pod Allocatable Resource)가 줄어들기 때문에, 하나의 Node에 스케줄링되는 Pod 수를 줄일 수 있어요. 위 그림은 kube-reserved와 Pod Allocatable Resource의 관계를 간략하게 나타내고 있어요.
하지만 결론적으로 kube-reserved 값을 높이는 방법은 효과적이지 않았어요. Pod Right-sizing이 되지 않은 상태에서 kube-reserved 값을 높이는 것만으로는 Pod 쏠림을 막기에는 한계가 있었기 때문이에요. 값을 더 높이는 것도 고려했지만, kube-reserved가 너무 커지면 Pod에 할당 가능한 Resource도 함께 줄어들기 때문에 공격적으로 높이기도 어려웠어요.
그래서 찾은 다른 방법이 kubelet의 maxPods 값을 조정하는 것이었어요. maxPods는 Node에 할당할 수 있는 최대 Pod 수를 의미해요. maxPods 값을 줄이면 하나의 Node에 스케줄링될 수 있는 Pod 수가 제한되기 때문에, Bin-packing을 유지하면서도 특정 Node에 Pod가 과도하게 집중되는 것을 방지할 수 있어요.
status:
allocatable:
cpu: 15890m
ephemeral-storage: "95491281146"
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 29310740Ki
pods: "60"
capacity:
cpu: "16"
ephemeral-storage: 104779756Ki
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 32310036Ki
pods: "60"
설정된 maxPods는 Node status의 allocatable 또는 capacity의 pods 값으로 확인할 수 있어요. 위 YAML 파일은 Node status의 예시를 나타내고 있어요. AWS EKS에서 인스턴스 타입이 4xlarge 이상인 경우 기본값은 234로 설정돼요. 당근에서는 Job 워크로드용 Node에 60을 설정하여 운영하고 있어요.
maxPods 설정은 Cluster Autoscaler 환경이라면 EC2 UserData의 NodeConfig를 통해 적용할 수 있고, Karpenter 환경이라면 EC2NodeClass의 kubelet.maxPods에 설정하면 돼요.
Image Pull 실패
다음으로 겪은 문제는 Image Pull 실패였어요. 정시에 많은 수의 Job 워크로드 Pod가 한꺼번에 생성되면서 ImagePullBackOff 에러가 빈번하게 발생했어요. ImagePullBackOff는 kubelet이 Container Image를 Pull하는 데 실패했을 때 발생하는 에러로, Pod가 정상적으로 시작되지 못하고 대기 상태에 머무르게 돼요.
원인을 파악해보니 kubelet의 registryPullQPS와 registryBurst 설정이 문제였어요. registryPullQPS는 kubelet이 Container Registry에서 Image를 Pull할 수 있는 초당 최대 요청 수를 의미하고, registryBurst는 순간적으로 허용할 수 있는 최대 요청 수를 의미해요. 두 값 모두 기본값이 낮게 설정되어 있어, 한꺼번에 많은 Pod가 생성될 때 Image Pull 요청이 병목 현상을 일으키고 있었어요.
이를 해결하기 위해 registryPullQPS를 기본값인 5에서 40으로, registryBurst를 기본값인 10에서 60으로 상향 조정했고, 이후 ImagePullBackOff 에러가 더 이상 발생하지 않았어요. 참고로 두 값은 앞서 설정한 maxPods 값인 60을 기준으로 산정했어요.
registryPullQPS 와 registryBurst 값 설정은 Cluster Autoscaler, Karpenter 환경 모두 EC2 UserData의 NodeConfig를 통해 적용할 수 있어요.
EBS Volume Throttling
E1218 15:20:53.795027 5233 log.go:32] "CreateContainer in sandbox from runtime service failed" err="rpc error: code = Unknown desc = failed to reserve container name \\"init_sync-sa-resources-to-db-1766070900-hook-207168736_krp_f181b699-cbb4-4633-b6d4-b54c531be57e_1\\": name \\"init_sync-sa-resources-to-db-1766070900-hook-207168736_krp_f181b699-cbb4-4633-b6d4-b54c531be57e_1\\" is reserved for \\"1ddbf865c643250c8a88ab890a841409cb21feef6b27e7666a460d226b34db2d\\"" podSandboxID="f2deaecc93076c82e1f7dc686eae07091d12ac3657168ecd65edb3b7836bf69c"
registryPullQPS와 registryBurst 값을 높여 kubelet이 동시에 더 많은 Image를 Pull할 수 있도록 조정하고 나니, 이번에는 위와 같은 에러 로그가 종종 발생했어요. 에러 내용은 Container Runtime이 Container 이름을 지정하여 생성하지 못했다는 것이었어요.
원인을 좀 더 파악해보니, kubelet이 동시에 많은 양의 Image Pull을 수행하면서 EC2 인스턴스에 연결된 EBS Volume에 Throttling이 발생하고 있었어요. IOPS와 Bandwidth 모두 Throttling이 발생하면서 Container Runtime이 제대로 동작하지 못한 것이 근본 원인이었어요.
이를 해결하기 위해 EBS Volume의 IOPS를 3,000에서 8,000으로, Throughput을 150MB/s에서 800MB/s로 증설했고, 이후 해당 에러가 더 이상 발생하지 않았어요.
CNI Plugin IP 할당 지연
Node에 갑자기 많은 수의 Pod가 생성되면 각 Pod에 IP를 할당하는 CNI Plugin도 부하를 받게 돼요. AWS VPC CNI를 사용하고 있다면 일반적으로 WARM IP 개수를 늘리거나 Prefix Mode 도입을 함께 검토하는 것을 권장해요.
WARM IP는 Pod가 생성되기 전에 미리 IP를 확보해 두는 방식이에요. IP를 필요할 때마다 새로 할당받는 것이 아니라 미리 준비해 둔 IP Pool에서 즉시 가져다 쓰기 때문에, Pod 생성 시 IP 할당 지연을 줄일 수 있어요. Prefix Mode는 IP를 하나씩 개별적으로 할당받는 대신 IP 대역(/28 CIDR) 단위로 한꺼번에 할당받는 방식이에요. 한 번의 요청으로 여러 개의 IP를 한꺼번에 확보하기 때문에, 다수의 Pod가 생성되더라도 더 빠르게 IP를 할당받을 수 있어요.
하지만 당근에서는 WARM IP 증가나 Prefix Mode 도입 대신 Host Network Mode를 적용하고 있어요. Host Network Mode는 Pod가 고유의 IP를 갖지 않고 Node의 IP를 그대로 사용하는 방식이라, Pod 생성 시 IP 할당 과정 자체가 생략되어 CNI Plugin의 부하를 완전히 회피할 수 있기 때문이에요.
오토스케일링 적용 후 지표 변화

위 그래프는 오토스케일링 적용 전후의 Job 워크로드용 Node Group의 시간별 Node 수를 나타내고 있어요. 당근에서는 CPU Architecture별로 Node Group을 구분하여 운영하고 있어요. job은 amd64 Architecture를 위한 Node Group이고, job-arm은 arm64 Architecture를 위한 Node Group이에요.
그래프를 통해 오토스케일링 적용 후 Node 수가 유동적으로 증감하는 것을 확인할 수 있어요. 오토스케일링 적용 후 평균 Node 수는 job Node Group의 경우 5개에서 4.198개로 약 16% 감소했고, job-arm Node Group의 경우 2개에서 4.18개로 약 109% 증가했어요.
job-arm Node Group의 Node 수가 크게 늘어난 데는 두 가지 이유가 있어요. 오토스케일링 적용 이후 job-arm Node Group에서 실행되는 Job 워크로드 수 자체가 늘어난 것도 있고, maxPods 설정으로 Node당 할당 가능한 Pod 수가 제한되면서 Job 워크로드가 더 많은 Node에 분산된 것도 영향을 미쳤어요.

위 그래프는 오토스케일링 적용 전후의 Image Pull 시간 변화를 나타내고 있어요. Image Pull 시간은 job Node Group의 경우 3초에서 7.972초로, job-arm Node Group의 경우 16초에서 29.76초로 늘어났어요. 이는 오토스케일링으로 인해 Node가 생성과 삭제를 반복하면서, 각 Node에 캐싱된 Container Image가 줄어들었기 때문이에요.

위 그래프는 오토스케일링 적용 전후의 Job과 Workflow Pod의 Pending 시간을 나타내고 있어요. 여기서 Job은 Kubernetes Job으로 생성된 Pod를 의미하고, Workflow는 Argo Workflow를 통해 생성된 Pod를 의미해요.
오토스케일링 적용 전에는 workflow Pod의 Pending 시간이 최대 14초까지 증가했고, 한번 Pending이 발생하면 오랫동안 지속되는 패턴을 보였어요. 반면 오토스케일링 적용 이후에는 최대 Pending 시간이 10초로 줄어들었고, Pending이 짧게 발생했다 빠르게 해소되는 패턴으로 개선된 것을 확인할 수 있어요. 평균 Pending 시간도 1.54초에서 0.78초로 약 49% 감소했어요.
Pending 시간에는 Image Pull 시간도 포함되는데, 앞서 살펴봤듯이 오토스케일링 적용 이후 Image Pull 시간이 늘었음에도 불구하고 Pending 시간이 줄어들었다는 것은 그만큼 오토스케일링이 빠르게 동작하여 Pod가 신속하게 Node에 배치되고 있다는 의미예요.

마지막 그래프는 오토스케일링 적용 전후의 Job과 Workflow Pod의 Running 시간을 나타내고 있어요. workflow Pod는 138초에서 72.6초로 약 47% 감소했고, job Pod는 290.76초에서 321.48초로 약 10.56% 증가했어요. 대부분의 workflow Pod가 job-arm Node Group에서 실행되고 있는데, job-arm Node Group은 Node 수가 약 2배 증가한 만큼 Running 시간도 절반 정도로 줄어든 것을 확인할 수 있어요.

위 표는 지금까지 소개한 주요 지표의 변화량과 변화율을 나타내고 있어요. 오토스케일링 도입 이후 적절한 Node 수가 자연스럽게 조정되었고, 이로 인해 Pod Pending 시간 감소와 Running 시간 단축 등 전반적인 성능 개선을 확인할 수 있었어요.
마무리
당근에서는 다양한 종류의 Job 워크로드가 AWS EKS 클러스터에서 실행되고 있어요. Kontrol이라고 불리는 IDP(Internal Developer Platform)에서 제공하는 CronJob을 비롯해, Data Orchestrator 역할을 수행하는 Airflow/Dagster, CI/CD를 담당하는 GitHub Actions Runner 등이 있어요.
이러한 모든 Job 워크로드에 이번 글에서 소개한 오토스케일링을 도입하여, 비용 효율적이면서도 유연하게 Node를 활용하고 있어요. 오토스케일링 도입을 통해 Pending 시간 감소, Running 시간 단축과 같은 성능적 이점도 얻을 수 있었지만, 무엇보다 Node 수를 일일이 모니터링하고 수동으로 조정해야 했던 운영 부담에서 벗어날 수 있었다는 점이 가장 큰 이점이었어요.
이번 글이 Job 워크로드 운영에 조금이나마 도움이 되었으면 해요. 앞으로도 Kubernetes 클러스터의 안정성과 효율성을 높이기 위한 다양한 시도를 이어나갈 예정이에요. 긴 글 읽어주셔서 감사해요.
Job 워크로드를 위한 EKS Node Group 오토스케일링 도입기 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.