관리 메뉴

근묵자흑

쿠버네티스 애플리케이션 배포를 위한 고급 설정: 자원 관리, 스케줄링, 그리고 배포 전략 본문

k8s

쿠버네티스 애플리케이션 배포를 위한 고급 설정: 자원 관리, 스케줄링, 그리고 배포 전략

Luuuuu 2025. 6. 1. 16:32

1. 포드의 자원 사용량 제한

쿠버네티스 클러스터에서 안정적인 서비스를 제공하려면 각 파드가 사용하는 자원을 적절히 관리해야 합니다. 이는 노드의 자원이 고갈되는 것을 방지하고, 애플리케이션 간의 공정한 자원 분배를 보장합니다.

1.1 컨테이너와 파드의 자원 사용량 제한: Limits

Limits는 컨테이너가 사용할 수 있는 최대 자원량을 정의합니다. CPU와 메모리에 대해 각각 설정할 수 있으며, 이를 초과하면 시스템이 개입합니다.

apiVersion: v1
kind: Pod
metadata:
  name: resource-limited-pod
spec:
  containers:
  - name: app
    image: nginx
    resources:
      limits:
        memory: "256Mi"  # 메모리 상한선: 256 메가바이트
        cpu: "500m"      # CPU 상한선: 0.5 코어 (500 밀리코어)

메모리 limit을 초과하면 컨테이너는 OOMKilled(Out Of Memory Killed) 상태가 되어 재시작됩니다. 반면 CPU limit은 다르게 동작합니다. CPU는 압축 가능한(compressible) 자원이므로, limit을 초과하려 하면 throttling이 발생하여 성능이 저하될 뿐 컨테이너가 종료되지는 않습니다.

1.2 컨테이너와 파드의 사용량 제한하기: Requests

Requests는 컨테이너가 필요로 하는 최소 자원량을 나타냅니다. 이는 스케줄러가 파드를 배치할 노드를 선택할 때 중요한 기준이 됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: resource-requested-pod
spec:
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        memory: "128Mi"  # 최소 보장 메모리
        cpu: "250m"      # 최소 보장 CPU
      limits:
        memory: "256Mi"
        cpu: "500m"

스케줄러는 노드의 allocatable 자원(전체 자원에서 시스템 예약분을 뺀 값)과 이미 배치된 파드들의 requests 합계를 비교하여, 새 파드를 수용할 수 있는 노드를 찾습니다.

1.3 CPU 자원 사용량의 제한 원리

CPU 제한은 Linux의 CFS(Completely Fair Scheduler)와 cgroups를 통해 구현됩니다. 쿠버네티스는 다음과 같은 방식으로 CPU를 제어합니다:

예를 들어, 500m (0.5 코어) CPU limit은 100ms 주기 동안 50ms만 CPU를 사용할 수 있다는 의미입니다. 이 할당량을 모두 사용하면 나머지 주기 동안은 throttling됩니다.

1.4 QoS 클래스와 메모리 자원 사용량 제한 원리

쿠버네티스는 파드의 자원 설정에 따라 자동으로 QoS(Quality of Service) 클래스를 할당합니다. 이는 노드의 자원이 부족할 때 어떤 파드를 먼저 제거할지 결정하는 기준이 됩니다.

Guaranteed 클래스: 모든 컨테이너에 대해 requests와 limits가 설정되어 있고, 두 값이 동일한 경우입니다. 이 파드들은 가장 높은 우선순위를 가지며, 자원 부족 시 마지막에 제거됩니다.

Burstable 클래스: requests나 limits 중 하나 이상이 설정되어 있지만 Guaranteed 조건을 만족하지 않는 경우입니다. 중간 우선순위를 가집니다.

BestEffort 클래스: requests와 limits가 모두 설정되지 않은 경우입니다. 가장 낮은 우선순위를 가지며, 자원 부족 시 가장 먼저 제거됩니다.

1.5 ResourceQuota와 LimitRange

네임스페이스 수준에서 자원 사용을 제한하려면 ResourceQuota와 LimitRange를 사용합니다.

