복사되었습니다.

Kubernetes에서 CPU의 request와 limit은 어떻게 설정해야 할까?

Cover Image for Kubernetes에서 CPU의 request와 limit은 어떻게 설정해야 할까?

Kubernetes 환경에 다양한 app들을 배포하여 운영하다보면 리소스 할당의 중요성을 깨닫는 순간이 온다. 별도로 설정을 하지 않으면 kubernetes는 CPU와 memory에 대한 request와 limit 값을 알아서 채워주지 않는다. 그렇다면 request와 limit 값을 설정하지 않고 app들을 운영하는 것은 괜찮은 걸까? 만약 몇몇 app들이 CPU를 과도하게 사용한다면 어떤 일이 벌어질까? 실제 stress test를 통해 어떤 현상이 나타나는지 알아보고, CPU의 request와 limit 값은 어떻게 설정하는 것이 좋은지 알아보자.

Kubernetes의 CPU 자원 할당에 대한 사전 조사

Request와 limit의 의미와 동작 방식

Kubernetes 공식 문서에 따르면, request 값은 pod를 노드에 배포할 때 참고하는 기준이고, limit은 실제 사용량을 제한해주는 값이다.

Pod들이 limit 없이 CPU를 과도하게 사용하더라도, 노드에 존재하는 최대 CPU를 초과해서 사용하지는 못한다. 따라서, kubernetes는 과도하게 CPU를 사용하는 pod를 딱히 evict시키지 않는다.

이 글에 따르면, CPU에 limit을 두는 것은 안티패턴이라고 한다. Request 값을 잘 설정해야 한다는 것에는 이견이 없겠지만, limit 값은 정말 설정하지 않아도 되는 것일까?

Limit을 설정하지 않으면 kubelet이 죽는다?

CPU request와 limit을 제대로 설정하지 않았을 때의 영향을 조사하던 중, 글 하나를 발견했다. 이 글에서는 pod가 limit없이 CPU를 과도하게 사용할 경우 kubelet 프로세스에도 영향을 미치기 때문에, 노드가 NotReady 상태에 빠질 수 있다고 경고한다. 정말 pod 하나의 무분별한 CPU 남용으로 노드 전체가 위기에 빠질 수도 있는 것일까?

알고 싶은 것은 무엇인가?

  • Request 설정은 반드시 해야 하는 것일까?
  • Request 값은 어느 정도로 설정해야 할까?
  • Limit 설정 "해야 한다" vs "하지 말아야 한다" 어느 쪽이 맞을까?

Kubernetes에서 CPU stress test해보기

CPU stress test를 위해 필요한 것

CPU stress test를 위한 tool들이 많이 있지만, 여기서는 간단한 python 코드로 진행한다. 코드는 이 곳을 참고했다.

import time
import sys
from itertools import repeat
from multiprocessing import Pool, cpu_count

def f(x, runtime=1, sleeptime=0, busycycles=100000):
  timeout = time.time() + runtime
  cnt = 0
  while True:
    if time.time() > timeout:
      break
    if sleeptime and cnt % busycycles == 0:
      time.sleep(sleeptime)
    x*x
    cnt += 1

if __name__ == '__main__':
  runtime = 5 if len(sys.argv) <= 1 else float(sys.argv[1])
  sleeptime = 0.0 if len(sys.argv) <= 2 else float(sys.argv[2])
  busycycles = 100000 if len(sys.argv) <= 3 else int(sys.argv[3])
  processes = cpu_count() if len(sys.argv) <= 4 else int(sys.argv[4]) if 0 < int(sys.argv[4]) <= cpu_count() else cpu_count()
  print(f'Running for {runtime}s with sleep time of {sleeptime}s per {busycycles} cycles utilizing {processes} cores')
  pool = Pool(processes)
  pool.starmap(f, zip(range(processes), repeat(runtime), repeat(sleeptime), repeat(busycycles)))

Pod의 CPU 사용량 확인은 metrics-server 설치 후 kubectl top pods 명령어를 실행한 결과를 사용한다.

CPU stress test 방법

위에서 살펴본 python 코드를 활용하여 app 하나가 실행되면 10분 동안 사용할 수 있는 모든 CPU 자원을 사용하도록 pod로 배포한다. 총 4개의 pod를 만들며, request와 limit 값은 다음과 같이 설정한다.

NameRequestLimit
pod1--
pod21.5 cores-
pod30.5 cores-
pod40.5 cores0.75 cores

노드의 CPU가 총 6코어인 환경에서 다음과 같이 여러 실험을 진행한다.

(1) 실험 1: pod1을 띄워둔 상태에서 kubelet이 죽는지 확인한다. 노드의 상태가 NotReady가 되는지 확인하면 된다.

