Skip to content
목록으로

스케줄링과 오토스케일링

Kubernetes2025년 6월 8일
스케줄링과 오토스케일링

저번 포스팅에서는, 쿠버네티스의 스토리지를 공부하였습니다. 이번에는 스케줄링과 오토스케일링입니다. Pod 를 어느 노드에 놓을지 어떻게 정하는지, 자원을 어떻게 나눠 갖는지, 그리고 부하에 따라 어떻게 자동으로 늘어나고 줄어드는지를 다룹니다.

하둡 시리즈에서 YARN 을 공부할 때 스케줄러와 큐를 봤는데, 쿠버네티스는 같은 문제를 꽤 다른 방식으로 풉니다. 그 차이를 비교해보는 재미도 있었습니다.


1. 자원 요청과 제한


스케줄링의 출발점입니다. 여기가 흐릿하면 뒤가 전부 흔들립니다.

1.1. requests 와 limits

컨테이너마다 두 가지를 적을 수 있습니다.

  1. requests : "최소 이만큼은 보장받아야 한다" 는 요청량입니다.
  2. limits : "이 이상은 쓰지 않겠다" 는 상한입니다.

가장 중요한 것은 스케줄러가 보는 것은 requests 뿐이라는 점입니다. 노드에 Pod 를 놓을 수 있는지 판단할 때, 그 노드에 이미 있는 Pod 들의 requests 합과 노드 용량을 비교합니다. 실제 사용량은 보지 않습니다.

그래서 이런 일이 생깁니다. requests 를 실제 사용량보다 크게 잡으면, 노드가 놀고 있는데도 새 Pod 가 들어가지 못합니다. 반대로 너무 작게 잡으면 한 노드에 Pod 가 몰려 서로 자원을 다투게 됩니다.

1.2. CPU 와 메모리는 다르게 취급된다

이 차이를 모르면 장애 원인을 못 찾습니다. CPU 는 압축 가능한(compressible) 자원입니다. 부족하면 느려질 뿐 프로세스가 죽지는 않습니다. limits 를 넘으면 커널이 스로틀링(throttling) 을 걸어 그 이상 쓰지 못하게 잡아둡니다. 메모리는 압축 불가능한(incompressible) 자원입니다. 이미 할당한 메모리를 빼앗을 수 없습니다. 그래서 limits 를 넘으면 OOMKilled 로 컨테이너가 죽습니다.

여기서 CPU limits 에 대한 논쟁이 나옵니다. CPU limits 를 걸면 노드에 여유가 있어도 그 이상 쓰지 못해 응답 지연이 생깁니다. 그래서 지연에 민감한 서비스에는 CPU limits 를 걸지 않고 requests 만 두는 선택을 하기도 합니다. 다만 이렇게 하면 한 Pod 가 노드의 CPU 를 독차지할 수 있어, 격리와 성능 사이의 절충입니다.

메모리는 다릅니다. limits 를 걸지 않으면 노드 전체가 위험해지므로 대체로 거는 편이 낫습니다.

1.3. QoS 클래스

requests 와 limits 를 어떻게 적었는지에 따라 Pod 에 등급이 매겨집니다. 노드에 자원이 부족해져 누군가를 쫓아내야 할 때 순서를 정하는 기준입니다.

클래스 조건 쫓겨나는 순서
Guaranteed 모든 컨테이너가 requests == limits 가장 나중
Burstable requests 는 있는데 limits 와 다르거나 없음 중간
BestEffort requests 도 limits 도 없음 가장 먼저

중요한 서비스라면 Guaranteed 로 두는 것이 안전합니다. 다만 그만큼 자원을 고정으로 잡아두는 셈이라 클러스터 사용률은 떨어집니다.

1.4. 노드 압박과 축출

노드의 자원이 부족해지면 kubelet 이 Pod 를 쫓아냅니다. 이를 축출(Eviction) 이라고 합니다. kubelet 은 메모리, 디스크, inode 등을 감시하다가 임계치를 넘으면 노드에 조건을 표시합니다. MemoryPressure, DiskPressure 같은 것들입니다. 그리고 QoS 클래스와 requests 대비 실제 사용량을 보고 쫓아낼 대상을 고릅니다.

