🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

쿠버네티스 스케줄링과 노드 배치 전략 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 29. · 11 Views

쿠버네티스 스케줄링과 노드 배치 전략 완벽 가이드

쿠버네티스에서 파드를 원하는 노드에 정확하게 배치하는 방법을 알아봅니다. NodeSelector부터 Affinity, Taint/Toleration까지 스케줄링의 모든 것을 다룹니다.


목차

  1. NodeSelector_기본_사용법
  2. Node_Affinity_설정
  3. Pod_Affinity_Anti-Affinity
  4. Taint와_Toleration
  5. PriorityClass_활용
  6. 리소스_기반_스케줄링

1. NodeSelector 기본 사용법

어느 날 김개발 씨가 쿠버네티스 클러스터에 애플리케이션을 배포했습니다. 그런데 이상한 일이 벌어졌습니다.

GPU가 필요한 머신러닝 파드가 CPU 전용 노드에 배치되어 전혀 동작하지 않는 것이었습니다. "어떻게 하면 특정 파드를 원하는 노드에 배치할 수 있을까요?"

NodeSelector는 파드를 특정 레이블이 붙은 노드에만 배치하도록 지정하는 가장 간단한 방법입니다. 마치 택배 기사가 "서울 지역만 배송"이라는 조건을 보고 해당 지역으로만 물건을 전달하는 것과 같습니다.

노드에 레이블을 붙이고, 파드에서 그 레이블을 선택하면 됩니다.

다음 코드를 살펴봅시다.

# 먼저 노드에 레이블을 추가합니다
kubectl label nodes worker-node-1 disktype=ssd
kubectl label nodes worker-node-2 gpu=nvidia

# 파드 스펙에서 nodeSelector를 지정합니다
apiVersion: v1
kind: Pod
metadata:
  name: ml-training-pod
spec:
  # nodeSelector로 원하는 노드 선택
  nodeSelector:
    gpu: nvidia
  containers:
  - name: tensorflow
    image: tensorflow/tensorflow:latest-gpu
    resources:
      limits:
        nvidia.com/gpu: 1

김개발 씨는 입사 6개월 차 DevOps 엔지니어입니다. 회사에서 쿠버네티스 클러스터를 운영하면서 다양한 워크로드를 관리하고 있었습니다.

어느 날 데이터 사이언스 팀에서 급하게 요청이 들어왔습니다. "김개발 씨, 우리 머신러닝 모델 학습 파드가 자꾸 일반 노드에 배치되어서 GPU를 못 쓰고 있어요.

어떻게 좀 해주세요!" 확인해보니 정말 그랬습니다. 클러스터에는 GPU 노드와 일반 노드가 섞여 있었는데, 스케줄러가 아무 노드에나 파드를 배치하고 있었던 것입니다.

선배 박시니어 씨가 다가와 화면을 살펴봅니다. "아, 이건 NodeSelector를 사용하면 간단히 해결할 수 있어요." 그렇다면 NodeSelector란 정확히 무엇일까요?

쉽게 비유하자면, NodeSelector는 마치 도서관의 책 분류 시스템과 같습니다. 도서관에서는 책에 분류 번호를 붙여서 해당 서가에만 꽂아두죠.

"소설은 A구역, 기술서적은 B구역"처럼요. NodeSelector도 마찬가지입니다.

노드에 레이블이라는 분류표를 붙이고, 파드가 특정 분류표가 있는 노드만 찾아가도록 하는 것입니다. NodeSelector가 없다면 어떤 일이 벌어질까요?

쿠버네티스 스케줄러는 기본적으로 리소스 여유가 있는 아무 노드에나 파드를 배치합니다. 이것이 문제가 되는 상황은 여러 가지가 있습니다.

GPU 워크로드가 CPU 노드에 배치되거나, SSD가 필요한 데이터베이스가 HDD 노드에 배치되거나, 특정 지역의 노드에만 배치해야 하는 규정 준수 요건을 만족시키지 못하는 경우가 생깁니다. 바로 이런 문제를 해결하기 위해 NodeSelector가 존재합니다.

사용 방법은 두 단계로 나뉩니다. 먼저 노드에 레이블을 붙입니다.

kubectl label nodes 명령어를 사용하면 됩니다. 그 다음 파드 스펙의 nodeSelector 필드에 원하는 레이블을 지정합니다.