(2) 실험 2: pod1이 떠있는 상태에서 pod1을 추가적으로 2개 더 띄운다. 이를 통해 이미 CPU 자원이 모두 점유된 상태에서 새로운 pod가 request, limit을 설정하지 않은 상태로 배포되는 경우 정상적으로 동작하는지 확인한다.

(3) 실험 3: pod1이 떠있는 상태에서 pod2와 pod3을 추가적으로 띄운다. 이를 통해 이미 CPU 자원이 모두 점유된 상태에서 새로운 pod가 request를 설정한 상태로 배포되는 경우 정상적으로 동작하는지 확인한다.

(4) 실험 4: pod1이 떠있는 상태에서 pod2, pod3, 그리고 pod4를 추가적으로 띄운다. 이를 통해 limit이 설정된 pod가 추가될 경우 어떤 양상을 보이는지 확인한다.

CPU stress test 결과

실험 1: kubelet down 여부 확인

$ kubectl top pods
NAME   CPU(cores)   MEMORY(bytes)
pod1   5742m        21Mi
$ kubectl get nodes
NAME     STATUS   ROLES                  AGE   VERSION
master   Ready    control-plane,master   7d    v1.23.6

pod1이 노드의 거의 모든 CPU(6 코어)를 점유하더라도 kubelet은 죽지 않는 것으로 확인된다.

실험 2: 전부 request 없을 때의 CPU 분배

$ kubectl top pods
NAME   CPU(cores)   MEMORY(bytes)
pod1   1856m        21Mi
pod2   2191m        21Mi
pod3   2027m        21Mi

세 pod들 모두 균등하게 CPU를 점유한다. 먼저 배포되는 pod가 이미 CPU를 점유하고 있더라도, 추가 pod가 배포될 때 자원을 빼앗으면서 균형이 맞춰진다.

실험 3: 일부 request 있을 때의 CPU 분배

$ kubectl top pods
NAME   CPU(cores)   MEMORY(bytes)
pod1   21m          21Mi
pod2   4487m        21Mi
pod3   1334m        21Mi

pod1은 처음에 거의 모든 CPU를 점유하고 있다가, pod2가 배포되면 pod2에게 모든 CPU를 빼앗긴다. pod3이 배포되면 pod2와 pod3이 3:1 비율로 모든 CPU를 점유하고, pod1은 거의 CPU을 할당받지 못한다.

실험 4: 일부 limit이 있을 때의 CPU 분배

$ kubectl top pods
NAME   CPU(cores)   MEMORY(bytes)
pod1   17m          21Mi
pod2   3740m        21Mi
pod3   1363m        21Mi
pod4   750m         21Mi

실험 3의 결과와 거의 동일하다. 단, limit 값을 설정한 pod4는 정확히 limit 값만큼만 CPU를 할당받고, 남은 자원을 pod2와 pod3이 request 값 비율대로 나눠가진다.

결론: CPU의 request와 limit은 어떻게 설정해야 할까?

CPU stress test 결과 요약

앞선 실험 결과들을 종합 정리해보자.

  • 노드의 CPU 자원을 거의 100%에 가깝게 사용하더라도 kubelet은 죽지 않는다.
  • 모든 pod들이 request 값이 없다면, 각 pod는 전체 CPU 자원을 균등하게 나눠서 사용한다. 이미 다른 pod가 먼저 CPU를 점유하더라도, 이후에 배포되는 pod들이 여유분의 CPU를 빼앗는다.
  • Pod들 중 하나라도 request를 설정한다면, request를 설정하지 않은 pod는 CPU 자원을 거의 사용하지 못한다. Request 값의 크기와 상관이 없다. 다시 말해서, request를 매우 적게 설정한 pod라도 대부분의 CPU를 독점한다.
  • Request를 설정한 pod들끼리는 설정값의 비율대로 남은(노드의 전체 core 수 - request의 합) 자원을 나눠가진다. 예를 들어, 노드의 CPU가 10일 때, pod1, pod2, pod3이 각각 3, 1, 1만큼 CPU를 요청했다면, 각각 6, 2, 2만큼 할당받는다.
  • Limit 값을 설정하면, 어떤 상황에서든 최대 사용량이 limit만큼으로 제한된다.

한 가지 유의할 점은, 현재 살펴보는 내용들은 각 pod들이 모두 과도하게 CPU를 쓰고자하는 상황을 전제로 한다는 것이다. CPU 수요가 많지 않은 평상시의 상황에서는 위 내용이 맞지 않을 수 있다.

결국 어떻게 하라는 말인가?

알고 싶은 것은 무엇인가?에서 던진 의문을 해소해보자.

Request 설정은 반드시 해야 하는 것일까?

