Kubernetes Resources, 제대로 이해하기
본 포스팅에서는 Kubernetes의 Resources 를 제한하고 보장받을 양을 지정하는 방법을 익힙니다.
🔗 Kubernetes Series
모든 Kubernetes 시리즈를 확인하시려면 위를 참고해 주세요.
Resource Management
for Pods and Containers
모든 Pod를 운영하려면 자원이 필요하며,
모든 노드엔 사용 가능한 CPU와 메모리 리소스를 가지고 있습니다.
쿠버네티스 스케줄러는 Pod에 요구되는 리소스 양을 고려하여,
노드에서 사용 가능한 리소스에 따라 Pod가 배치될 최적의 노드를 결정합니다.
이때, 노드에 배치할 Pod를 지정 시, 컨테이너에 필요한 각 리소스의 양을 선택적으로 지정할 수 있는데요.
대표적인 리소스에는, CPU와 메모리(RAM) 등이 있습니다.
🔋 CPU / 🛢 Memory
Pod A: 🔋🔋🛢🛢🛢
Node1 [🔋🛢🛢🛢]
Node2 [🔋🔋🛢🛢🛢]
Node3 [🔋🔋🔋🔋🛢🛢🛢🛢]
위의 예시의 경우, Pod A이 요구하는 사항 (🔋: 3, 🛢: 3) 을 만족하는 Node2 혹은 Node3에 배치될 수 있겠죠.
Request & Limit
리소스에 대해서 request 와 limit 값을 지정할 수 있는데요.
request
Pod에서 컨테이너에 대한 리소스 요구 사항(request )을 지정하면,
kube-scheduler는 이를 보장하여 배치될 노드를 결정합니다.
kubelet이 컨테이너가 사용할 해당 시스템 리소스의 최소 request 양을 예약해둡니다.
가령, 컨테이너에 256MiB의 memory
request 를 설정하고,
8GiB의 메모리를 가진 노드로 배치될 Pod 내에 해당 컨테이너가 있으며,
다른 파드는 없는 경우 컨테이너는 더 많은 RAM을 사용할 수 있습니다.
🛢 Memory
Pod 1️⃣: Requests 🛢🛢
Node1 [🛢]
Node2 [🛢🛢🛢]
Node3 [🛢🛢🛢🛢🛢🛢]
Pod 1️⃣은 Node2, Node3 에 배치될 수 있습니다.
limit
컨테이너에 대한 리소스 제한(limit)을 지정하면,
kubelet은 실행 중인 컨테이너가 설정한 제한보다 많은 리소스를 사용할 수 없도록 합니다.
가령, 컨테이너에 4GiB의 memory
lmit을 설정하면, kubelet (& Container Runtime)이 제한을 적용합니다.
Container Runtime은 컨테이너가 해당 리소스 제한을 초과하지 못하게 합니다.
Before
Node [🛢🛢🛢🛢🛢🛢]
Pod 1️⃣ 의 Limit 🛢🛢🛢
After
Node [🛢🛢🛢]
1️⃣: 🛢🛢🛢 ← ❌ 🛢
Pod 1은 Node에 자원이 남아있어도, 3🛢 이상 사용할 수 없습니다.
limit은 두 가지 방식으로 구현할 수 있습니다.
- reactively: 반응적. 시스템이 위반을 감지한 후에 개입
- enforcement: 강제적. 시스템이 컨테이너가 제한을 초과하지 않도록 방지
⚠️ limit은 지정하고 request은 지정하지 않는 경우,
해당 리소스에 대한 요청 기본값을 지정하는 admission-time mechanism이 없다면,
limit을 복사하여 해당 리소스의 request 값으로 사용합니다.
컨테이너 프로세스가 허용된 양보다 많은 메모리를 사용하려고 하면,
시스템 커널은 메모리 부족(out of memory, OOM) 오류와 함께 할당을 시도한 프로세스를 종료합니다.
노드에 충분한 리소스가 없으면,
스케줄러는 해당 노드에 Pod를 두는 걸 피하고 리소스가 충분히 사용 가능한 곳에 배치합니다.
모든 노드에 사용 가능한 리소스가 충분하지 않으면 스케줄러가 Pod의 스케줄링을 보류합니다.
가령, CPU가 충분치 않을 때, 아래와 같은 오류 이벤트가 발생합니다.
Events:
Reason Message
------ -------
FailedScheduling No nodes are available that match all of the following predicates:: Insufficient cpu (3).
Pod은 현재 Pending 상태 유지한 상태이며,
kubectl describe 명령어로 확인한 Events에서 CPU가 불충분하다는 것을 확인할 수 있습니다.
Resources
그럼, Resources를 실제로 설정 방법에 대해 알아보겠습니다.
각 컨테이너에 대해, 다음과 같은 리소스 제한(limit) 및 요청(request)을 지정할 수 있습니다.
✔️ spec.containers[].resources.limits.cpu
✔️ spec.containers[].resources.limits.memory
✔️ spec.containers[].resources.limits.hugepages-<size>
✔️ spec.containers[].resources.requests.cpu
✔️ spec.containers[].resources.requests.memory
✔️ spec.containers[].resources.requests.hugepages-<size>
Resources: CPU
Pod 정의를 통해 생성 시, 필요한 CPU와 메모리의 양을 지정할 수 있습니다.
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"
- name: log-aggregator
image: images.my-company.example/log-aggregator:v6
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
⚠️ 이 때, Pod 내 모든 컨테이너마다 제한과 요청이 설정됩니다.
컨테이너가 여러 개면 각 컨테이너는 개별적으로 요청이나 제한을 설정할 수 있습니다.
쿠버네티스에서 CPU 리소스에 대한 단위는, 1 CPU 단위는 1 Physical CPU core 혹은, 1 virtual core입니다.
가령, 컨테이너의 spec.containers[].resources.requests.cpu
를 0.5로 설정한다는 것은,
1.0 CPU 절반의 CPU 타임을 요청한다는 의미입니다.
Resources: Memory
memory 에 대한 제한 및 요청은 바이트 단위를 사용합니다.
E, P, T, G, M, k 혹은 Ei, Pi, Ti, Gi, Mi, Ki 와 같이, Quantity 접미사 중 하나를 사용하여 표현할 수 있습니다.
1 G (Gigabyte) = 1,000,000,000 bytes
1 M (Megabyte) = 1,000,000 bytes
1 K (Kilobyte) = 1,000 bytes
1 Gi (Gigibyte) = 1,073,741,824 bytes
1 Mi (Mebibyte) = 1,048,576 bytes
1 Ki (Kibibyte) = 1,024 bytes
Behavior
Pod가 지정된 limit을 초과하면 어떻게 될까요?
CPU는 시스템이 CPU를 조절해 지정된 한도를 넘지 않습니다.
즉, 컨테이너가 한계점 이상으로 CPU 리소스를 쓸 수 없습니다.
Memory는 컨테이너는 한도보다 많은 메모리 리소스를 초과하여 쓸 수 있습니다.
계속해서 limit 보다 많은 메모리를 소모하면 Out-of-Memory 오류로 종료 (terminated) 됩니다.
kubeclt 명령어의 log나 describe 을 사용하면, 출력에서 Pod가 OOM 에러를 확인할 수 있습니다.
기본적으로 쿠버네테스는 CPU나 메모리 요청, 한계점이 없습니다.
즉, 어떤 Pod든 어떤 노드에서 필요한 만큼의 자원을 소비할 수 있고,
해당 노드에서 실행 중인 다른 Pod나 프로세스를 질식(suffocate)시킬 수 있습니다.
그렇다면, 클러스터에서 CPU 리소스를 공유하는 여러 Pod가 있을 때,
어떻게 설정하는 게 가장 이상적일까요?
Behavior: CPU
한 Pod가 필요한 양의 CPU를 아래와 같이 표시해보겠습니다.
Available CPU: 🔋🔋🔋🔋🔋🔋
위 CPU를 공유해서 사용하는 Pod 1️⃣ 과 Pod 2️⃣ 가 있을 때,
가장 이상적인 상황을 찾아보도록 하겠습니다.
✔️ CASE 1. NO REQUESTS / NO LIMITS
리소스 limit을 지정하지 않으면,
Pod 하나가 노드의 모든 CPU 리소스를 소비하고 두 번째 리소스 request도 막을 수 있습니다.
Pod 1️⃣ 에 Limit과 Request를 지정하지 않을 때
Available CPU: (Empty)
Limit: X, Request: X
1️⃣: 🔋🔋🔋🔋🔋🔋
2️⃣: ❌ (사용 불가)
✔️ CASE 2. NO REQUESTS / LIMITS
구체적인 request은 없지만 limit가 구체적으로 명시된 경우,
이 경우 쿠버네티스는 자동으로 요청을 한계와 동일하게 설정합니다.
Pod 1️⃣ 에 Limit만 지정했을 때
Available CPU: (Empty)
Limit: 3 🔋, Request: (Empty) = Limit
1️⃣: 🔋🔋🔋
2️⃣: 🔋🔋🔋
request이 비어있지만, limit 값이 존재하기 때문에 request를 3개로 추정합니다.
Pod마다 vCPU가 3개 이상 보장합니다.
✔️ CASE 3. REQUESTS / LIMITS
request과 limit가 모두 구체적으로 명시된 경우,
각 Pod에는 vCPU 하나인 CPU 요청이 보장되고, vCPU 3 까지만 사용 가능합니다.
Pod 1️⃣ 에 Limit과 request 둘 다 지정했을 때
Available CPU: 🔋🔋
Limit: 3 🔋, Request: 1 🔋
1️⃣: 🔋🔋🔋
2️⃣: 🔋
이상적으로 보이지만, 비효율이 존재합니다.
그런데, Pod 2️⃣ 또한 CPU를 많이 차지 않는 상황이라면, CPU의 제한을 두고 싶지 않을 수 있습니다.
그래서 1️⃣번 Pod의 CPU를 더 사용할 수 있게 하고 싶다면,
CPU 사이클의 리소스를 불필요하게 제한하지 않는게 낫습니다.
✔️ CASE 4. REQUESTS / NO LIMITS
limit 없이 request 설정할 경우,
요청이 정해져 있어서, 각 구역마다 vCPU를 사용을 보장받습니다.
Pod 1️⃣ 에 request만 지정했을 때
Available CPU: 🔋
Limit: (Empty), Request: 1 🔋
1️⃣: 🔋🔋🔋🔋
2️⃣: 🔋
사용 가능 시에는 한계가 설정되지 않기 때문에, Pod마다 CPU 사용량을 최대한 많이 사용 가능합니다.
또, 어떤 시점에서든 Pod 2의 CPU 사용이 요구되면 요청된 CPU 사용량이 보장됩니다.
즉, 가장 이상적인 환경이죠.
Behavior: Memory
이번에는 Pod가 필요한 양의 Memory를 아래와 같이 표시해보겠습니다.
Available Memory: 🛢️🛢️🛢️🛢️🛢️🛢️
메모리도 CPU와 동일한데,
위 Memory를 공유해서 사용하는 Pod 1️⃣ 과 Pod 2️⃣ 가 있을 때를 예시로 들어보겠습니다.
✔️ CASE 1. NO REQUESTS / NO LIMITS
메모리 limit을 지정하지 않으면, Limit이 없어 Pod 하나가 노드의 메모리 리소스 전체를 소비할 수 있습니다.
즉, 다른 Pod가 필요한 리소스 사용을 막을 수 있습니다.
Pod 1️⃣ 에 request과 limit 둘 다 지정하지 않을 때
Available Memory: (Empty)
Limit: X, Request: X
1️⃣: 🛢️🛢️🛢️🛢️🛢️🛢️
2️⃣: ❌ (사용 불가)
✔️ CASE 2. NO REQUESTS / LIMITS
Request가 구체적으로 명시되지 않고 limit는 명시되어 있는 경우,
컨테이너는 자동으로 request을 한계로 설정됩니다.
Pod 1️⃣ 에 limit만 지정할 때
Available Memory: (Empty)
Limit: 3 Gi, Request: (Empty) = Limit
1️⃣: 🛢️🛢️🛢️
2️⃣: 🛢️🛢️🛢️
위 예시에서는, Request과 Limit는 둘 다 3Gi로 설정 됩니다.
✔️ CASE 3. REQUESTS / LIMITS
request과 limit가 모두 구체적으로 명시된 경우,
각 Pod에는 메모리 request 만큼의 양이 보장되고, 3Gi 까지만 사용 가능합니다.
Pod 1️⃣ 에 request과 limit 둘 다 지정할 때
Available Memory: 🛢️🛢️
Limit: 3Gi, Request: 1Gi
1️⃣: 🛢️🛢️🛢️
2️⃣: 🛢️
Request과 Limit을 설정한 경우, 각 포드마다 메모리가 보장됩니다.
✔️ CASE 4. REQUESTS / NO LIMITS
Request을 정하되 Limit은 지정하지 않을 경우, 각 Pod에 정해진 Request 만큼의 양을 보장합니다.
Pod 1️⃣ 에 request만 지정할 때
Available Memory: (Empty)
Limit: (Empty), Request: 1Gi
1️⃣: 🛢️🛢️🛢️🛢️🛢️ 💥 OOM
2️⃣: 🛢️
하지만, 한계가 설정되지 않기 때문에 어떤 포드든 사용 가능한 메모리를 소모할 수 있습니다.
CPU와는 달리 메모리를 조절할 수 없기 때문에,
Pod 2가 제공된 메모리 보다 더 요청하면 죽게됩니다.
Memory를 되찾을 유일한 방법은 Pod를 죽이고 Pod에 사용한 Memory를 해제(free)시켜주는 것입니다.
LimitRange: Pod Resource Limit
기본적으로 Pod에 구성된 리소스 요청이나 한계가 없다고 했는데,
그렇다면 Pod 내 Container에 Default로 설정할 Resource Limit을 지정할 수 있을까요?
이는 LimitRange 정의를 설정해서 가능합니다.
LimitRange는 하나의 쿠버네티스 객체이며,
Namespace 레벨에서 생성됩니다.
Pod 정의 파일에 Resource 나 특정 Limit 없이 생성된 컨테이너에 한해, Pod 내 모든 컨테이너에 적용됩니다.
가령, default Limit 을 500m으로 지정하고,
defaultRequest도 동일하게 500m로 지정할 수 있습니다.
apiVersion: v1
kind: LimitRange
metadata:
name: cpu-resource-constraint
spec:
limits:
- default: # limit
cpu: 500m
defaultRequest: # request
cpu: 500m
max: # limit
cpu: "1"
min: # request
cpu: 500m
type: Container
max는 Pod 내 Container에 지정할 수 있는 Maximum limit,
min 은 Pod 내 Container가 만들 수 있는 Minimum request 의미합니다.
✔️ 적용한 Pod에 명시되는 정보
Name: myapp-pod
Namespace: default
Priority: 0
Service Account: default
Node: kind-control-plane/172.21.0.2
Start Time: Sat, 30 Mar 2024 17:04:30 +0900
Labels: app=myapp
type=front-end
Annotations: kubernetes.io/limit-ranger: LimitRanger plugin set: cpu request for container nginx-container; cpu limit for container nginx-container
Status: Running
메모리도 동일합니다.
apiVersion: v1
kind: LimitRange
metadata:
name: memory-resource-constraint
spec:
limits:
- default:
memory: 1Gi
defaultRequest:
memory: 1Gi
max:
memory: 1Gi
min:
memory: 500Mi
type: Container
⚠️ 위 Limitation은 Pod가 생성될 때 강제로 설정됩니다.
만약, LimitRange를 생성하거나 수정한다면, 기존 Pod에는 적용되지 않고 새로 생성되는 Pod에 한해서만 적용됩니다.
쿠버네티스 클러스터에 배포된 앱이 사용할 수 있는 전체 리소스를 제한할 방법이 있습니다.
Resource Quota는 네임스페이스 레벨에서, Request와 Limit의 Hard Limit 값을 설정할 수 있습니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: my-resource-quota
spec:
hard:
request.cpu: 4
request.memory: 4Gi
limit.cpu: 10
limit.memory: 10Gi
그럼 지금까지, 쿠버네티스에서 최소 리소스를 설정하고 제한하는 방법에 대해 알아보았습니다.
| Reference |