위의 코드를 살펴보겠습니다. 먼저 worker-node-1에는 disktype=ssd라는 레이블을, worker-node-2에는 gpu=nvidia라는 레이블을 붙였습니다.

그 다음 파드 스펙에서 nodeSelector에 gpu: nvidia를 지정했습니다. 이제 이 파드는 반드시 gpu=nvidia 레이블이 있는 노드에만 배치됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 금융 서비스를 운영한다고 가정해봅시다.

규정상 고객 데이터를 처리하는 파드는 특정 보안 등급의 노드에서만 실행되어야 합니다. 이럴 때 security-level=high라는 레이블을 보안 노드에 붙이고, 민감한 워크로드의 파드에서 이 레이블을 선택하면 됩니다.

하지만 주의할 점도 있습니다. NodeSelector에서 지정한 레이블을 가진 노드가 클러스터에 없으면 파드는 영원히 Pending 상태에 머물게 됩니다.

또한 NodeSelector는 "이 레이블이 있는 노드에만 배치해라"라는 단순한 조건만 표현할 수 있습니다. "가능하면 이 노드에 배치하되, 안 되면 다른 곳도 괜찮아"와 같은 유연한 조건은 표현하지 못합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 GPU 노드에 gpu=nvidia 레이블을 붙이고, 머신러닝 파드에 nodeSelector를 추가했습니다.

곧 데이터 사이언스 팀에서 감사 인사가 왔습니다. "덕분에 잘 돌아가요!" NodeSelector는 단순하지만 강력합니다.

하지만 더 복잡한 조건이 필요하다면 다음에 배울 Node Affinity를 사용해야 합니다.

실전 팁

💡 - 노드 레이블은 kubectl get nodes --show-labels 명령어로 확인할 수 있습니다

  • 여러 레이블을 지정하면 AND 조건으로 동작합니다
  • 쿠버네티스가 기본 제공하는 레이블(kubernetes.io/os, kubernetes.io/arch 등)도 활용할 수 있습니다

2. Node Affinity 설정

김개발 씨가 NodeSelector를 잘 활용하고 있던 어느 날, 새로운 요구사항이 생겼습니다. "GPU 노드가 여유가 없으면 CPU 노드에라도 배치해주세요.

그리고 가능하면 최신 GPU가 있는 노드를 선호하게 해주세요." NodeSelector로는 이런 복잡한 조건을 표현할 수 없었습니다.

Node Affinity는 NodeSelector의 확장판으로, 더 풍부한 표현력을 제공합니다. "반드시 이 조건을 만족해야 한다"는 필수 조건과 "가능하면 이 조건을 선호한다"는 선호 조건을 모두 표현할 수 있습니다.

마치 집을 구할 때 "역세권은 필수, 공원 근처면 더 좋고"라고 조건을 거는 것과 같습니다.

다음 코드를 살펴봅시다.

apiVersion: v1
kind: Pod
metadata:
  name: web-server
spec:
  affinity:
    nodeAffinity:
      # 필수 조건: 반드시 만족해야 함
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - linux
      # 선호 조건: 가능하면 이 노드를 선호
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd
  containers:
  - name: nginx
    image: nginx:latest

김개발 씨는 NodeSelector를 마스터한 후 자신감이 붙었습니다. 그런데 이번에는 인프라팀에서 까다로운 요청이 들어왔습니다.

"김개발 씨, 이번에 배포하는 웹 서버는요, 일단 Linux 노드에만 배치되어야 해요. 그건 필수예요.

그리고 가능하면 SSD 노드에 배치하면 좋겠는데, SSD 노드가 꽉 차면 HDD 노드에 배치해도 괜찮아요." 김개발 씨는 고민에 빠졌습니다. NodeSelector로는 "가능하면"이라는 조건을 표현할 수 없었기 때문입니다.

이때 박시니어 씨가 힌트를 줍니다. "Node Affinity를 사용해봐요.

훨씬 더 세밀한 제어가 가능해요." Node Affinity란 무엇일까요? 쉽게 비유하자면, Node Affinity는 마치 부동산 앱에서 집을 검색하는 것과 같습니다.

"서울 지역은 필수"라는 조건과 "역세권이면 좋겠다"는 선호 조건을 함께 설정할 수 있죠. 필수 조건을 만족하는 집들 중에서 선호 조건을 더 많이 만족하는 집이 상위에 노출됩니다.

Node Affinity에는 두 가지 핵심 개념이 있습니다. 첫 번째는 requiredDuringSchedulingIgnoredDuringExecution입니다.