OOMKilled 와 Eviction 은 다릅니다. OOMKilled 는 컨테이너가 자기 limits 를 넘어 커널이 죽인 것이고, Eviction 은 노드 전체가 위험해서 kubelet 이 내보낸 것입니다. 원인도 대응도 다르므로 구별해야 합니다.

1.5. LimitRange 와 ResourceQuota

네임스페이스 단위로 거는 통제 장치입니다.

  1. LimitRange : 컨테이너 하나가 요청할 수 있는 최소·최대를 정하고, 아무것도 적지 않았을 때의 기본값을 넣어줍니다. 개발자가 requests 를 빠뜨려 BestEffort 가 되는 것을 막습니다.
  2. ResourceQuota : 그 네임스페이스 전체가 쓸 수 있는 총량을 제한합니다. 오브젝트 개수도 제한할 수 있습니다.

ResourceQuota 를 걸면 그 네임스페이스의 모든 Pod 는 반드시 requests 와 limits 를 적어야 합니다. 총량을 계산할 수 없기 때문입니다. 그래서 LimitRange 와 함께 쓰는 것이 보통입니다.


2. 스케줄러가 노드를 고르는 방법


2편에서 필터링과 스코어링 두 단계라고 했는데, 그 안을 들여다보겠습니다.

2.1. 필터링

이 Pod 를 놓을 수 있는 노드만 남깁니다.

  1. 노드에 requests 만큼의 여유가 있는가
  2. 요청한 nodeSelector · affinity 조건에 맞는가
  3. 노드의 taint 를 견딜 수 있는가
  4. 요청한 볼륨을 붙일 수 있는 노드인가
  5. 요청한 포트가 비어 있는가 (hostPort 를 쓸 때)

여기서 하나도 남지 않으면 Pod 는 Pending 으로 남고, 이벤트에 이유가 기록됩니다.

2.2. 스코어링

남은 후보에 점수를 매깁니다. 주로 반영되는 것들입니다.

  1. 자원이 고르게 퍼지는 쪽 (또는 설정에 따라 한쪽에 모으는 쪽)
  2. 이미지를 이미 가진 노드 : 내려받는 시간을 아낍니다
  3. 같은 서비스의 Pod 가 몰려 있지 않은 노드
  4. affinity 의 선호(preferred) 조건 충족 여부

2.3. 바인딩

가장 점수가 높은 노드를 골라 nodeName 을 채웁니다. 이때 스케줄러는 apiserver 응답을 기다리지 않고 자기 캐시에 먼저 반영해둔다(assume). 그래야 다음 Pod 를 스케줄할 때 방금 배치한 것을 감안할 수 있기 때문입니다. 나중에 실제 바인딩이 실패하면 되돌립니다.


3. 배치를 제어하는 방법들


3.1. nodeSelector

가장 단순합니다. 노드의 라벨과 정확히 일치하는 곳에만 배치합니다.

yaml
nodeSelector:
  disktype: ssd

조건이 하나이고 반드시 지켜야 할 때만 쓸 수 있습니다. "A 또는 B", "가능하면 A" 같은 것은 표현할 수 없습니다.

3.2. Node Affinity

nodeSelector 를 확장한 것입니다. 두 종류가 있습니다.

  1. requiredDuringSchedulingIgnoredDuringExecution : 반드시 만족해야 합니다. 필터링 단계에 반영됩니다.
  2. preferredDuringSchedulingIgnoredDuringExecution : 만족하면 점수를 더 줍니다. 못 맞춰도 배치는 됩니다.

이름이 길지만 뜯어보면 뜻이 분명합니다. IgnoredDuringExecution 은 "이미 떠 있는 Pod 에는 적용하지 않는다" 는 뜻입니다. 나중에 노드의 라벨이 바뀌어 조건에 맞지 않게 되어도 이미 돌고 있는 Pod 를 쫓아내지는 않습니다.

3.3. Pod Affinity 와 Anti-Affinity