Request 값을 줄 것이라면 하나도 빠짐없이 모든 pod에 주어야 한다. 모든 app에 request를 할당하지 않겠다고 다짐하더라도, helm을 통해 monitoring 툴이나 storage class 등 다른 리소스들을 설치하면서 request가 예기치 않게 설정되는 경우가 있다. 이런 경우 request를 설정하지 않은 app들의 성능이 급격히 저하될 가능성이 있다.

Request 값은 어느 정도로 설정해야 할까?

Request 값은 평상시 app의 사용량 정도로 할당하자. Request에 설정한 값만큼의 CPU는 반드시 보장된다. 갑자기 많은 연산량이 요구될 때에도, 노드에 남은 CPU 자원에서 추가적으로 자원을 확보한다. 단, 추가분은 request의 비율대로 할당받는다는 점을 기억해야 한다. 어렵겠지만, 전체적인 관점에서 상대적으로 높은 연산량을 요구하는 app일수록 높은 request를 지정해야 한다.

만약 타이트하게 노드의 모든 CPU를 남김없이 request해서 사용해야 하는 환경이라면, 평상시 app의 사용량 정도보다 조금 높게 할당하는 것이 좋다. 보통 kubernetes 클러스터에 taint같은 설정을 제대로 하지 않으면 pod 배포가 특정 노드에 쏠리는 현상이 발생할 때가 있다. 이런 경우 app들의 성능이 저하될 수 있다. 노드에 request 여분이 남아있도록 잘 설정하자.

적절한 값을 찾기가 어렵다면, Robusta KRR같은 툴을 사용하면 큰 도움이 된다. 일정 기간 동안의 사용량을 바탕으로 적절한 request 값을 추천해준다. 사용법도 매우 간편하다.

Limit 설정 "해야 한다" vs "하지 말아야 한다" 어느 쪽이 맞을까?

Limit 값은 특정 pod가 딱 정해진 만큼만 쓰도록 강제하고 싶을 때만 사용하자. CPU를 나눠쓴다는 것은 물리적인 core를 실제로 나눠서 사용하는 것이 아니라, 일정 시간을 나눠쓰는 time slicing 개념에 가깝다. 만약 모든 pod들에 limit이 걸려있고 모든 limit의 합이 노드의 전체 CPU 양보다 적다면, CPU가 백수(idle) 상태에 빠지는 시간이 발생하게 된다. 일한 시간만큼만 돈을 버는 아르바이트에 비유해보면, 24시간 일을 하지 않고 잠자는 시간이 늘어날수록 수익이 떨어지는 구간이 생긴다는 얘기다. CPU 수요가 많은 app은 limit을 설정하지 않는 것이 좋다고 할 수 있다.

여전히 실제 적용할 때는 어려움이 따른다. 전반적인 성능을 생각한다면 request 값의 비율을 잘 조절하고 limit 값을 아예 설정하지 않는 것이 좋을 것이다. 그런데, 만약 사용자들이 1 core를 사용하는 pod를 요청했다고 가정해보자. 이 pod가 정확히 1 core까지만 사용하도록 제한하는 것이 맞을까? 아니면 여유가 있을 때 1 core 이상 사용하는 것도 허용해주는 것이 맞을까? 이런 고민들은 사실 정답이 없다. 결국 합의가 필요하다.

Comments

    More Posts

    Git-sync로 kubernetes 환경에 앱 배포 자동화하기 - 초간단 CI/CD 구축

    Git-sync를 사용하면 github에 코드를 올릴 때마다 자동으로 앱이 업데이트되도록 만들 수 있다. 코드를 수정할 때마다 직접 배포하지 말고, git-sync를 통해 kubernetes 환경에 간단하게 CI/CD 환경을 구성해보자.

    Kubernetes 1.23 버전의 최후 - Ubuntu kubernetes apt install 에러

    2024년 3월 초, docker가 익숙해서 끝까지 버티고 있던 kubernetes v1.23.x 개발 환경 설치에 돌연 문제가 생겼다. Ubuntu에서 kubeadm, kubelet, kubectl 등을 apt로 설치할 수 없게 된 것이다. 원인과 해결 방법에 대해 알아보자.

    Kubernetes에서 memory가 부족해지면 어떻게 될까? - Kubernetes Out-Of-Memory(OOM) kill and eviction

    Kubernetes의 주요 역할 중 하나는 클러스터의 자원을 효율적으로 관리해주는 것이다. Kubernetes는 클러스터에 자원이 부족한 상황에서 자원을 재분배하기도 한다. 쉽게 말해서, 어떤 앱들은 잘 동작하고 있다가도 외부의 압력으로 인해 강제로 종료될 수 있다는 의미다. 그렇다면, kubernetes는 어떤 기준으로 pod들을 종료시키는 것일까? 가장 흔히 접하게 되는 Out-Of-Memory(OOM) 현상을 위주로 살펴보자.

    Font Size