이름이 길지만 의미는 간단합니다. "스케줄링할 때 반드시 이 조건을 만족해야 한다"는 뜻입니다.

뒷부분의 IgnoredDuringExecution은 "이미 실행 중인 파드는 조건이 바뀌어도 쫓아내지 않는다"는 의미입니다. 두 번째는 preferredDuringSchedulingIgnoredDuringExecution입니다.

"가능하면 이 조건을 만족하는 노드를 선호한다"는 뜻입니다. 이 조건을 만족하지 못해도 파드는 배치될 수 있습니다.

선호 조건에는 weight라는 가중치가 있습니다. 1부터 100까지 설정할 수 있으며, 숫자가 클수록 해당 조건의 중요도가 높습니다.

여러 선호 조건이 있을 때 스케줄러는 각 노드의 점수를 계산하여 가장 높은 점수의 노드를 선택합니다. 위의 코드를 분석해보겠습니다.

requiredDuringScheduling 부분에서 kubernetes.io/os 레이블이 linux인 노드만 후보로 선정합니다. 이 조건을 만족하지 않는 노드는 아예 고려 대상에서 제외됩니다.

preferredDuringScheduling 부분에서는 disktype이 ssd인 노드에 가중치 80을 부여합니다. Linux 노드 중에서 SSD 노드가 있다면 그쪽을 선호하게 됩니다.

operator 필드는 다양한 비교 연산을 지원합니다. In, NotIn, Exists, DoesNotExist, Gt, Lt 등을 사용할 수 있습니다.

예를 들어 Gt는 "보다 크다"를 의미하여 숫자 비교에 활용됩니다. 실제 프로젝트에서는 어떻게 활용할까요?

대규모 클러스터를 운영하는 기업에서는 노드를 여러 그룹으로 나누어 관리합니다. 프로덕션 워크로드는 고성능 노드에, 개발 환경은 저사양 노드에 배치하는 식입니다.

Node Affinity를 사용하면 "프로덕션 노드가 꽉 차면 개발 노드 중 여유 있는 곳에 배치"와 같은 유연한 전략을 구현할 수 있습니다. 주의해야 할 점이 있습니다.

preferredDuringScheduling은 "선호"일 뿐 "보장"이 아닙니다. 다른 요소들, 예를 들어 리소스 가용량, 다른 affinity 규칙 등에 의해 선호하지 않는 노드에 배치될 수 있습니다.

또한 weight 값을 너무 복잡하게 설정하면 디버깅이 어려워집니다. 김개발 씨는 Node Affinity를 적용한 후 만족스러운 결과를 얻었습니다.

웹 서버는 평소에는 SSD 노드에서 빠르게 동작하다가, SSD 노드가 바쁠 때는 HDD 노드에서라도 안정적으로 서비스를 제공했습니다. "Node Affinity 덕분에 훨씬 유연한 배치가 가능해졌네요!"

실전 팁

💡 - 필수 조건이 너무 엄격하면 파드가 Pending 상태에 빠질 수 있으니 주의하세요

  • weight 값은 팀 내에서 일관된 기준을 정해서 사용하는 것이 좋습니다
  • kubectl describe node 명령어로 노드의 레이블을 확인하며 설정하세요

3. Pod Affinity Anti-Affinity

김개발 씨가 마이크로서비스 아키텍처를 구축하던 중 새로운 고민이 생겼습니다. 프론트엔드 파드와 백엔드 파드가 서로 다른 노드에 배치되어 네트워크 지연이 발생했습니다.

반대로, 같은 서비스의 복제본들이 한 노드에 몰려서 그 노드가 죽으면 서비스 전체가 중단되는 문제도 있었습니다.

Pod Affinity는 특정 파드와 같은 노드나 같은 영역에 배치되도록 하는 규칙입니다. 반대로 Pod Anti-Affinity는 특정 파드와 다른 노드에 배치되도록 합니다.

마치 친한 친구끼리는 같은 반에 배정하고, 사이가 안 좋은 학생들은 다른 반에 배정하는 것과 같습니다.

다음 코드를 살펴봅시다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-frontend
  template:
    metadata:
      labels:
        app: web-frontend
    spec:
      affinity:
        # 백엔드와 같은 노드에 배치 선호
        podAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: web-backend
              topologyKey: kubernetes.io/hostname
        # 같은 프론트엔드끼리는 다른 노드에 분산
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: web-frontend
            topologyKey: kubernetes.io/hostname
      containers:
      - name: frontend
        image: nginx:latest