노드의 라벨이 아니라 그 노드에 어떤 Pod 가 있는지를 기준으로 삼습니다.

  1. Pod Affinity : 특정 Pod 와 가까이 두고 싶을 때. 캐시와 애플리케이션을 같은 노드에 두어 지연을 줄이는 경우 등.
  2. Pod Anti-Affinity : 특정 Pod 와 떨어뜨리고 싶을 때. 같은 서비스의 복제본을 서로 다른 노드에 흩어 노드 하나가 죽어도 전체가 죽지 않게 하는 경우.

여기서 핵심은 topologyKey 입니다. "가깝다" 와 "멀다" 의 기준이 되는 노드 라벨을 지정합니다.

yaml
topologyKey: kubernetes.io/hostname    # 노드 단위로 흩기
topologyKey: topology.kubernetes.io/zone  # 가용 영역 단위로 흩기

영역 단위로 흩으면 데이터센터 하나가 통째로 죽어도 서비스가 유지됩니다. 다만 Pod Affinity 는 계산 비용이 큽니다. 새 Pod 를 배치할 때마다 기존 Pod 들의 위치를 전부 확인해야 하기 때문에, 대규모 클러스터에서는 스케줄링이 느려질 수 있습니다.

3.4. Topology Spread Constraints

Anti-Affinity 로 "흩기" 를 하려면 표현이 투박합니다. "완전히 못 겹치게" 아니면 "가능하면" 둘뿐이라, "고르게 나눠서" 를 표현할 수 없습니다. Topology Spread Constraints 는 이를 위해 나왔습니다.

  1. maxSkew : 영역 간 Pod 개수 차이가 최대 몇까지 허용되는가
  2. topologyKey : 무엇을 기준으로 나눌 것인가
  3. whenUnsatisfiable : 못 맞추면 배치를 막을 것인가(DoNotSchedule), 그래도 배치할 것인가(ScheduleAnyway)

세 영역에 6개를 maxSkew: 1 로 두면 2-2-2 로 퍼집니다. "고르게" 를 직접 표현할 수 있다는 것이 Anti-Affinity 와의 차이입니다.

3.5. Taint 와 Toleration

지금까지는 Pod 가 노드를 고르는 방식이었습니다. Taint 는 반대로 노드가 Pod 를 밀어내는 방식입니다. 노드에 taint 를 걸면, 그것을 견딜 수 있다고 선언한(toleration) Pod 만 들어올 수 있습니다.

효과는 세 가지입니다.

  1. NoSchedule : 새 Pod 를 배치하지 않습니다. 이미 있는 것은 그대로 둡니다.
  2. PreferNoSchedule : 가능하면 피합니다.
  3. NoExecute : 새 배치도 막고, 이미 있는 Pod 도 쫓아냅니다.

쓰임새는 이렇습니다.

  1. 컨트롤 플레인 노드 보호 : 기본으로 taint 가 걸려 있어 일반 워크로드가 들어가지 않습니다.
  2. 전용 노드 : GPU 노드에 taint 를 걸어 GPU 가 필요한 Pod 만 들어오게 합니다.
  3. 노드 장애 처리 : 노드가 응답하지 않으면 쿠버네티스가 자동으로 NoExecute taint 를 겁니다. 이때 tolerationSeconds 로 "몇 초까지는 버틴다" 를 정할 수 있습니다. 일시적인 네트워크 문제로 Pod 가 곧바로 옮겨가는 것을 막는 장치입니다.

주의할 점은 taint 와 toleration 만으로는 전용 노드가 완성되지 않는다는 것입니다. toleration 이 있는 Pod 가 반드시 그 노드로 간다는 보장은 없습니다. "들어올 수 있다" 일 뿐입니다. 정말 그 노드에만 두려면 node affinity 를 함께 걸어야 합니다. 자주 놓치는 부분입니다.

3.6. 우선순위와 선점

PriorityClass 로 Pod 에 우선순위 값을 줄 수 있습니다. 자원이 부족해 Pending 인 Pod 가 있을 때, 스케줄러는 더 낮은 우선순위의 Pod 를 쫓아내고(preemption) 자리를 만듭니다. 쫓겨난 Pod 는 다시 스케줄링 대기열로 갑니다.

