쿠버네티스 리소스 요청(Requests)과 제한(Limits) 마스터하기: 최고 성능을 위한 가이드

쿠버네티스 CPU 및 메모리 리소스 요청(Requests)과 제한(Limits)의 중요한 차이점을 알아보세요. 이 가이드는 이러한 설정이 서비스 품질(QoS) 클래스(Guaranteed, Burstable, BestEffort)를 결정하고, 노드 불안정을 방지하며, 클러스터 스케줄링 효율성을 최적화하는 방법을 설명합니다. 실용적인 YAML 예제와 성능 튜닝을 위한 모범 사례를 포함합니다.

쿠버네티스 리소스 요청(Requests)과 제한(Limits) 마스터하기: 최고 성능을 위한 가이드

쿠버네티스 리소스 요청과 제한은 YAML에서 단순해 보이지만, 클러스터의 거의 모든 운영 동작을 결정합니다: 파드가 배치되는 위치, 어떤 워크로드가 먼저 축출되는지, 앱이 부하 상태에서 CPU 제한(throttling)을 받는지, 그리고 사용 가능한 여유 용량이 얼마나 되는지 등입니다. 잘못된 설정은 정상적인 애플리케이션을 고장난 것처럼 보이게 만들 수 있습니다. 설정이 누락되면 스케줄러가 첫 번째 트래픽 급증이 잡음이웃(noisy-neighbor) 사고로 이어질 때까지 파드를 노드에 채워 넣을 수 있습니다.

팀을 당황하게 만드는 부분은 요청과 제한이 서로 다른 시스템에서 사용된다는 점입니다. 요청은 주로 스케줄링 약속입니다. 제한은 강제 경계입니다. 이 둘을 동일하게 취급하면, 특히 CPU에서 예상치 못한 결과가 발생합니다.

핵심 개념 이해: 요청(Requests) vs. 제한(Limits)

쿠버네티스에서 컨테이너는 resources.requestsresources.limits를 사용하여 예상 리소스 소비량을 정의할 수 있습니다. 클러스터가 LimitRange나 승인 제어(admission controls)와 같은 정책을 사용하지 않는 한 기술적으로 필수는 아니지만, 프로덕션 워크로드는 일반적으로 CPU와 메모리에 대한 요청을 최소한 정의해야 합니다. 요청이 없으면 스케줄러는 유용한 정보를 거의 갖지 못하며 파드는 더 낮은 서비스 품질 상태에 놓이게 됩니다.

1. 리소스 요청 (requests)

요청은 컨테이너가 스케줄링 시 보장받는 리소스의 양을 나타냅니다. 이것은 kube-스케줄러가 파드를 배치할 노드를 결정할 때 사용하는 최소 리소스 양입니다.

  • 스케줄링: 새 파드를 스케줄링하려면 노드에 모든 파드 요청의 합계를 충족할 수 있는 사용 가능한 할당 가능 리소스가 충분히 있어야 합니다.
  • 런타임 우선순위: 요청은 CPU 점유율(CPU shares)과 메모리 축출 결정에 영향을 줍니다. 모든 속도 저하를 방지하는 마법 같은 예약은 아니지만, 경합 상황에서 쿠버네티스와 커널에 더 나은 정보를 제공합니다.

2. 리소스 제한 (limits)

제한은 컨테이너가 소비할 수 있는 리소스의 최대 양을 정의합니다. 이 제한을 초과하면 CPU와 메모리에 대해 각각 정의된 특정 동작이 발생합니다.

  • CPU 제한: 컨테이너가 제한보다 더 많은 CPU를 사용하려고 하면 Linux 커널의 cgroups가 사용을 **제한(throttle)**하여 추가 사이클을 소비하지 못하도록 합니다.
  • 메모리 제한: 컨테이너가 메모리 제한을 초과하면 커널이 컨테이너의 프로세스를 종료할 수 있습니다. 쿠버네티스는 이것이 기록된 종료 이유일 때 OOMKilled로 보고합니다.

CPU vs. 메모리 동작

쿠버네티스가 CPU와 메모리 경계를 어떻게 강제하는지에 대한 질적 차이를 이해하는 것이 중요합니다:

리소스 제한 초과 시 동작 강제 메커니즘
CPU 제한됨 (속도 저하) cgroups (CPU 대역폭 제어)
메모리 종료됨 (OOMKill) 커널 OOM Killer