김개발 씨는 요즘 마이크로서비스 아키텍처에 푹 빠져 있었습니다. 프론트엔드, 백엔드, 데이터베이스를 각각 독립적인 파드로 분리하여 운영하고 있었죠.

그런데 모니터링 대시보드를 보다가 이상한 점을 발견했습니다. "프론트엔드에서 백엔드로 요청을 보내는데 왜 이렇게 지연 시간이 높지?" 확인해보니 프론트엔드 파드는 서울 리전의 노드에, 백엔드 파드는 부산 리전의 노드에 배치되어 있었습니다.

물리적 거리 때문에 네트워크 지연이 발생한 것입니다. 박시니어 씨가 또 한 번 도움의 손길을 내밀었습니다.

"Pod Affinity를 써보세요. 관련 있는 파드들을 가까이 배치할 수 있어요." Pod Affinity는 노드가 아닌 다른 파드를 기준으로 배치를 결정합니다.

비유하자면 이렇습니다. Node Affinity가 "서울 지역의 집을 찾아줘"라면, Pod Affinity는 "내 친구가 사는 동네 근처의 집을 찾아줘"입니다.

이미 배치된 파드의 위치를 기준으로 새 파드의 위치를 결정하는 것입니다. 여기서 중요한 개념이 topologyKey입니다.

topologyKey는 "같은 위치"의 기준을 정의합니다. kubernetes.io/hostname을 사용하면 "같은 노드"를 의미합니다.

topology.kubernetes.io/zone을 사용하면 "같은 가용 영역(AZ)"을 의미합니다. 클라우드 환경에서는 여러 가용 영역에 걸쳐 노드가 분산되어 있기 때문에 이 개념이 중요합니다.

위의 코드를 살펴보겠습니다. podAffinity 부분에서는 app: web-backend 레이블을 가진 파드와 같은 노드(hostname)에 배치되기를 선호한다고 명시했습니다.

프론트엔드와 백엔드가 같은 노드에 있으면 네트워크 통신이 훨씬 빨라집니다. podAntiAffinity 부분에서는 app: web-frontend 레이블을 가진 파드, 즉 자기 자신과 같은 종류의 파드와는 다른 노드에 배치되어야 한다고 명시했습니다.

이것은 required 조건이므로 반드시 지켜집니다. 왜 Anti-Affinity가 필요할까요?

같은 서비스의 복제본이 한 노드에 몰려 있다고 가정해봅시다. 그 노드에 장애가 발생하면 어떻게 될까요?

서비스 전체가 중단됩니다. Anti-Affinity를 사용하면 복제본들이 여러 노드에 분산되어 고가용성을 확보할 수 있습니다.

실무에서는 이 두 가지를 조합하여 사용합니다. 예를 들어 Redis 클러스터를 구축한다면, 마스터와 슬레이브가 같은 가용 영역에 있으면 좋지만(Affinity), 같은 노드에 있으면 안 됩니다(Anti-Affinity).

이런 복잡한 요구사항도 Pod Affinity와 Anti-Affinity를 조합하면 표현할 수 있습니다. 주의할 점이 있습니다.

Pod Affinity와 Anti-Affinity는 스케줄링 성능에 영향을 줄 수 있습니다. 특히 대규모 클러스터에서 복잡한 규칙을 적용하면 스케줄러가 최적의 노드를 찾는 데 시간이 오래 걸릴 수 있습니다.

필요한 경우에만 사용하고, 규칙은 단순하게 유지하는 것이 좋습니다. 김개발 씨는 Pod Affinity와 Anti-Affinity를 적용한 후 두 가지 문제를 모두 해결했습니다.

프론트엔드와 백엔드의 통신 지연이 줄어들었고, 한 노드 장애 시에도 서비스가 중단되지 않았습니다. "파드 간의 관계까지 고려한 스케줄링이라니, 쿠버네티스가 정말 똑똑하네요!"

실전 팁

💡 - Anti-Affinity는 replicas 수만큼의 노드가 필요하므로 클러스터 규모를 고려하세요

  • topologyKey를 zone으로 설정하면 가용 영역 수준의 고가용성을 확보할 수 있습니다
  • 디버깅할 때는 kubectl describe pod로 스케줄링 실패 이유를 확인하세요

4. Taint와 Toleration