여기서 3편의 PodDisruptionBudget 이 다시 나옵니다. 선점은 PDB 를 최대한 존중하려고 시도하지만, 존중하면 자리를 만들 수 없는 경우에는 PDB 를 어기고 진행합니다. PDB 가 절대적인 보장은 아니라는 점을 알아둘 필요가 있습니다.


4. 오토스케일링


지금까지는 "이미 있는 자원을 어떻게 나눌 것인가" 였습니다. 이제 자원 자체를 늘리고 줄이는 이야기입니다.

세 가지가 있고, 서로 다른 축을 다룹니다.

  1. HPA : Pod 개수를 늘리고 줄임 (수평)
  2. VPA : Pod 의 requests/limits 를 조정 (수직)
  3. Cluster Autoscaler : 노드 개수를 늘리고 줄임

4.1. HPA

지표를 보고 복제본 수를 조정합니다. 가장 널리 쓰입니다. 계산식은 단순합니다.

원하는 복제본 수 = 현재 복제본 수 × (현재 지표값 / 목표 지표값)

CPU 사용률 목표가 50% 인데 현재 100% 라면 복제본을 두 배로 만듭니다. 여기서 "사용률" 의 분모가 limits 가 아니라 requests 라는 점이 중요합니다. requests 를 작게 잡아두면 사용률이 쉽게 100% 를 넘어 과도하게 확장됩니다.

지표는 세 가지를 쓸 수 있습니다.

  1. Resource : CPU, 메모리. metrics-server 가 있어야 합니다.
  2. Custom : 초당 요청 수 같은 애플리케이션 지표.
  3. External : 큐 길이처럼 클러스터 밖의 지표.

메모리 기준 HPA 는 조심해야 합니다. JVM 처럼 한 번 잡은 메모리를 잘 반환하지 않는 런타임에서는, 부하가 줄어도 메모리 사용량이 내려가지 않아 축소가 일어나지 않습니다. 플래핑(flapping) 을 막기 위해 안정화 구간이 있습니다. 기본적으로 확장은 빠르게, 축소는 천천히(기본 5분 관찰) 반응합니다. behavior 로 이 속도를 조정할 수 있습니다.

4.2. VPA

Pod 의 requests 를 자동으로 조정합니다. 실제 사용량을 관찰해 적정값을 추천하거나 직접 적용합니다. 문제는 requests 를 바꾸려면 Pod 를 다시 만들어야 한다는 점이었습니다. 그래서 VPA 는 Pod 를 죽이고 새로 띄우는 방식으로 동작했고, 이 때문에 운영 환경에서 쓰기 부담스러웠습니다.

최근에는 In-place Pod Resize 가 들어와, 재시작 없이 자원을 조정하는 길이 열리고 있습니다. HPA 와 VPA 를 같은 지표로 함께 쓰면 안 됩니다. HPA 가 개수를 늘려 Pod 당 사용률을 낮추면, VPA 가 requests 를 줄이고, 그러면 사용률이 다시 올라 HPA 가 또 늘리는 식으로 서로를 자극합니다. VPA 는 추천만 받고 HPA 는 CPU 로 돌리는 식으로 역할을 가르는 것이 보통입니다.

4.3. Cluster Autoscaler

Pending 인 Pod 가 있으면 노드를 늘리고, 오래 놀고 있는 노드가 있으면 줄입니다. 여기서 HPA 와의 관계가 드러납니다. HPA 가 Pod 를 늘렸는데 노드에 자리가 없으면 Pod 가 Pending 이 되고, 그것을 보고 Cluster Autoscaler 가 노드를 추가합니다. 두 단계가 이어져 동작합니다.

축소할 때는 조건이 까다롭습니다. 그 노드의 Pod 를 다른 노드로 다 옮길 수 있어야 줄입니다. 그래서 다음 경우에는 노드가 줄어들지 않습니다.

  1. PDB 때문에 옮길 수 없는 Pod 가 있을 때
  2. 로컬 스토리지(emptyDir 등) 를 쓰는 Pod 가 있을 때
  3. DaemonSet 이 아닌 시스템 Pod 가 그 노드에만 있을 때