실용적 주의사항: CPU 제한은 노드를 실행 중인 프로세스로부터 보호할 수 있지만, 너무 낮게 설정하면 지연 시간 문제를 일으킬 수 있습니다. 많은 플랫폼 팀은 메모리 제한을 일관되게 설정하고, 지연 시간에 민감한 서비스의 경우 위험 허용 수준과 클러스터 정책에 따라 CPU 제한을 더 선택적으로 사용합니다.

파드 스펙에서 리소스 정의하기

리소스는 spec.containers[*].resources 블록 내에 정의됩니다. 수량은 표준 쿠버네티스 접미사(예: CPU의 경우 m (milli-CPU), 메모리의 경우 Mi (Mebibytes))를 사용하여 지정됩니다.

CPU 단위 정의

  • 1 CPU 단위는 1개의 전체 코어(또는 클라우드 제공업체의 vCPU)와 같습니다.
  • 1000m (millicores)은 1 CPU 단위와 같습니다.

메모리 단위 정의

  • Mi (Mebibytes) 또는 Gi (Gibibytes)가 일반적입니다.
  • 1024Mi = 1Gi.

예제 YAML 구성

최소 500m CPU와 256Mi 메모리를 보장받아야 하지만 1 CPU와 512Mi를 절대 초과하지 않아야 하는 컨테이너를 고려해보세요:

resources:
  requests:
    memory: "256Mi"
    cpu: "500m"
  limits:
    memory: "512Mi"
    cpu: "1"

숫자는 추측이 아닌 관찰된 동작에서 나와야 합니다. 작은 HTTP API의 경우, 초기 요청은 업무 시간 동안의 정상적인 p50 또는 p90 사용량을 기반으로 하고 부하 테스트 후 조정될 수 있습니다. JVM 서비스의 경우 메모리는 힙, 메타스페이스, 네이티브 메모리, 스레드 스택, 다이렉트 버퍼, 사이드카 오버헤드를 포함해야 합니다. 배치 작업의 경우 평균 메모리보다 가장 큰 입력 시의 최대 메모리가 더 중요할 수 있습니다.

서비스 품질 (QoS) 클래스

요청과 제한 간의 관계는 파드에 할당되는 서비스 품질 (QoS) 클래스를 결정합니다. 이 클래스는 리소스가 부족해지고 노드가 메모리를 회수(축출)해야 할 때 파드의 우선순위를 결정합니다.

쿠버네티스는 세 가지 QoS 클래스를 정의합니다:

1. Guaranteed

정의: 파드의 모든 컨테이너는 CPU와 메모리 모두에 대해 동일하고 0이 아닌 요청과 제한을 가져야 합니다.

  • 이점: 이러한 파드는 리소스 압박 시 가장 마지막에 축출되어 최대 안정성을 보장합니다.
  • 사용 사례: 엄격한 성능 격리가 필요한 중요 시스템 구성 요소 또는 데이터베이스.

2. Burstable

정의: 파드의 최소 하나의 컨테이너가 요청을 정의했지만, 요청과 제한이 모든 컨테이너에 대해 동일하지 않거나, 일부 리소스에 제한이 없는 경우입니다(제한 설정은 매우 권장됨).

  • 이점: 컨테이너가 정의된 제한까지 노드의 사용되지 않은 용량을 활용하여 요청 이상으로 버스트할 수 있습니다.
  • 축출 우선순위: BestEffort 파드 다음, Guaranteed 파드 전에 축출됩니다.
  • 사용 사례: 지연 시간의 약간의 변동이 허용되는 대부분의 표준 무상태 애플리케이션.

3. BestEffort

정의: 파드에 어떤 컨테이너에 대한 요청이나 제한이 없습니다.

  • 이점: 단순함 외에는 없습니다.
  • 위험: 이러한 파드는 노드에 메모리 압박이 있을 때 첫 번째 축출 대상입니다. 또한 요청이 선언되지 않았기 때문에 CPU 경쟁에서 불리할 수 있습니다.
  • 사용 사례: 쉽게 재시작할 수 있는 중요하지 않은 배치 작업 또는 로깅 에이전트.

실용적인 최적화 전략

효과적인 리소스 관리는 측정, 반복 및 신중한 계획이 필요합니다.

전략 1: 요청을 정확하게 측정하고 설정하기

