시작하며

쿠버네티스 클러스터에서 여러 팀이 애플리케이션을 배포하다 보면 자원 관리와 포드 배치 전략이 중요해진다. 스케일 아웃만큼 중요한 것이 컴퓨팅 자원 활용률(Utilization)을 높이는 것이다. 이를 위해 쿠버네티스는 Limits/Requests, QoS 클래스, ResourceQuota, LimitRange, 그리고 다양한 스케줄링 옵션을 제공한다.

자원 할당과 제한

Pod 자원 사용량 제한 — Limits and Requests

spec.containers.resources.limits는 해당 포드 컨테이너가 최대로 사용할 수 있는 자원의 상한선이다.

Requests는 적어도 이 정도의 자원은 컨테이너에 보장되어야 한다는 것을 의미한다. 자원의 Overcommit을 가능하게 하며, 포드를 할당할 때 사용하는 기준은 최솟값인 requests이다.

  • requests: 최소 요구량
  • limits: 최대 한계치

CPU Requests and Limits

쿠버네티스에서는 CPU를 m(밀리코어) 단위로 제한한다. --cpu-shares(Request)는 할당 비율에 따라 자원 할당이 결정된다. Requests 범위를 넘어 자원을 사용하면 CPU throttle이 발생하지만, 컨테이너 자체에는 큰 문제가 발생하지 않는다.

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

메모리는 CPU와 다르게 압축 불가능한 자원이다. 메모리가 부족한 경우(MemoryPressure = true):

  1. 우선순위가 낮은 포드는 다른 노드로 퇴거(Evict)되거나 프로세스가 강제 종료된다.
  2. 급작스럽게 메모리 사용량이 많아지면 OOM(Out Of Memory) 점수가 높은 순서대로 프로세스가 종료된다.

쿠버네티스는 어떤 포드를 먼저 종료할지 결정하기 위해 세 가지 QoS 클래스를 사용한다.

1) Guaranteed class
   - 포드의 컨테이너에 설정된 Limits와 Requests 값이 완전히 동일할 때 부여되는 클래스
2) BestEffort
   - Requests와 Limits을 아예 설정하지 않은 포드에 설정되는 클래스
   - 유휴 자원이 있다면 제한 없이 모든 자원을 사용할 수 있다
3) Burstable
   - Guaranteed나 BestEffort에 속하지 않는 모든 포드
   - Requests 내에서 자원을 사용하면 문제 없으나, Limits를 사용하려 하면 다른 포드와 자원 경합 발생

기본 우선순위: Guaranteed > Burstable > BestEffort

OOM Killer에 의해 컨테이너 프로세스가 종료되면 포드의 재시작 정책(restartPolicy)에 의해 다시 시작된다.

Tip

오버커밋이 반드시 좋은 것은 아니다. 애플리케이션의 안정성을 위해 모든 포드의 Limits와 Requests를 동일하게 설정해 Guaranteed 클래스로 보장받는 것도 좋은 전략이다.

ResourceQuota와 LimitRange

여러 개발팀이 k8s에서 개발·테스트를 진행하기 위한 방법들:

1) k8s 클러스터를 팀마다 하나씩 제공 → 관리가 어렵고 비용이 높다
2) 네임스페이스를 팀마다 생성한 뒤 RBAC으로 접근 제한 → 다른 네임스페이스에서 자원이 부족해질 수 있다
3) ResourceQuota와 LimitRange를 사용 → 권장

ResourceQuota

특정 네임스페이스에서 사용할 수 있는 자원 사용량의 합을 제한하는 쿠버네티스 오브젝트.

  • 네임스페이스에서 할당할 수 있는 자원(CPU, 메모리, PVC 크기, 컨테이너 내부 스토리지)의 합을 제한
  • 네임스페이스에서 생성할 수 있는 리소스(디플로이먼트, 포드, 시크릿, 컨피그맵, PVC, NodePort/LoadBalancer 서비스, BestEffort 포드)의 개수를 제한

LimitRange

특정 네임스페이스에서 할당되는 자원의 범위 또는 기본값을 지정할 수 있는 쿠버네티스 오브젝트.

  • 포드 컨테이너에 CPU나 메모리 할당량이 설정되어 있지 않은 경우, 자동으로 기본 Requests(:defaultRequest) 또는 Limits(:default) 값을 설정
  • 포드 또는 컨테이너의 CPU, Memory, PVC의 최대/최소값 설정

Admission Controller

ResourceQuota와 LimitRange는 Admission Controller에 의해 조작된다.

step 1) 사용자가 kubectl 명령어로 API 서버에 요청을 보낸다.
step 2) x509 인증서, serviceAccount 등을 통해 인증 단계를 거친다.
step 3) role, cluster-role 등을 통해 인가 단계를 거친다.
step 4) ResourceQuota(Admission Controller)가 해당 자원 할당이 적절한지 검증(Validating)한다.
step 5) 포드 데이터에 자원 할당이 설정되지 않은 경우, LimitRange(Admission Controller)가
        포드 데이터에 자원 할당 기본값을 추가하고 API 요청 데이터를 변형한다.

Kubernetes 스케줄링

스케줄링이란 인스턴스를 새롭게 생성할 때, 특정 목적에 따라 가장 합리적으로 어느 서버에 생성할 것인지 결정하는 일이다.

스케줄링 과정

