쿠버네티스는 기본적으로 도커를 런타임으로 사용하기 때문에 메모리/CPU 사용량 제한을 하지 않으면 파드 하나가 노드의 물리 자원을 모두 소진하는 상황이 발생할 수 있다.
(도커는
—-memory
, —-cpus
, —-cpu-shares
, —-cpu-quota
등의 옵션을 사용하지 않고 컨테이너를 실행할 경우 기본적으로 호스트의 메모리/CPU 를 제한없이 사용한다.)Limit
파드가 사용할 자원을 제한하는 가장 간단한 방법은
spec.containers[].resources.limit
을 정의하는 것이다.apiVersion: v1 kind: Pod metadata: name: resource-limit-pod labels: name: resource-limit-pod spec: containers: - name: nginx image: nginx:latest resources: limits: memory: "256Mi" cpu: "1000m"
노드 스펙은 AWS
t2.medium
이다. (AWS EC2 인스턴스 타입 별 스펙 리스트는 여기 참고.)- CPU core 2개, 각 코어 당 하이퍼스레드 1개 → vCPU = 2 코어 X 1 하이퍼스레드 = 2 vCPUs
- 메모리 4GiB ( = 4096 MiB)
CPU 리소스에 대한 제한 및 요청은 cpu 단위로 측정된다. 쿠버네티스의 CPU 1개는 클라우드 공급자용 vCPU/Core 1개와 베어메탈 인텔 프로세서에서의 1개 하이퍼스레드에 해당한다.
resource-limit-pod
파드는 메모리를 최대 256MiB 까지 사용할 수 있고 최대 1 vCPU 의 100% 까지 사용할 수 있다. (공식문서에는 1 cpu time 이라고 나와있는데 1 cpu time 이란 뭘 나타는지 이해가 잘 안되서 좀 찾아보다가 아래 내용으로 정리했다. cpu time은 각 코어가 연산을 수행한 시간의 총합이라고 보면 될 듯.)CPU를 스케줄링 하는 방법으로는 크게 2가지가 있다.
- Completely Fair Scheduler (CFS)
- Real-Time scheduler (RT)
리눅스는 기본적으로 CFS 스케줄러를 사용한다.
CFS는 각 코어의 시간을 1000개로 쪼개서 cgroup 으로 구분되는 프로세스들에게 상대적인 수치로 할당한다.
예를 들어서 프로세스
A
에게는 cpu.shares
를 100, B
에게는 300을 셋팅했다고 하자. 가용한 코어가 1개밖에 없는 상황에서 A와 B가 CPU 사용을 요청했다면 코어가 어떤 단위시간동안 B 요청을 처리하는 시간이 A 요청을 처리하는 시간의 3배가 된다. (단위시간이 1초라고 하면 B 요청을 처리하는 데 0.75초 이면서 단위시간의 75%, A 요청을 처리하는 데 0.25초이면서 단위시간의 25%를 할애하는 것.)
그런데 가용한 코어가 2개인 상황이라면 A, B는 각각 하나의 코어를 사용하기 때문에 A가 B에 비해 상대적으로 1/3 만큼의 시간을 사용한다고 셋팅되어 있더라도 A는 코어 하나를 온전히 다 점유할 수 있다.
반대로
cpu.shares
가 100
인 프로세스 C와 함께 A,B가 하나의 코어를 사용해야 하는 경우프로세스 B 는
300 / 100 + 300 + 100 = 0.6초
단위시간의 60% 만큼의 시간을 할애받고프로세스 A, C 는
100 / 100 + 300 + 100 = 0.2초
단위시간의 20% 만큼의 시간을 할애받는다.cpu.shares
contains an integer value that specifies a relative share of CPU time available to the tasks in a cgroup. For example, tasks in two cgroups that havecpu.shares
set to100
will receive equal CPU time, but tasks in a cgroup that hascpu.shares
set to200
receive twice the CPU time of tasks in a cgroup wherecpu.shares
is set to100
. The value specified in thecpu.shares
file must be2
or higher. Note that shares of CPU time are distributed per all CPU cores on multi-core systems. Even if a cgroup is limited to less than 100% of CPU on a multi-core system, it may use 100% of each individual CPU core.
spec.containers[].resources.limit
에는 위에서 설명한 CFS 외에 bandwith/quota 개념이 추가되었다. 위에서 언급한 단위시간을 쿠버네티스에서는 늘리고 줄일 수 있는 게 (보통은 1/10초로 되어있다.) bandwith 에 해당하고 파드가 단위시간 내에 최대로 할애받을 수 있는 시간을 설정하는 게 quota에 해당한다. 예를 들어 파드 A에는
spec.containers[].resources.limit
을 100m
, 파드 B에는 300m
으로 설정했다고 하자.쿠버네티스 클러스터 내에 vCPU 1개가 가용한 상황에서 두 개 파드에 있는 컨테이너 프로세스가 cpu 사용을 요청한다면 매 1/10초 동안 파드 B에 있는 컨테이너의 요청을 처리하는 데에 할애하는 시간이 A에 비해 3배 더 많으면서, 매 1/10초 마다 cpu를 점유하는 시간이 1/10초의 30% (30,000ms) 를 넘기지 않는다는 걸 의미한다.
반대로 파드 A는 매 1/10초 마다 cpu를 점유하는 시간이 1/10초의 10% (10,000ms)를 넘기지 않는다.
참고한 문서
위 YAML 파일로 파드를 생성하고 파드가 생성된 노드의 상세정보를 조회해봤다.
ubuntu@ip-10-220-12-225:~/k8s-practice$ ku get nodes NAME STATUS ROLES AGE VERSION ip-10-220-12-225 Ready control-plane,master 24d v1.23.4 ip-10-220-6-77 Ready <none> 24d v1.23.4 ubuntu@ip-10-220-12-225:~/k8s-practice$ ku describe nodes ip-10-220-6-77
중간에
Non-terminated Pods
항목을 보면 resource-limit-pod
가 전체 CPU의 50% 까지 사용할 수 있고 (전체 cpu time의 50%), 메모리는 256 / 4096 * 100 = 6%
정도를 사용할 수 있다고 한다.Over Commit
위에 노드 상세정보를 보면 CPU Requests, Memory Requests 항목이 있는데 Request 는 ‘적어도 이 만큼의 자원은 컨테이너에게 보장이 되어야 한다는 것’을 의미한다.
쿠버네티스가 Request 를 지원하는 이유는 오버커밋(Over Commit)을 가능하게 하기 위함이다.
오버커밋은 가상머신 또는 컨테이너에게 사용할 수 있는 자원보다 더 많은 양을 할당함으로써 전체 자원의 사용률 (utilization)을 높이는 방법이다.
메모리 1GB를 가지는 서버에 위와 같이 컨테이너 A, B 를 생성했으며 각 컨테이너에게 500MB 를 고정으로 할당한다고 해보자.
이렇게 고정으로 자원을 할당하면 한 가지 단점이 있는데, 컨테이너 A가 자원을 적게 사용하고 있을 때 A의 유휴 자원을 자원이 더 필요한 컨테이너 B에게 줄 수 없다는 것이다. 즉, 자원이 남아 돌아서 사용률이 낮아진다.
애초에 컨테이너를 생성할 때 컨테이너가 사용할 자원의 양을 미리 예측해서 할당하는 것도 방법이 되겠으나 예측에서 벗어나는 경우가 빈번하다.
쿠버네티스에서는 오버커밋(over commit)을 통해 실제로 존재하는 물리 자원보다 더 많은 양을 할당하게 한다.
위 예시에서는 실제 존재하는 메모리는 1GB 이기 때문에 최대 750MB를 사용할 수 있는 컨테이너 2개를 생성한다고 해서 1.5GB를 사용할 수 있는 것은 아니다.
오버커밋은 어떤 컨테이너가 자원을 적게 사용하고 있을 때 그 컨테이너의 유휴 자원을 다른 컨테이너가 사용할 수 있게 해서 자원의 사용률을 높이는 것이다.
이 때 컨테이너가 사용할 수 있는 최대 자원의 양을
limit
으로 설정한다. 위의 예시에서는 컨테이너 A, B의 메모리 limit
은 750MB
으로 설정되어 있다.대신에 컨테이너 A가 500MB를 이미 점유하고 있는데 컨테이너 B가 750MB를 사용 할 수는 없다.
컨테이너 A의
request
가 500MB
로 설정되어 있기 때문이다.정리하면
request
: 컨테이너에게 보장하는 최소 자원의 양
limit
: 유휴 자원이 있을 때request
에 설정된 자원의 양을 포함하여 최대로 점유가능한 자원의 양
request
는 각 컨테이너에게 할당하는 최소한의 양이므로 request
의 총합이 노드의 총 자원의 양을 초과할 수 없다. 그래서 쿠버네티스 스케줄러는 파드를 생성할 때 파드 내에 컨테이너들의 총 request
만큼 자원이 놀고 있는 노드에게 파드를 생성한다. 즉, 파드를 생성할 때 기준은 limit
이 아닌 request
이다.CPU Request & Limit
파드에 cpu request, limit 이 실제로 어떻게 동작하는지 알아보자.
쿠버네티스 cpu limit = 도커
—-cpu-shares
쿠버네시트 cpu request = 도커
—-cpu
1. 다른 프로세스들과 CPU를 같이 사용해야할 때
위의 그림처럼 컨테이너 A, B, C 의
limit
이(cpu-shares) 1000m, 1000m, 500m 으로 되어있다면각각 2:2:1 의 비율로 CPU를 점유한다.
(위에 퍼센테이지는 파드 내에 컨테이너끼리 상대적인 비율이며, 다른 프로세스들과 CPU를 같이 쓰는 상황에서도 변하지 않는다. 대신 CPU time 에서 차지하는 절대적인 시간은 다른 프로세스들의 CPU 점유시간에 따라 달라지고 컨테이너의 성능이 낮아지거나 올라갈 수 있다.)
2. 유휴 CPU가 있을 때
하지만 2개 코어가 놀고 있고 컨테이너 C가 멈춰있다면 두 개 컨테이너 A, B가 CPU를 전부 점유할 수 있다.
쿠버네티스에서는 CPU 단위를 밀리코어(m)으로 나타내기 때문에
request
값 또한 m
으로 나타낸다. 이 값이 실제 cpu share 의 값으로 변환되는 식은 아래와 같다.(500m * 1024) / 1000 = 512 (실제로 컨테이너에 설정된 —cpu-shares 의 값)
3. CPU 경합이 발생할 때
이미
request
보다 더 많이 CPU 를 점유하고 있는데 다른 컨테이너에서 request
만큼 CPU를 사용하려고 하면 자기 request
보다 더 많이 쓰던 컨테이너에 쓰로틀(throttle)이 발생하고 request
에 설정된 단위까지 CPU 점유시간이 줄어든다.쿠버네티스에서는 CPU를 compressible resource 으로 간주한다.
이는 request 보다 많은 CPU를 사용하여 다른 컨테이너와 CPU 경합이 발생하더라도 쓰로틀을 걸어서 컨테이너의 CPU 점유시간을 조절할 수 있기 때문이다.
반면에 메모리는 incompressible resource 으로 간주해서 메모리 경합이 발생하면 우선순위가 낮은 컨테이너가 종료된다.
참고자료