요청은 애플리케이션이 대부분의 시간 동안 수용 가능하게 실행되는 데 필요한 리소스 양을 반영해야 합니다. 요청을 너무 높게 설정하면 스케줄러가 해당 용량을 이미 사용된 것으로 간주하기 때문에 클러스터 용량을 낭비하게 됩니다. 너무 낮게 설정하면 스케줄러가 노드에 너무 많은 파드를 배치할 수 있으며, 경합 시 워크로드가 더 큰 어려움을 겪을 수 있습니다.

Prometheus 및 Grafana와 같은 모니터링 도구를 사용하여 요청 값을 실제 사용량과 비교하십시오. 일반적인 시작점은 며칠간의 정상 트래픽을 살펴보고 명백한 일회성 사고를 무시하며, 가장 높은 단일 급증이 아닌 지속적인 백분위수 근처에 요청을 설정하는 것입니다. 정확한 백분위수는 정책 선택 사항입니다. 중요한 점은 데이터를 사용하고 이를 재검토하는 것입니다.

예를 들어, 서비스가 일반적으로 180m CPU를 사용하고, 배포 준비 중에 약 450m까지 치솟으며, 알려진 배치 작업 중에 900m까지 드물게 급증하는 경우, 900m 요청을 설정하면 하루 종일 용량을 낭비할 수 있습니다. 50m 요청을 설정하면 파드 스케줄링은 저렴해지지만 경합 시 불안정해집니다. 일반적인 지속 범위의 요청과 배치 경로에 대한 별도 처리가 종종 더 나은 대안입니다.

전략 2: 보수적인 제한 정의하기

제한은 안전 경계 역할을 하지만 공짜는 아닙니다. 메모리의 경우, 측정된 최대 사용량보다 약간 높은 제한은 하나의 컨테이너가 노드를 모두 소비하는 것을 방지할 수 있습니다. CPU의 경우, 제한은 실행 중인 프로세스가 무제한 CPU를 사용하는 것을 방지하지만, 공격적인 제한은 노드에 유휴 코어가 있음에도 서비스를 제한할 수 있습니다.

CPU 제한에 대한 경고: 실제 수요보다 낮은 CPU 제한을 설정하면 제한으로 인해 눈에 띄는 지연 시간이 발생할 수 있습니다. Burstable QoS는 많은 무상태 서비스에 적합한 반면, Guaranteed QoS는 격리 트레이드오프가 의도적인 워크로드에 더 적합합니다.

전략 3: 수직 파드 오토스케일러 (VPA) 활용하기

리소스를 수동으로 조정하는 것은 어렵고 시간이 많이 소요됩니다. **수직 파드 오토스케일러 (VPA)**는 런타임 사용량을 모니터링하고 모드에 따라 리소스 요청을 추천하거나 업데이트할 수 있습니다. 많은 설정에서 VPA는 먼저 추천 모드로 사용되어 팀이 자동 업데이트를 허용하기 전에 제안된 요청을 검토할 수 있도록 합니다.

VPA를 수평 파드 오토스케일러(HPA)와 결합할 때는 주의하십시오. HPA는 종종 요청 대비 사용률을 기반으로 확장되므로, 요청을 변경하면 확장 동작이 변경될 수 있습니다. 잘 작동할 수 있지만 의도적으로 테스트해야 합니다.

전략 4: 네임스페이스에 대한 리소스 할당량 (Resource Quotas)

팀 또는 환경 간의 리소스 독점을 방지하기 위해 관리자는 네임스페이스 수준에서 **리소스 할당량 (Resource Quotas)**을 사용해야 합니다. ResourceQuota는 해당 네임스페이스 내에 존재할 수 있는 CPU/메모리 요청 및 제한의 총량에 집계 제한을 부과하여 공정성을 보장합니다.

네임스페이스 할당량 예제

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: development
spec:
  hard:
    requests.cpu: "10"
    limits.memory: "20Gi"

이것은 development 네임스페이스의 모든 파드에 걸쳐 요청된 총 CPU가 10코어를 초과할 수 없고, 총 메모리 제한이 20Gi를 초과할 수 없도록 보장합니다.

잘못된 설정이 실제 클러스터에서 나타나는 방식

리소스 실수는 거의 "리소스 실수"라고 스스로 알리지 않습니다. 일반적으로 애플리케이션 사고처럼 보입니다.

CPU 제한이 너무 낮으면 CPU 사용량 그래프가 제한된 것처럼 보이는 동안 높은 요청 지연 시간이 발생할 수 있습니다. 컨테이너는 더 많은 CPU를 원하지만 cgroup 제한이 이를 억제합니다. Prometheus 기반 설정에서 container_cpu_cfs_throttled_periods_totalcontainer_cpu_cfs_periods_total과 같은 메트릭은 제한이 문제의 일부인지 보여주는 데 도움이 될 수 있습니다.