김개발 씨가 클러스터를 운영하던 중 심각한 문제가 발생했습니다. 마스터 노드에 일반 워크로드가 배치되어 클러스터 전체가 불안정해진 것입니다.

또한 GPU 노드에 GPU를 사용하지 않는 파드들이 배치되어 정작 필요한 파드가 자리를 못 찾는 상황도 있었습니다. 특정 노드를 보호하면서도 필요할 때는 사용할 수 있는 방법이 필요했습니다.

Taint는 노드에 "오염 표시"를 하여 일반 파드가 배치되지 못하도록 막는 기능입니다. Toleration은 파드가 특정 Taint를 "용인"하여 해당 노드에 배치될 수 있도록 허용하는 것입니다.

마치 "관계자 외 출입금지" 표지판이 붙은 구역에 출입증이 있는 사람만 들어갈 수 있는 것과 같습니다.

다음 코드를 살펴봅시다.

# 노드에 Taint 추가
kubectl taint nodes gpu-node-1 gpu=true:NoSchedule
kubectl taint nodes master-node node-role=master:NoExecute

# Taint를 용인하는 파드 정의
apiVersion: v1
kind: Pod
metadata:
  name: gpu-workload
spec:
  tolerations:
  # gpu=true:NoSchedule Taint를 용인
  - key: "gpu"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"
  # 또는 특정 키의 모든 값을 용인
  - key: "gpu"
    operator: "Exists"
    effect: "NoSchedule"
  nodeSelector:
    gpu: nvidia
  containers:
  - name: cuda-app
    image: nvidia/cuda:latest

클러스터 관리자로서 김개발 씨의 고민은 깊어져 갔습니다. GPU 노드는 비싸고 소중한 자원인데, GPU를 사용하지 않는 일반 파드들이 그 노드에 배치되어 리소스를 낭비하고 있었습니다.

"이 노드는 GPU 워크로드 전용으로 쓰고 싶은데, 어떻게 해야 일반 파드가 못 들어오게 막을 수 있을까요?" 박시니어 씨가 화이트보드에 그림을 그리며 설명합니다. "Taint와 Toleration을 사용하면 됩니다.

생각의 방향을 바꿔보세요." 지금까지 배운 NodeSelector와 Affinity는 파드 입장에서 "나는 이 노드에 가고 싶다"고 말하는 것이었습니다. 반면 Taint는 노드 입장에서 "나는 아무나 받지 않는다"고 선언하는 것입니다.

관점이 완전히 다릅니다. 비유하자면 이렇습니다.

NodeSelector는 "나는 VIP석에 앉고 싶어요"라고 요청하는 것이고, Taint는 "이 자리는 VIP 전용입니다"라고 표시해두는 것입니다. VIP 표시가 있어도 VIP 카드(Toleration)가 있으면 앉을 수 있습니다.

Taint는 세 가지 effect를 가질 수 있습니다. NoSchedule은 가장 많이 사용됩니다.

이 Taint가 있는 노드에는 Toleration이 없는 파드가 새로 배치되지 않습니다. 하지만 이미 실행 중인 파드는 영향받지 않습니다.

PreferNoSchedule은 소프트한 버전입니다. 가능하면 배치하지 않지만, 다른 선택지가 없으면 배치될 수 있습니다.

NoExecute는 가장 강력합니다. 새로운 파드 배치를 막을 뿐 아니라, 이미 실행 중인 파드도 Toleration이 없으면 쫓아냅니다.

위의 코드를 분석해보겠습니다. 첫 번째 명령어에서 gpu-node-1에 gpu=true:NoSchedule Taint를 추가했습니다.

이제 이 노드에는 Toleration이 없는 파드가 배치되지 않습니다. 파드 스펙에서 tolerations 필드를 보면, key가 "gpu"이고 value가 "true"이며 effect가 "NoSchedule"인 Taint를 용인한다고 명시했습니다.

이 파드만 gpu-node-1에 배치될 수 있습니다. operator 필드에는 두 가지 옵션이 있습니다.

"Equal"은 key, value, effect가 모두 일치해야 합니다. "Exists"는 key와 effect만 일치하면 됩니다.

실무에서 Taint는 다양한 용도로 활용됩니다. 쿠버네티스 마스터 노드에는 기본적으로 node-role.kubernetes.io/master:NoSchedule Taint가 적용되어 있습니다.

그래서 일반 워크로드가 마스터 노드에 배치되지 않습니다. GPU 노드, 고메모리 노드 등 특수 목적 노드에도 Taint를 적용하여 해당 리소스가 필요한 파드만 배치되도록 합니다.