"노드가 비어 있는데 왜 안 줄어드나" 의 원인은 대개 이 목록 안에 있습니다.

4.4. Karpenter

Cluster Autoscaler 는 미리 정의된 노드 그룹 안에서 개수만 조절합니다. 그래서 그룹의 인스턴스 타입이 워크로드에 맞지 않으면 낭비가 생깁니다. Karpenter 는 Pending 인 Pod 의 요구사항을 보고 그에 맞는 인스턴스 타입을 직접 골라 노드를 띄웁니다. 노드 그룹을 미리 정의하지 않아도 되고, 더 저렴한 조합으로 재배치하는 통합(consolidation)도 합니다.

4.5. 이벤트 기반 확장

HPA 는 지표가 올라간 뒤에 반응하므로 이미 부하를 받은 다음입니다. 그리고 0 으로 줄일 수 없습니다. KEDA 는 큐 길이 같은 이벤트 소스를 직접 보고 확장하며, 0 에서 1 로 띄우는 것(scale to zero) 도 가능합니다. 배치 성격의 워크로드에서 자원을 크게 아낄 수 있습니다.


5. YARN 과 비교해보면


하둡 시리즈에서 YARN 을 공부한 입장에서 비교해보면 차이가 뚜렷했습니다.

  1. YARN 은 큐 중심이다 : 조직이나 팀별로 큐를 만들고 용량 비율을 배분합니다. 쿠버네티스에는 이런 계층적 큐가 기본으로 없고, ResourceQuota 로 네임스페이스에 상한을 거는 정도입니다. 그래서 배치 워크로드에 필요한 큐와 대기열을 얹기 위해 Volcano, Kueue 같은 프로젝트가 따로 있습니다.
  2. YARN 은 애플리케이션 단위, 쿠버네티스는 Pod 단위다 : YARN 은 ApplicationMaster 가 자기 작업의 컨테이너를 요청합니다. 쿠버네티스 스케줄러는 Pod 하나씩 독립적으로 배치합니다.
  3. 그래서 갱 스케줄링이 기본으로 없다 : 분산 학습처럼 "Pod 10개가 전부 떠야 의미가 있는" 작업에서, 쿠버네티스는 7개만 띄우고 3개는 Pending 으로 둘 수 있습니다. 7개가 자원을 잡은 채 대기만 하는 낭비가 생깁니다. 이것을 풀려면 역시 Volcano 같은 것을 씁니다.

범용 오케스트레이터로 출발했기 때문에 배치 작업에 필요한 개념이 뒤늦게 얹히고 있다는 인상을 받았습니다.


6. 조금 더 들어가보기 : 스로틀링과 메모리 회계


앞에서 CPU 는 느려지고 메모리는 죽는다고 정리했는데, 그것이 커널에서 어떻게 일어나는지까지 내려가 보겠습니다. 사용률은 낮은데 응답이 느린 상황을 진단하려면 이 층을 알아야 합니다.

6.1. CPU 스로틀링은 주기 단위로 일어난다

리눅스의 기본 스케줄러인 CFS(Completely Fair Scheduler) 는 CPU 상한을 기간과 할당량의 쌍으로 표현합니다.

cpu.max = "20000 100000"
          할당량   기간      → 100ms 마다 20ms, 즉 0.2 코어

여기서 중요한 것은 평균이 아니라 매 기간마다 따로 계산된다는 점입니다. 기본 기간은 100ms 입니다. 한 기간 안에서 할당량을 다 쓰면, 그 기간이 끝날 때까지 그 cgroup 의 모든 스레드가 강제로 멈춥니다. 다음 기간이 시작되면 다시 풀립니다.

6.2. 그래서 평균 사용률이 낮아도 느려진다

여기서 실무에서 자주 겪는 현상이 설명됩니다. CPU limit 을 1코어로 두고, 스레드가 8개인 애플리케이션을 돌린다고 하겠습니다. 요청이 들어와 8개 스레드가 동시에 일하면, 12.5ms 만에 100ms 어치 할당량을 전부 소진합니다. 나머지 87.5ms 동안 그 Pod 는 아무것도 하지 못하고 멈춰 있습니다.