메모리 제한이 너무 낮으면 파드가 Reason: OOMKilled와 함께 재시작될 수 있습니다. 프로세스가 정상적으로 종료되지 않았기 때문에 애플리케이션 로그가 갑자기 끝날 수 있습니다. 이 경우 kubectl describe pod가 일반적으로 앱 로그보다 더 빠르게 진실을 알려줍니다.

요청이 너무 높으면 대시보드에 평균 노드 사용량이 낮게 표시됨에도 파드가 Pending 상태에 머무를 수 있습니다. 스케줄러는 평균 실제 사용량을 기반으로 파드를 배치하지 않습니다. 요청된 리소스를 할당 가능한 리소스와 비교합니다. 이것이 클러스터가 사용률이 낮아 보이면서도 새 파드를 거부할 수 있는 이유입니다.

요청이 누락되면 워크로드는 조용한 기간 동안에는 괜찮아 보이다가 노드가 바쁠 때 가장 먼저 압박을 받는 대상이 될 수 있습니다. 이는 일회성 작업에는 허용될 수 있지만 사용자 대면 서비스에는 좋지 않은 기본값입니다.

더 안전한 튜닝 워크플로우

실용적인 튜닝 루프는 다음과 같습니다:

  1. 사이드카를 포함한 모든 프로덕션 컨테이너에 대해 명시적인 CPU 및 메모리 요청으로 시작합니다.
  2. 관찰된 최대 사용량에 여유분을 더한 값을 기반으로 메모리 제한을 설정하고 OOMKilled 재시작을 모니터링합니다.
  3. CPU 제한이 정책 또는 워크로드 위험에 필요한지 결정합니다. 사용하는 경우 제한을 모니터링합니다.
  4. 요청된 CPU 및 메모리를 주별 또는 월별로 실제 사용량과 비교합니다. 특히 주요 릴리스 이후에 더욱 그렇습니다.
  5. 적정 규모 조정을 변경 관리로 취급합니다. 더 낮은 요청은 빈 패킹 밀도를 높일 수 있지만 노드 압박 시 실패 동작을 변경할 수도 있습니다.

사이드카는 주의가 필요합니다. 서비스 메시 프록시, 로그 전송기 또는 보안 에이전트는 파드의 실제 리소스 사용량을 변경할 만큼 충분한 CPU 또는 메모리를 소비할 수 있습니다. 메인 앱 컨테이너만 튜닝되면 파드는 여전히 스케줄러에 잘못 표현될 수 있습니다.

예제: CPU 제한으로 인한 지연 시간 급증 해결

다음 구성을 가진 API 컨테이너를 상상해보세요:

resources:
  requests:
    cpu: "100m"
    memory: "256Mi"
  limits:
    cpu: "200m"
    memory: "512Mi"

세일 이벤트 중에 지연 시간이 급증합니다. 노드에는 여전히 유휴 CPU가 있지만 컨테이너는 200m로 제한됩니다. 레플리카를 늘리는 것이 도움이 될 수 있지만 각 파드는 여전히 개별적으로 제한됩니다. 더 나은 해결책은 CPU 제한을 높이거나 제거하고, 요청을 정상적인 지속 수요와 일치하도록 늘리며, 지연 시간이 나빠지기 전에 서비스가 확장되도록 HPA를 사용하는 것입니다.

중요한 교훈은 그래프에서 낮은 CPU 사용량이 항상 앱이 유휴 상태임을 의미하지는 않는다는 것입니다. 앱이 더 많이 사용하도록 허용되지 않았을 수도 있습니다.

최종 확인

요청은 쿠버네티스에 파드를 배치하고 우선순위를 지정하는 방법을 알려줍니다. 제한은 커널에 파드를 중지할 위치를 알려줍니다. 좋은 값은 실제 메트릭, 부하 테스트, 그리고 각 워크로드에 대해 무엇이 더 중요한지(밀도, 격리, 지연 시간 또는 비용)에 대한 명확한 결정에서 비롯됩니다. 트래픽 변경, 종속성 변경 및 런타임 업그레이드 후에 값을 재검토하십시오. 오래된 리소스 설정은 클러스터가 성능 저하로 이어지는 가장 조용한 방법 중 하나입니다.