노드에 문제가 생겼을 때도 Taint가 자동으로 추가됩니다. 노드가 NotReady 상태가 되면 node.kubernetes.io/not-ready:NoExecute Taint가 추가되어 파드들이 다른 노드로 이동합니다.

이것이 쿠버네티스의 자가 치유 기능의 핵심입니다. 주의할 점이 있습니다.

Toleration이 있다고 해서 그 노드에 반드시 배치되는 것은 아닙니다. Toleration은 "그 노드에 배치될 수 있다"는 허가일 뿐입니다.

특정 노드에 반드시 배치하려면 nodeSelector나 Node Affinity와 함께 사용해야 합니다. 김개발 씨는 GPU 노드에 Taint를 적용하고, GPU 워크로드 파드에만 Toleration을 추가했습니다.

이제 비싼 GPU 리소스가 낭비되는 일이 없어졌습니다. "Taint와 Toleration의 조합이 정말 강력하네요.

노드를 용도별로 깔끔하게 관리할 수 있겠어요!"

실전 팁

💡 - kubectl describe node로 노드의 Taint를 확인할 수 있습니다

  • Taint를 제거하려면 kubectl taint nodes <node> <key>:<effect>- 명령어를 사용합니다
  • DaemonSet은 기본적으로 일부 시스템 Taint에 대한 Toleration이 있습니다

5. PriorityClass 활용

어느 날 클러스터에 장애가 발생했습니다. 리소스가 부족해서 새 파드를 배치할 수 없는 상황이었는데, 문제는 중요한 프로덕션 파드가 Pending 상태에 머물러 있는 동안 덜 중요한 배치 작업 파드들이 리소스를 점유하고 있었다는 것입니다.

김개발 씨는 파드 간의 우선순위를 정해야 할 필요성을 절실히 느꼈습니다.

PriorityClass는 파드의 우선순위를 정의하는 클러스터 수준의 리소스입니다. 리소스가 부족할 때 높은 우선순위의 파드가 낮은 우선순위의 파드를 밀어내고(preemption) 배치될 수 있습니다.

마치 응급실에서 위급한 환자를 먼저 치료하는 것과 같습니다.

다음 코드를 살펴봅시다.

# PriorityClass 정의
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "프로덕션 핵심 서비스용"
preemptionPolicy: PreemptLowerPriority
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 1000
globalDefault: false
description: "배치 작업 및 개발용"
preemptionPolicy: Never
---
# 파드에서 PriorityClass 사용
apiVersion: v1
kind: Pod
metadata:
  name: critical-service
spec:
  priorityClassName: high-priority
  containers:
  - name: api-server
    image: my-api:latest
    resources:
      requests:
        memory: "1Gi"
        cpu: "500m"

김개발 씨의 회사에서 대형 장애가 발생했습니다. 블랙 프라이데이 세일 기간이었는데, 트래픽이 폭증하면서 클러스터 리소스가 바닥났습니다.

문제는 그 다음이었습니다. 결제 서비스 파드가 스케일 아웃되어야 하는데 Pending 상태에 머물러 있었습니다.

정작 리소스를 점유하고 있는 것은 통계 분석용 배치 작업 파드들이었습니다. 고객들은 결제를 못 하고 있는데, 내일 봐도 되는 통계 분석이 돌아가고 있었던 것입니다.

"파드마다 중요도를 정해서 급한 것부터 처리하게 할 수는 없을까요?" 박시니어 씨가 심각한 표정으로 대답합니다. "PriorityClass를 미리 설정해뒀어야 했어요.

이제라도 적용합시다." PriorityClass는 파드의 "VIP 등급"을 정의하는 것입니다. 비유하자면, 공항의 탑승 우선순위와 같습니다.

퍼스트 클래스 승객은 비즈니스 클래스보다 먼저, 비즈니스 클래스는 이코노미보다 먼저 탑승합니다. 리소스가 충분할 때는 모두 탑승할 수 있지만, 좌석이 부족하면 높은 등급의 승객이 우선입니다.

PriorityClass의 핵심은 value 필드입니다. value는 정수값으로, 숫자가 클수록 우선순위가 높습니다.

쿠버네티스는 10억(1,000,000,000)을 시스템 예약 값으로 사용하므로, 사용자 정의 PriorityClass는 그보다 작은 값을 사용해야 합니다. 일반적으로 1000, 10000, 100000, 1000000 같은 값을 단계별로 사용합니다.