ResourceQuota는 네임스페이스에서 사용할 수 있는 총 자원량을 제한합니다:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: production
spec:
  hard:
    requests.cpu: "100"      # 총 CPU requests 합계 제한
    requests.memory: "200Gi" # 총 메모리 requests 합계 제한
    limits.cpu: "200"
    limits.memory: "400Gi"
    persistentvolumeclaims: "10"
    pods: "50"

LimitRange는 개별 객체(파드, 컨테이너 등)의 자원 사용량 범위를 제한합니다:

apiVersion: v1
kind: LimitRange
metadata:
  name: mem-limit-range
  namespace: production
spec:
  limits:
  - default:        # 기본 limits
      memory: "512Mi"
      cpu: "1"
    defaultRequest: # 기본 requests
      memory: "256Mi"
      cpu: "0.5"
    max:           # 최대 허용값
      memory: "1Gi"
      cpu: "2"
    min:           # 최소 요구값
      memory: "128Mi"
      cpu: "100m"
    type: Container

1.6 Admission Controller

Admission Controller는 API 요청이 etcd에 저장되기 전에 검증하고 변경할 수 있는 플러그인입니다. 자원 관리와 관련된 주요 Admission Controller들은 다음과 같습니다:

ResourceQuota Admission Controller: ResourceQuota 정책을 강제합니다. 새로운 파드 생성 요청이 들어오면, 현재 사용량과 요청된 자원을 합산하여 quota를 초과하는지 검사합니다.

LimitRanger Admission Controller: LimitRange 정책을 적용합니다. 자원 요구사항이 명시되지 않은 파드에 기본값을 설정하고, 최대/최소 제한을 검증합니다.

2. 쿠버네티스 스케줄링

스케줄링은 파드를 적절한 노드에 배치하는 과정입니다. 이는 클러스터의 자원을 효율적으로 사용하고 애플리케이션의 요구사항을 만족시키는 데 핵심적인 역할을 합니다.

2.1 파드가 실제로 노드에 생성되기까지의 과정

파드 생성 요청부터 실제 컨테이너가 실행되기까지의 전체 과정을 살펴보겠습니다:

  1. API 서버 수신: 사용자가 kubectl이나 API를 통해 파드 생성을 요청하면, API 서버가 이를 검증하고 etcd에 저장합니다. 이 시점에서 파드의 상태는 Pending이며, nodeName 필드는 비어있습니다.
  2. 스케줄러 감지: 스케줄러는 Informer를 통해 nodeName이 없는 새 파드를 감지합니다. 스케줄링 큐에 파드를 추가하고 처리를 시작합니다.
  3. 노드 선택: 스케줄러는 필터링과 스코어링 과정을 거쳐 최적의 노드를 선택합니다. 선택이 완료되면 파드의 nodeName을 업데이트합니다.
  4. Kubelet 감지: 해당 노드의 kubelet이 자신에게 할당된 새 파드를 감지합니다.
  5. 컨테이너 런타임 실행: Kubelet은 CRI(Container Runtime Interface)를 통해 컨테이너를 생성하고 시작합니다.

2.2 파드가 생성될 노드를 선택하는 스케줄링 과정

스케줄링 알고리즘은 크게 두 단계로 구성됩니다:

필터링(Filtering) 단계: 파드를 실행할 수 없는 노드들을 제외합니다. 이 단계에서는 다음과 같은 조건들을 검사합니다:

  • 노드의 자원이 충분한가? (PodFitsResources)
  • 노드의 포트가 충돌하지 않는가? (PodFitsHostPorts)
  • 노드 셀렉터가 일치하는가? (PodMatchNodeSelector)
  • 파드가 요구하는 볼륨을 마운트할 수 있는가? (CheckVolumeBinding)
  • 노드의 taint를 tolerate할 수 있는가? (PodToleratesNodeTaints)