모니터링에서 보이는 CPU 사용률은 여전히 낮습니다. 100ms 중 12.5ms 만 썼으니까요. 그런데 응답 시간의 p99 는 크게 튑니다. "자원은 남는데 왜 느리지" 의 전형적인 원인입니다. 이것은 지표로 확인할 수 있습니다.

cpu.stat 안의 값
  nr_periods      지난 기간 수
  nr_throttled    그중 스로틀링이 걸린 기간 수
  throttled_usec  멈춰 있던 총 시간

nr_throttled / nr_periods 비율이 높으면 limit 이 낮은 것입니다. 사용률만 보고 있으면 이 문제를 영영 못 찾습니다. 앞에서 지연에 민감한 서비스에는 CPU limit 을 걸지 않기도 한다고 했는데, 그 판단의 근거가 이것입니다.

6.3. requests 는 상한이 아니라 비중이다

requests.cpu 는 cgroup 의 가중치로 들어갑니다. 이 값은 경합이 있을 때만 의미가 있습니다. 노드에 여유가 있으면 요청량이 0.1 코어인 Pod 도 3코어를 쓸 수 있습니다. 그러다 다른 Pod 가 몰려오면 가중치 비율대로 나눠 갖게 됩니다.

그래서 requests 만 두고 limits 를 두지 않으면, 평소에는 빠르다가 노드가 붐빌 때 갑자기 느려집니다. 성능이 이웃에 따라 달라지는 셈이라, 예측 가능성과 맞바꾸는 선택입니다.

6.4. 메모리는 왜 죽을 수밖에 없는가

CPU 는 "잠시 안 주면" 되지만 메모리는 그럴 수 없습니다. 이미 할당해 프로세스가 쓰고 있는 메모리를 커널이 도로 가져갈 방법이 없습니다. 그래서 memory.max 를 넘으면 커널은 그 cgroup 안에서 프로세스를 죽입니다. 이것이 OOMKilled 입니다. 노드 전체의 OOM 이 아니라 그 컨테이너 안에서 일어나는 국소적인 OOM 이라는 점이 중요합니다.

노드에서 dmesg 를 보면 어느 cgroup 에서 무엇을 죽였는지가 남아 있습니다. 컨테이너 로그에는 아무것도 안 남고 그냥 사라지는 경우가 많은데, 커널 로그에는 기록됩니다.

6.5. 페이지 캐시가 사용량에 포함된다

자주 헷갈리는 부분입니다. memory.current 에는 애플리케이션이 직접 할당한 메모리뿐 아니라 파일을 읽으며 생긴 페이지 캐시도 포함됩니다. 그래서 파일을 많이 읽는 컨테이너는 실제로 쓰는 메모리는 적은데 사용량이 limit 근처까지 올라가 보입니다.

다행히 페이지 캐시는 회수 가능합니다. 압박이 오면 커널이 캐시를 버려 공간을 만듭니다. 그래서 캐시 때문에 바로 OOM 이 나지는 않습니다. 다만 모니터링 그래프만 보면 위험해 보이고, HPA 를 메모리 기준으로 걸어두었다면 불필요하게 확장되는 원인이 됩니다. 메모리 기준 HPA 를 조심해야 하는 이유 중 하나입니다.

memory.high 라는 값도 있습니다. max 가 넘으면 죽이는 선이라면, high 는 넘으면 회수 압력을 강하게 걸어 프로세스를 느리게 만드는 선입니다. 죽이기 전에 속도로 눌러보는 장치입니다.

6.6. 노드 압박은 다른 층의 이야기다

앞에서 OOMKilled 와 Evicted 를 구분했는데, 이제 그 차이가 분명해집니다.

  1. OOMKilled : 커널이 cgroup 한도를 근거로 죽인 것입니다. 그 컨테이너만의 문제입니다.
  2. Evicted : kubelet이 노드 전체의 여유를 보고 내보낸 것입니다. 커널이 아니라 사용자 공간의 판단입니다.