preemptionPolicy는 중요한 옵션입니다. PreemptLowerPriority로 설정하면 이 파드가 배치될 자리가 없을 때 낮은 우선순위의 파드를 쫓아내고 그 자리를 차지합니다.

이것을 **선점(preemption)**이라고 합니다. Never로 설정하면 선점을 하지 않고 자리가 빌 때까지 기다립니다.

위의 코드를 분석해보겠습니다. high-priority PriorityClass는 value가 1000000이고 preemptionPolicy가 PreemptLowerPriority입니다.

이 클래스를 사용하는 파드는 리소스가 부족하면 낮은 우선순위 파드를 밀어낼 수 있습니다. low-priority PriorityClass는 value가 1000이고 preemptionPolicy가 Never입니다.

배치 작업처럼 급하지 않은 워크로드에 적합합니다. 다른 파드를 밀어내지 않고 여유 리소스가 생길 때까지 기다립니다.

파드에서는 priorityClassName 필드로 사용할 PriorityClass를 지정합니다. 실무에서는 보통 3-4단계의 우선순위를 정의합니다.

system-critical(시스템 핵심 컴포넌트), production-high(프로덕션 핵심 서비스), production-low(프로덕션 보조 서비스), batch(배치 작업) 정도로 나누는 것이 일반적입니다. 각 팀이나 서비스에 적절한 PriorityClass를 할당하고, 이를 팀 간에 합의하여 관리합니다.

주의할 점이 있습니다. 선점은 강력한 기능이지만 남용하면 안 됩니다.

높은 우선순위 파드가 너무 많으면 낮은 우선순위 파드가 계속 쫓겨나는 "기아 상태"가 발생할 수 있습니다. 또한 선점 시 기존 파드가 갑자기 종료되므로, 애플리케이션이 graceful shutdown을 잘 처리해야 합니다.

김개발 씨는 장애 복구 후 PriorityClass 체계를 수립했습니다. 결제, 인증 같은 핵심 서비스는 high-priority를, 통계, 로그 처리 같은 배치 작업은 low-priority를 사용하도록 했습니다.

"다음 블랙 프라이데이에는 결제 서비스가 배치 작업 때문에 밀리는 일은 없을 거예요!"

실전 팁

💡 - globalDefault: true로 설정하면 priorityClassName을 지정하지 않은 파드의 기본값이 됩니다

  • 선점당한 파드는 PodDisruptionBudget을 존중하지 않으므로 주의하세요
  • kubectl get priorityclass로 클러스터의 PriorityClass 목록을 확인할 수 있습니다

6. 리소스 기반 스케줄링

김개발 씨가 파드를 배포했는데 계속 Pending 상태였습니다. "노드는 여유가 있는 것 같은데 왜 배치가 안 되지?" kubectl describe pod로 확인해보니 "Insufficient memory"라는 메시지가 보였습니다.

알고 보니 파드에 설정한 리소스 요청량이 실제 필요량보다 훨씬 컸던 것입니다.

리소스 기반 스케줄링은 파드가 요청한 CPU와 메모리를 기준으로 배치할 노드를 결정하는 것입니다. requests는 "최소한 이만큼은 필요해요"라는 보장된 자원이고, limits는 "최대 이만큼까지 쓸 수 있어요"라는 상한선입니다.

마치 회의실 예약에서 "최소 5명은 앉을 수 있어야 하고, 최대 10명까지 사용할 예정"이라고 말하는 것과 같습니다.

다음 코드를 살펴봅시다.

apiVersion: v1
kind: Pod
metadata:
  name: resource-demo
spec:
  containers:
  - name: app
    image: my-app:latest
    resources:
      # 스케줄링 기준: 이만큼의 리소스가 있는 노드에만 배치
      requests:
        memory: "256Mi"
        cpu: "250m"       # 0.25 CPU 코어
      # 실행 시 상한: 이 이상 사용하면 제한됨
      limits:
        memory: "512Mi"   # 초과 시 OOMKilled
        cpu: "500m"       # 초과 시 throttling
---
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: development
spec:
  limits:
  - default:
      memory: "256Mi"
      cpu: "200m"
    defaultRequest:
      memory: "128Mi"
      cpu: "100m"
    type: Container