스코어링(Scoring) 단계: 필터링을 통과한 노드들에 점수를 매겨 최적의 노드를 선택합니다. 주요 스코어링 플러그인들은 다음과 같습니다:

  • NodeResourcesFit: 자원 사용률을 균등하게 분산
  • InterPodAffinity: 파드 간 친화성/반친화성 규칙 적용
  • NodeAffinity: 노드 친화성 규칙 적용
  • TaintToleration: Taint가 적은 노드 선호

2.3 NodeSelector와 Node Affinity, Pod Affinity

파드 배치를 세밀하게 제어하기 위한 다양한 메커니즘이 있습니다.

NodeSelector는 가장 간단한 형태의 노드 선택 방법입니다:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  nodeSelector:
    gpu: "true"           # gpu=true 레이블이 있는 노드에만 배치
    disktype: "ssd"       # AND 조건으로 동작
  containers:
  - name: cuda-container
    image: nvidia/cuda

Node Affinity는 더 유연한 노드 선택을 가능하게 합니다:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:  # 필수 조건
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution: # 선호 조건
      - weight: 1
        preference:
          matchExpressions:
          - key: node-type
            operator: In
            values:
            - gpu-node

Pod Affinity/Anti-Affinity는 다른 파드와의 관계를 기반으로 배치를 제어합니다:

apiVersion: v1
kind: Pod
metadata:
  name: web-server
spec:
  affinity:
    podAffinity:  # 특정 파드와 같은 노드에 배치
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - cache
        topologyKey: kubernetes.io/hostname
    podAntiAffinity:  # 특정 파드와 다른 노드에 배치
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - web-server
          topologyKey: kubernetes.io/hostname

이러한 affinity 규칙들은 스케줄러의 필터링 및 스코어링 단계에서 평가됩니다. 특히 Pod Anti-Affinity는 고가용성을 위해 동일한 애플리케이션의 복제본을 서로 다른 노드나 가용 영역에 분산시키는 데 유용합니다.

3. 쿠버네티스 애플리케이션 상태와 배포

애플리케이션의 안정적인 배포와 운영을 위해서는 파드의 생애주기를 이해하고, 적절한 상태 검사와 배포 전략을 구성해야 합니다.

3.1 디플로이먼트를 통한 롤링 업데이트

디플로이먼트는 파드의 선언적 업데이트를 제공하는 고수준 API 객체입니다. 롤링 업데이트는 서비스 중단 없이 애플리케이션을 업데이트하는 핵심 전략입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2        # 업데이트 중 추가로 생성할 수 있는 파드 수
      maxUnavailable: 1  # 업데이트 중 사용 불가능한 파드 수
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.20  # 이 버전을 변경하면 롤링 업데이트 시작

롤링 업데이트의 내부 동작 과정은 다음과 같습니다:

  1. 새 ReplicaSet 생성: 디플로이먼트 컨트롤러는 새로운 파드 템플릿으로 ReplicaSet을 생성합니다.
  2. 점진적 교체: maxSurge와 maxUnavailable 설정에 따라 새 파드를 생성하고 기존 파드를 제거합니다.
  3. 상태 확인: 각 단계에서 새 파드가 Ready 상태가 될 때까지 기다립니다.
  4. 롤백 가능성: 문제가 발생하면 이전 ReplicaSet으로 빠르게 롤백할 수 있습니다.

3.2 파드의 생애주기

파드는 생성부터 종료까지 여러 단계를 거칩니다. 각 단계를 이해하면 애플리케이션의 동작을 더 잘 제어할 수 있습니다.

Pending: 파드가 생성되었지만 아직 모든 컨테이너가 시작되지 않은 상태입니다. 이미지 다운로드, 스케줄링 대기 등의 이유로 이 상태가 됩니다.

Running: 모든 컨테이너가 생성되고 적어도 하나의 컨테이너가 실행 중인 상태입니다.

Succeeded: 모든 컨테이너가 성공적으로 종료되고 재시작되지 않을 상태입니다. 주로 Job에서 볼 수 있습니다.

Failed: 모든 컨테이너가 종료되었고, 적어도 하나의 컨테이너가 실패로 종료된 상태입니다.

Unknown: 파드의 상태를 확인할 수 없는 경우입니다. 보통 노드와의 통신 문제로 발생합니다.

