[k8s] 리소스 제한: CPU와 메모리

k8s는 기본적으로 컨테이너 관리자이기 때문에, 하나의 노드에 여러개의 컨테이너를 띄워서 갈라먹을 때가 매우 많다.

근데 메모리나 CPU 자원을 어떻게 분산해줘야 할까? 달라는대로 다 줘서는 특정 서버만 메모리를 다 먹고 다른 서버는 메모리가 부족해서 돌지도 못하는 문제가 일어날 수도 있을 것이다.

k8s는 "request"와 "limit"을 통해서 컨테이너의 사용량을 제한하고 분배한다.




Request & Limit

Request와 Limit은 개별 컨테이너 스펙에 정의되는 리소스 사용량 옵션이다.
아래는 그에 대한 간단한 예제다.

---
apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

간단히 분석해보자

- requests
requests은 쉽게 말해 최소 사용량이다.
이 컨테이너는 기본적으로 메모리 64Mi와 CPU 250m를 사용할 수 있지만, 다른 Pod가 별로 없고 노드에 리소스가 넉넉하다면 더 사용할 수도 있다. 그래서 이건 느슨한 사용량 제한이고, 어느 노드에 띄울지를 선택할 때의 근거값으로 주로 쓰인다.
만약 pod request의 총합이 노드의 메모리 제한을 넘는다면, k8s 스케줄러는 실제 사용량과 별개로 pod를 추가로 띄우지 않는다.

- limit
limit는 최대 사용량이다.
이 컨테이너는 사용량이 아무리 많아져도 메모리 128Mi와 CPU 500m 이상을 사용할 수 없다.
메모리 사용량이 정해진 limit을 넘으면 kubelet은 해당 컨테이너를 OOM으로 죽여버린다.
request가 없고 limit만 있다면 limit 값을 request 값으로 사용한다.




CPU 사용량

CPU 사용량은 단순 숫자, 혹은 밀리(m) 접미사가 붙은 숫자로 표현할 수 있다.
1는 1000m와 같다.

메모리는 사용량을 측정하고 제한하는 것이 비교적 직관적이고 단순하지만, CPU 리소스는 경우가 다르다.

k8s에서 1 CPU는 하나의 물리 코어가 가지는 연산능력을 뜻한다. 이건 상대값이 아니라 절대값임에 유의한다.
따라서 cpu: "500m"는 "CPU를 0.5개 정도 쓰는 수준"이라고 이해할 수 있다.

Memory Limit은 초과시에 컨테이너를 종료시키지만, CPU Limit에 걸렸고 노드에도 자원이 부족한 경우에는 종료시키지는 않고 throttle을 건다.




CPU Limit - 찬반

Pod가 CPU Limit에 걸리면 스로틀을 걸어서 사용량을 제한한다고 했었다.

근데 사실 CPU 리소스는 "압축 가능한(compressible)" 자원이기 때문에 Limit을 전혀 걸지 않아도 Pod가 다른 Pod를 방해하는 일이 두드러지지 않는다. Pod들이 알아서 서로 CPU 자원을 적당히 갈라먹으면서 분배될 수 있다.
게다가 request를 잘 설정했다면 request 값을 기준으로 우선순위를 매겨서 CPU 리소스를 분배한다.

오히려 CPU Limit을 도배할 경우 cgroup 기반의 자체적인 소프트 스로틀링 때문에 전체적인 성능 저하가 발생할 수 있다.

그래서 devops들 사이에서는 "CPU Limit은 걸지 않는게 좋다"는 의견들이 꽤 모이는 것 같더라.
물론 이게 아주 정설인건 아니고, 반론도 있긴 있다.




주의할 점

컨테이너 기술은 기본적으로 느슨한 격리다.
그래서 사실 완전히 격리된 것처럼 생각하고 시스템을 설계했다가는 낭패를 볼 수 있다.

예를 들어, limit에 cpu를 1개로 제한한다고 하더라도, 실제로 컨테이너에 cpu가 하나만 할당되는 것은 아니다.
limit이 걸려있더라도 컨테이너는 노드에 물리코어가 8개 있다는 것을 알 수 있다.

그래서 만약 리소스의 최대치를 기반으로 뭔가 프로그램을 구성한다면 예상치 못한 부작용에 부딪칠 수 있다.

예를 들면 전체 물리코어 갯수를 기반으로 스레드를 생성하다거나, JVM 힙 크기를 전체 메모리 크기로 맞춘다거나 하는 행위들 말이다. 이럴 때는 노드 정보가 아닌 컨테이너 limit을 기반으로 동작하도록 주의를 기울여야 한다.




참조
https://kubernetes.io/ko/docs/concepts/configuration/manage-resources-containers/
https://learnk8s.io/setting-cpu-memory-limits-requests
https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits?hl=en
https://dnastacio.medium.com/why-you-should-keep-using-cpu-limits-on-kubernetes-60c4e50dfc61
https://dev-whoan.xyz/110