김개발 씨는 새 서비스를 배포하면서 "넉넉하게 잡자"는 생각으로 메모리 요청량을 8Gi로 설정했습니다. 그런데 파드가 배치되지 않았습니다.

클러스터의 모든 노드가 8Gi의 여유 메모리가 없었기 때문입니다. 실제로 그 서비스는 500Mi 정도만 사용하고 있었습니다.

리소스 설정을 잘못한 것이 화근이었습니다. 박시니어 씨가 한숨을 쉬며 말합니다.

"리소스 설정은 스케줄링의 핵심이에요. 제대로 이해하고 설정해야 해요." 쿠버네티스 스케줄러는 requests 값을 기준으로 노드를 선택합니다.

비유하자면, requests는 호텔 예약과 같습니다. "2인실을 예약합니다"라고 하면 호텔은 2인실을 확보해줍니다.

실제로 1명만 묵어도 2인실 비용을 지불하고, 그 방은 다른 손님에게 배정되지 않습니다. 스케줄러는 각 노드의 "할당 가능한 리소스"를 계산합니다.

노드 전체 리소스에서 이미 배치된 파드들의 requests 합계를 뺀 값입니다. 새 파드의 requests가 이 여유분보다 작거나 같은 노드에만 배치될 수 있습니다.

limits는 스케줄링과는 관계없습니다. limits는 파드가 실행될 때 실제로 사용할 수 있는 리소스의 상한선입니다.

CPU limits를 초과하면 throttling이 발생하여 속도가 느려집니다. 메모리 limits를 초과하면 OOMKilled되어 파드가 재시작됩니다.

흥미로운 점은 requests와 limits가 다를 수 있다는 것입니다. requests가 256Mi이고 limits가 512Mi라면, 스케줄링 시에는 256Mi를 기준으로 노드가 선택됩니다.

하지만 실행 중에는 512Mi까지 사용할 수 있습니다. 이것을 **오버커밋(overcommit)**이라고 합니다.

위의 코드에서 LimitRange도 살펴보겠습니다. LimitRange는 네임스페이스 수준에서 기본 리소스 설정을 정의합니다.

개발자가 resources를 지정하지 않은 파드를 배포하면 자동으로 기본값이 적용됩니다. 이렇게 하면 리소스 설정 누락으로 인한 문제를 방지할 수 있습니다.

CPU 단위에 대해 알아두면 좋습니다. 1 CPU는 1개의 가상 코어를 의미합니다.

250m은 0.25 CPU, 즉 1코어의 25%입니다. m은 밀리코어(millicores)의 약자입니다.

1000m = 1 CPU입니다. 메모리 단위는 Mi(메비바이트), Gi(기비바이트)를 주로 사용합니다.

1Gi = 1024Mi입니다. MB, GB와 약간 다르니 주의하세요.

실무에서 리소스를 어떻게 설정해야 할까요? 처음에는 보수적으로 설정하고, 모니터링 데이터를 바탕으로 조정하는 것이 좋습니다.

kubectl top pods 명령어로 실제 사용량을 확인할 수 있습니다. Prometheus와 Grafana를 연동하면 더 상세한 히스토리를 볼 수 있습니다.

**Vertical Pod Autoscaler(VPA)**를 사용하면 실제 사용량 기반으로 자동 권장 값을 받을 수도 있습니다. 주의할 점이 있습니다.

requests를 너무 낮게 설정하면 노드가 과도하게 오버커밋되어 성능 저하나 OOMKilled가 빈번해집니다. 반대로 너무 높게 설정하면 클러스터 리소스가 낭비됩니다.

실제 사용량의 1.21.5배 정도를 requests로, 23배를 limits로 설정하는 것이 일반적인 시작점입니다. 김개발 씨는 리소스 설정을 현실적으로 조정했습니다.

모니터링으로 확인한 실제 사용량을 기반으로 requests를 설정하니, 파드가 원활하게 배치되었습니다. "리소스 설정이 이렇게 중요한 줄 몰랐어요.

스케줄링의 시작점이 여기였군요!"

실전 팁

💡 - kubectl describe node로 노드의 할당 가능한 리소스와 현재 requests 합계를 확인하세요

  • QoS 클래스(Guaranteed, Burstable, BestEffort)는 requests와 limits 설정에 따라 결정됩니다
  • 메모리 limits는 반드시 설정하고, CPU limits는 상황에 따라 생략할 수도 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Kubernetes#NodeSelector#Affinity#Taint#Toleration#Scheduling#Kubernetes,Scheduling

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.