3.3 Running 상태가 되기 위한 조건 - PostStart

컨테이너가 시작된 직후 실행되는 PostStart 훅을 통해 초기화 작업을 수행할 수 있습니다:

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: 
          - /bin/sh
          - -c
          - |
            # 애플리케이션 초기화 작업
            echo "Container started at $(date)" > /var/log/startup.log
            # 설정 파일 준비
            cp /config-template/nginx.conf /etc/nginx/nginx.conf
            # 워밍업 요청
            curl -s http://localhost/health || true

PostStart 훅은 컨테이너의 메인 프로세스와 병렬로 실행됩니다. 따라서 훅이 완료되기 전에 메인 프로세스가 시작될 수 있다는 점을 고려해야 합니다. 만약 PostStart 훅이 실패하면 컨테이너는 재시작됩니다.

3.4 애플리케이션 상태 검사 - LivenessProbe, ReadinessProbe

쿠버네티스는 두 가지 주요 헬스 체크 메커니즘을 제공합니다:

LivenessProbe는 컨테이너가 살아있는지 확인합니다. 실패하면 컨테이너를 재시작합니다:

apiVersion: v1
kind: Pod
metadata:
  name: liveness-example
spec:
  containers:
  - name: app
    image: myapp
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30  # 컨테이너 시작 후 첫 검사까지 대기 시간
      periodSeconds: 10        # 검사 주기
      timeoutSeconds: 5        # 타임아웃
      failureThreshold: 3      # 연속 실패 횟수

ReadinessProbe는 컨테이너가 트래픽을 받을 준비가 되었는지 확인합니다. 실패하면 서비스의 엔드포인트에서 제외됩니다:

apiVersion: v1
kind: Pod
metadata:
  name: readiness-example
spec:
  containers:
  - name: app
    image: myapp
    readinessProbe:
      exec:
        command:
        - /bin/sh
        - -c
        - |
          # 데이터베이스 연결 확인
          pg_isready -h $DB_HOST -p $DB_PORT
      initialDelaySeconds: 5
      periodSeconds: 5

헬스 체크는 세 가지 방식으로 수행할 수 있습니다:

  • HTTP GET: 지정된 경로에 HTTP 요청을 보내고 200-399 응답 코드를 확인
  • TCP Socket: 지정된 포트에 TCP 연결 시도
  • Exec: 컨테이너 내부에서 명령 실행하고 종료 코드 확인

3.5 Terminating 상태와 애플리케이션의 종료

그레이스풀 셧다운은 애플리케이션이 진행 중인 작업을 완료하고 깨끗하게 종료할 수 있도록 합니다. 파드 종료 과정은 다음과 같습니다:

  1. 종료 시작: 파드가 삭제되면 Terminating 상태가 되고, 새로운 요청 수신을 중단합니다.
  2. PreStop 훅 실행: 설정되어 있다면 PreStop 훅이 실행됩니다.
  3. SIGTERM 전송: 모든 컨테이너에 SIGTERM 신호가 전송됩니다.
  4. 유예 기간: terminationGracePeriodSeconds(기본 30초) 동안 대기합니다.
  5. 강제 종료: 유예 기간이 지나도 종료되지 않으면 SIGKILL로 강제 종료됩니다.
apiVersion: v1
kind: Pod
metadata:
  name: graceful-shutdown-example
spec:
  terminationGracePeriodSeconds: 60  # 60초 유예 기간
  containers:
  - name: app
    image: myapp
    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sh
          - -c
          - |
            # 새로운 연결 수신 중단
            echo "Stopping accepting new connections..."
            touch /tmp/shutdown
            
            # 진행 중인 요청 완료 대기
            while [ $(curl -s http://localhost:8080/active-connections) -gt 0 ]; do
              echo "Waiting for active connections to complete..."
              sleep 2
            done
            
            # 정리 작업 수행
            echo "Performing cleanup..."
            /app/cleanup.sh

애플리케이션은 SIGTERM 신호를 적절히 처리하도록 구현되어야 합니다: