7분 읽기

쿠버네티스에서 CPU와 메모리 관리하기

“Node CPU가 아직 남아 보이는데 왜 Pod가 Pending이지?”는 Kubernetes를 운영하며 자주 만나는 질문이다. 대개 원인은 현재 CPU 사용률이 아니라, 다른 Pod가 이미 요청해 둔 자원의 합에 있다.

resources.requestsresources.limits는 둘 다 CPU와 메모리 값을 적지만 역할은 다르다.

설정주로 관여하는 컴포넌트
requestskube-scheduler이 Pod를 배치할 때 확보해야 하는 최소 자원
limitskubelet / container runtime실행 중 컨테이너가 넘지 못하게 할 상한

공식 Resource Management for Pods and Containers 문서도 scheduler는 request로 Node를 고르고, kubelet은 실행 중인 컨테이너에 CPU와 메모리 limit을 적용한다고 설명한다.

request는 스케줄링 용량을 예약하고 limit은 실행 중 자원 사용 상한을 적용하는 흐름
request는 배치 가능 여부, limit은 실행 중 사용할 수 있는 최대량을 다룬다.

request로 Pod가 들어갈 자리를 계산한다

Node마다 Pod에 나눠 줄 수 있는 allocatable CPU와 메모리가 있다. scheduler는 이미 배치된 컨테이너의 request와 새 Pod의 request를 더한 뒤, 그 값이 Node 용량 안에 들어오는지 확인한다.

Node allocatable CPU: 4 cores
기존 Pod들의 CPU request 합: 3.7 cores
새 Pod의 CPU request: 500m

3.7 + 0.5 > 4.0  → 이 Node는 후보에서 제외

이때 실제 CPU 사용률이 20%여도 결과는 바뀌지 않는다. scheduler는 순간 사용량이 아니라 선언된 예산을 기준으로 안정적으로 배치한다. 컨테이너가 request보다 더 쓸 수 있는 상황도 있으므로, 낮은 사용률만 보고 request를 과하게 줄이는 것도 위험하다.

API와 배치 Job이 같은 Node를 쓴다면

thumbnail-api는 평소 CPU 사용량이 낮지만 트래픽이 들어오면 즉시 처리할 여유가 필요하다. 반면 image-converter Job은 몇 분 동안 CPU를 집중적으로 사용해도 된다. 둘을 같은 일반 Node pool에서 돌린다면 최소한의 실행 예산을 명시해야 한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: thumbnail-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: thumbnail-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: thumbnail-api
    spec:
      containers:
        - name: api
          image: example.registry/thumbnail-api:1.0
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi
        - name: metrics-sidecar
          image: example.registry/metrics-sidecar:1.0
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 200m
              memory: 256Mi

스케줄러가 계산하는 이 Pod의 CPU request는 600m, 메모리 request는 640Mi다. Pod 안에 컨테이너가 여러 개 있으면 각 컨테이너의 request와 limit을 더해 Pod가 필요한 전체 자원을 구한다.

Job도 같은 방식이다.

apiVersion: batch/v1
kind: Job
metadata:
  name: image-converter
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: converter
          image: example.registry/image-converter:1.0
          resources:
            requests:
              cpu: "1"
              memory: 1Gi
            limits:
              cpu: "2"
              memory: 2Gi

이 Job Pod는 Node에서 CPU 1코어와 메모리 1GiB를 확보할 수 있을 때만 배치된다. CPU limit인 2코어는 스케줄러가 예약하는 값이 아니라 실행 중 허용하는 상한이다.

CPU limit과 메모리 limit 비교하기

CPU limit을 넘기려는 컨테이너는 사용 속도가 제한되어 느려질 수 있지만, 일반적으로 바로 종료되지는 않는다. 반면 메모리는 필요한 양을 확보하지 못하면 컨테이너가 OOM(메모리 부족)으로 종료될 수 있다.

그래서 운영에서는 보통 다음을 분리해 생각한다.

관찰한 현상먼저 의심할 것
Pod가 Pending, Insufficient cpurequest가 Node의 남은 예약 용량보다 큰지
응답 시간이 주기적으로 튐CPU limit으로 throttling이 생기는지
컨테이너가 OOMKilled메모리 limit이 최대 사용량보다 낮은지, 메모리 누수가 있는지
Node는 비어 보이는데 새 Pod가 안 올라감다른 Pod들의 request 합, Node allocatable, taint/affinity

kubectl top은 실제 사용량을 볼 때 유용하지만 scheduler의 결정을 그대로 설명해 주지는 않는다. 스케줄링 문제는 kubectl describe pod <pod-name> 이벤트와 kubectl describe node <node-name>의 Allocated resources를 함께 봐야 한다.

request를 너무 작게 잡으면 어떻게 될까?

request를 실제 필요량보다 작게 잡으면 스케줄러는 많은 Pod를 한 Node에 넣을 수 있다. 처음에는 Node 활용률이 좋아 보인다. 하지만 트래픽이 함께 늘면 여러 Pod가 동시에 CPU와 메모리를 더 사용하면서 응답이 느려지거나 Node의 메모리가 부족해질 수 있다.

반대로 request를 무조건 크게 잡아도 문제다. 실제로는 잘 쓰지 않는 자리를 예약하게 되어 Pod가 Pending에 머물 수 있고, 자동 확장 기능이 불필요하게 Node를 늘릴 수도 있다.

좋은 출발점은 아래와 같다.

  1. 안정적으로 필요한 사용량과 피크 사용량을 모니터링으로 나눈다.
  2. request는 서비스가 평소 안정적으로 동작하는 데 필요한 수준으로 둔다.
  3. limit은 과도한 사용을 막는 안전장치로 두되, 최대 메모리 사용량보다 낮게 잡지 않는다.
  4. 배치 Job은 병렬도와 request를 함께 조정한다. Job 하나는 작아도 수십 개가 동시에 실행되면 합계가 커진다.

QoS 등급은 설정에 따라 정해진다

request와 limit 설정에 따라 Pod에는 Guaranteed, Burstable, BestEffort QoS 등급이 붙는다. 이 등급은 Node 자원이 부족해 Pod를 내보낼 때 우선순위에 영향을 줄 수 있다. 하지만 모든 Pod를 Guaranteed로 만드는 것이 목표는 아니다. 서비스에 필요한 request와 limit을 먼저 정하면 QoS 등급은 그 결과로 정해진다.

QoS의 정확한 조건은 Pod Quality of Service Classes 공식 문서에서 확인할 수 있다.

핵심 정리

자원 설정은 단순한 성능 튜닝이 아니라 스케줄링 정책이다.

  • request는 scheduler에게 필요한 자리 크기를 알려 준다.
  • limit은 실행 중 한 컨테이너가 쓸 수 있는 상한을 정한다.
  • Pod의 request는 컨테이너 request의 합으로 생각한다.
  • Pending은 현재 사용률보다 request와 allocatable의 조합으로 먼저 설명해야 한다.

다음 글에서는 자원이 정말 부족할 때 어떤 Pod를 먼저 대기열에 올리고, 언제 실행 중인 Pod를 비울 수 있는지 PriorityClass와 preemption으로 이어서 살펴본다.

참고 자료