step 1) 사용자가 kubectl로 API 서버에 포드 생성 요청을 보낸다.
step 2) ServiceAccount, RoleBinding 등을 이용해 인증·인가 작업을 수행한다.
step 3) Admission Controller가 해당 요청을 적절히 변형/검증한다.
step 4) 승인 후 kube-scheduler가 적절한 워커 노드를 선택한다.
        - etcd(분산 코디네이터): 클러스터 전반적인 상태 데이터를 저장. nodeName이 설정되지 않은 상태로 저장됨
        - kube-scheduler: nodeName이 할당되지 않은 포드를 감지하고 적절한 노드를 선택해 바인딩 요청

스케줄러는 두 단계로 적절한 노드를 선택한다.

  • 노드 필터링: 포드를 할당할 수 있는 노드와 그렇지 않은 노드를 분리하는 과정
  • 노드 스코어링: 도커 이미지 존재 여부, 가용 자원 용량 등을 자체 알고리즘으로 점수화

Schedule: nodeName, nodeSelector

가장 확실한 방법은 특정 워커 노드의 이름(nodeName)을 포드에 명시하는 것이다. 하지만 노드 이름이 고정되므로 다른 환경에서 YAML 파일을 보편적으로 사용하기 어렵다. 대안으로 노드의 라벨(Label)을 사용하는 nodeSelector를 선언하는 방법이 있다.

기본 라벨: 노드 OS, CPU 아키텍처, Hostname 등

Schedule: Node Affinity

nodeSelector에서 발전된 방법으로, 두 가지 조건을 설정할 수 있다.

  • requiredDuringSchedulingIgnoredDuringExecution (Hard): 반드시 만족해야 하는 조건. operator 옵션으로 In, NotIn, Gt, Lt 등이 있다.
  • preferredDuringSchedulingIgnoredDuringExecution (Soft): 선호하는 조건. 조건을 만족하는 노드가 있다면 우선 배치하지만 필수는 아니다.

포드가 할당될 때만 유효하며, 추후 라벨이 변경되어도 Eviction은 발생하지 않는다.

Schedule: Pod Affinity

특정 조건을 만족하는 포드와 함께 실행되도록 스케줄링하는 것이다. 토폴로지(topologyKey)에 속한 포드 중 조건에 가장 부합하는 하나를 선택하므로, 응답 시간을 줄이기 위해 동일한 가용 영역에 배포해야 하는 경우에 적합하다.

Schedule: Pod Anti-affinity

Pod Affinity와 반대로 동작한다. matchExpression 조건을 만족하는 포드와 최대한 멀리 떨어진 토폴로지에 포드를 배치한다. 고가용성을 보장하기 위해 포드를 여러 가용 영역에 분산할 수 있는 전략이다. topologyKey를 hostname으로 설정하면 하나의 노드에 두 개의 포드가 스케줄링되지 않는다(DaemonSet과 유사하게 동작).

Taints, Tolerations

특정 노드에 얼룩(Taint)을 남겨 해당 노드에 포드가 할당되는 것을 막는다. 단, 이를 용인(Toleration)할 수 있는 포드만 해당 노드에 할당된다. 예를 들어 master 노드에 taint를 남겨 worker 노드에만 포드가 할당되도록 하는 방법이 있다.

taint의 effect:
  - NoSchedule: 포드를 스케줄링하지 않음
  - NoExecute: 포드의 실행 자체를 허용하지 않음
  - PreferNoSchedule: 가능하면 스케줄링하지 않음

NoSchedule, NoExecute, tolerationSeconds

  • NoSchedule: 노드에 설정해도 기존에 실행 중이던 포드는 정상적으로 동작한다.
  • NoExecute: 스케줄링도 하지 않을 뿐더러, toleration이 없는 포드를 해당 노드에서 종료시킨다.

쿠버네티스는 문제가 발생한 노드에 자동으로 Taint를 추가한다.

- node.kubernetes.io/not-ready:NoExecute
- node.kubernetes.io/unreachable:NoExecute

tolerationSeconds를 설정하면 해당 시간 동안은 Taint를 용인해 강제 처리를 막을 수 있다.

Cordon, Drain, PodDisruptionBudget

  • Cordon: 지정된 노드에 새로운 포드가 할당되지 않도록 한다.
  • Drain: 노드에 스케줄링을 금지하는 것은 물론, 기존에 실행 중이던 포드를 다른 노드로 Eviction한다.
  • PodDisruptionBudget(PDB): drain 시 포드가 종료되어 다른 노드로 옮겨가는 사이에 어플리케이션이 중단될 수 있다. 특정 개수 또는 비율의 포드가 정상 상태를 유지하도록 보장한다.
    • maxUnavailable: 최대 몇 개까지 동시에 종료될 수 있는가?
    • minAvailable: 최소 몇 개가 정상 상태를 유지해야 하는가?

Custom Scheduler

1) API 서버의 Watch API를 통해 새롭게 생성된 포드의 데이터를 받아온다.
2) 포드의 데이터 중에서 nodeName이 설정되어 있지 않고, schedulerName이 일치하는지 검사한다.
3) 스케줄링 알고리즘을 수행하고, 바인딩 API 요청을 통해 스케줄링된 노드의 이름을 nodeName에 설정한다.

정리하며

쿠버네티스 배포 옵션은 자원 효율성과 애플리케이션 안정성 두 가지를 모두 고려해야 한다. Limits/Requests와 QoS 클래스를 통해 자원 경합 시 우선순위를 명확히 하고, ResourceQuota와 LimitRange로 네임스페이스 단위 자원을 통제한다. 스케줄링 전략(Affinity, Anti-affinity, Taints/Tolerations)을 조합하면 고가용성과 지역성을 동시에 만족하는 배포 구성이 가능하다.