kubelet 은 노드의 여유 메모리와 디스크를 주기적으로 확인하다가 임계치를 넘으면 대상을 골라 내보냅니다. 그래서 Evicted 는 자기 한도를 지키던 Pod 에게도 일어날 수 있습니다. 이웃이 문제를 일으켰는데 제가 쫓겨나는 상황입니다.

최근 커널에는 PSI(Pressure Stall Information) 라는 지표가 있어, 프로세스들이 자원을 기다리느라 멈춰 있던 시간의 비율을 직접 알려줍니다. 사용률은 자원이 얼마나 쓰였는지를 말할 뿐 부족했는지는 말해주지 않는데, PSI 는 그 부족을 직접 측정한다는 점에서 더 유용합니다.

6.7. swap 이 기본적으로 꺼져 있던 이유

오랫동안 쿠버네티스는 노드에 swap 을 끄도록 요구했습니다. 이유는 예측 가능성 때문입니다. swap 이 켜져 있으면 메모리가 부족할 때 죽는 대신 디스크로 밀려나는데, 그러면 프로세스가 살아는 있지만 극단적으로 느려집니다. 빠르게 실패하는 편이 낫다는 판단이었습니다. 스케줄러가 보는 메모리 계산도 흐려집니다.

최근 버전에서는 제한적으로 swap 을 쓸 수 있게 되었지만, 기본 전제는 여전히 "메모리가 부족하면 죽인다" 입니다.


결론


스케줄링을 정리하면서 가장 크게 깨달은 것은, 스케줄러가 실제 사용량을 보지 않는다는 사실이었습니다. requests 라는 선언된 숫자만 보고 자리를 정합니다. 처음에는 이상하게 느껴졌는데, 실제 사용량은 계속 변하기 때문에 그것으로 배치를 결정하면 판단이 매 순간 흔들린다는 것을 생각하니 납득이 되었습니다. 대신 그 숫자를 정확히 적는 책임이 사람에게 남는다는 것이 이 설계의 대가였습니다.

두 번째는 CPU 와 메모리의 비대칭이었습니다. 하나는 부족하면 느려지고 하나는 부족하면 죽습니다. 같은 "자원 제한" 이라는 말로 묶여 있지만 성질이 전혀 다르고, 그래서 limits 를 거는 판단도 달라야 했습니다. OOMKilled 와 스로틀링을 구분해서 볼 수 있게 된 것이 이번 편의 가장 실용적인 소득이었습니다.

세 번째는 taint 와 affinity 를 함께 써야 한다는 점이었습니다. taint 는 "들어오지 못하게" 하는 것이지 "이리로 오게" 하는 것이 아니라는 구분이, 실수하기 딱 좋은 지점으로 보였습니다. 밀어내는 장치와 끌어당기는 장치가 따로라는 것을 알고 나니 헷갈리지 않게 되었습니다.

마지막으로 YARN 과의 비교가 도움이 되었습니다. 두 시스템이 같은 문제를 놓고 큐 중심이냐 Pod 중심이냐로 갈렸고, 그 선택이 배치 워크로드에서의 약점으로 이어졌습니다. 하나를 공부해두면 다른 하나를 볼 때 질문할 거리가 생긴다는 것을 다시 느꼈습니다.


공부 참고 자료


📑 1. Kubernetes 공식 문서 - Resource Management for Pods

📑 2. Kubernetes 공식 문서 - Pod Quality of Service Classes

📑 3. Kubernetes 공식 문서 - Node-pressure Eviction

📑 4. Kubernetes 공식 문서 - Assigning Pods to Nodes

📑 5. Kubernetes 공식 문서 - Taints and Tolerations

📑 6. Kubernetes 공식 문서 - Pod Topology Spread Constraints

📑 7. Kubernetes 공식 문서 - Pod Priority and Preemption

📑 8. Kubernetes 공식 문서 - Horizontal Pod Autoscaling

📑 9. Cluster Autoscaler FAQ

📑 10. Karpenter 공식 문서

📑 11. KEDA 공식 문서

Comments