Kubernetes in Action : Chapter14. 파드의 컴퓨팅 리소스 관리

    들어가기 전

    이 글은 쿠버네티스 인 액션 14장을 공부하며 작성한 글입니다. 


    14.1 파드 컨테이너의 리소스 요청

    파드를 생성할 때, 파드가 사용할 수 있는 자원 (CPU / 메모리)를 제한할 수 있다. 다음 두 가지 설정값을 통해 이것을 제공한다. 

    • request(요청) : 최소로 사용할 수 있는 자원
    • limit(제한) : 최대로 사용할 수 있는 자원 

    14.1.1 리소스 요청(Request)을 갖는 파드 생성하기 

    CPU Request를 지정하지 않으면, 컨테이너에서 실행중인 프로세스에 할당되는 CPU 시간을 신경쓰지 않는다는 말이다. 최악의 경우 프로세스는 CPU 시간을 전혀 할당받지 못할 수도 있다. 아래를 살펴보자.

    // 자원 request(요청)를 가진 Pod 생성 요청
    apiVersion: v1
    kind: Pod
    metadata:
      name: c14-1-pod
    spec:
      containers:
        - image: busybox
          command: ["dd", "if=/dev/zero", "of=/dev/null"]
          name: main
          resources:
            requests:
              cpu: 200m # 200 밀리 코어 요청( 1코어 = 1000m)
              memory: 10Mi # 10Mi 메모리 요청
              
    // 리눅스 머신의 CPU 코어 갯수 살펴보기
    $ lscpu | grep -i 'per socket'
    >>> 
    Core(s) per socket:              2
    
    // 파드의 CPU 점유 살펴보기
    $ kubectl exec -it c14-1-pod -- /bin/sh
    $ top
    >>>
    Mem: 2785676K used, 5211000K free, 5144K shrd, 69700K buff, 2041028K cached
    CPU: 29.2% usr 36.5% sys  0.0% nic 31.7% idle  0.0% io  0.0% irq  2.4% sirq
    Load average: 1.78 1.42 1.00 5/559 14
      PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
        1     0 root     R     3828  0.0   1 24.3 dd if /dev/zero of /dev/null
        7     0 root     S     3828  0.0   3  0.0 /bin/sh
       14     7 root     R     3828  0.0   0  0.0 top

    하나씩 살펴보자.

    1. 파드를 생성할 때, 최소 CPU를 200밀리코어를 요청임. (1코어 = 1000밀리코어)
    2. 현재 리눅스 머신의 코어 갯수는 2개임.
    3. 파드에 접속 후, CPU 점유율을 살펴봤더니 25%를 사용하고 있음.

    2코어의 25%는 500밀리코어다. 그렇지만 내가 파드를 생성할 때는 200밀리코어를 요청했다. 이것은 당연한 동작이다. 200밀리코어는 Request(요청)한 것은 최소한 이만큼은 달라는 것이다. 대신 최대 얼마까지 쓸 수 있는지에 대한 것은 작성하지 않았다. 이런 이유 때문에 파드는 500 밀리코어까지 값을 사용할 수 있는 것이다. 


    14.1.2 리소스 요청이 스케쥴링에 미치는 영향

    스케쥴러는 파드를 스케쥴링 할 때 사용하는 조건 함수에서 '리소스 요청(Resource Requests)'를 고려한다. Resource Requests로 요청된 최소량을 제공할 수 없는 노드는 조건 함수에 필터링 된다. 즉, 스케쥴링 대상이 되지 않는다. 


    파드가 특정 노드에 실행할 수 있는지 스케쥴러가 결정하는 방법

    스케쥴러는 조건 함수를 실행할 때 다음과 같이 동작한다. 

    • 스케쥴러는 Resource Requests를 고려할 때, 노드에 배포된 파드들의 리소스 요청량의 전체합만을 고려한다. 
    • 스케쥴러는 현재 각 파드가 실제로는 얼마나 사용하고 있는지는 고려하지 않는다. 

    노드에 배포된 파드가 현재는 Request보다 적은 자원을 사용하고 있을 수는 있다. 이 값을 보고 스케쥴러가 파드를 해당 노드에 스케쥴링 한다면, 이미 배포되었던 파드의 'Request'에 대한 보장은 깨지게 된다. 따라서 스케쥴러는 노드에 배포된 파드들의 Request의 전체 합만 바라본다. 

    위 그림을 살펴보면 된다. 스케쥴러가 새로운 파드 D를 스케쥴링 할 때, 스케쥴러는 노드 A에 배포된 파드의 전체 Resource Request의 합만을 고려해서 배포가 가능한지 판단한다.  


    스케쥴러가 파드를 위해 최적의 노드를 선택할 때, 파드의 요청을 사용하는 방법

    스케쥴러는 조건 함수가 끝난 후, 우선순위 함수를 이용해서 어떤 노드에 우선적으로 배포해야할지 계산한다. 이 때, 두 가지 타입의 우선순위 함수를 사용한다. 우선순위 함수 마찬가지로 실제 사용량은 고려하지 않고, 현재 Request된 값들의 합만 고려한다.

    • LeastRequestedPriority : 현재 리소스 할당이 가장 적게된 노드를 우선적으로 선택함.
    • MostRequestedPriority : 현재 리소스 할당이 가장 많은 노드를 우선적으로 선택함. 

    온프레미스 클라우드를 사용하는 경우라면 노드에 골고루 파드가 분배되는 것을 선호할 것이기 때문에 주로 LeastRequestedPriority를 사용한다. 반면 AWS 같은 클라우드 서비스를 사용하는 경우, 가능한 적은 노드를 사용하면서 비용 절감을 선호할 수도 있다. 이 때, MostRequestedPriority를 이용해서 같은 노드에 최대한 많이 배포하고, 남는 노드는 제거하는 형태가 될 수도 있다.  


    노드의 용량 검사

    스케쥴러가 스케쥴링을 하기 위해서는 다음 두 가지를 알아야 한다.

    • 현재 노드에 배포된 파드들의 Request, Limits의 값. 
    • 노드가 제공하는 컴퓨팅 파워 

    각 노드에 배포된 kubelet은 노드의 컴퓨팅 파워 정보를 API 서버에게 주기적으로 알려준다. 이 값들은 모두 etcd에 저장된다. 각 노드의 사용 가능한 컴퓨팅 파워는 kubelet 명령어로도 살펴볼 수 있다. 

     

    $ kubectl describe nodes ubuntu-worker1
    >>>
    Capacity:
      cpu:                4
      ephemeral-storage:  61270332Ki
      hugepages-1Gi:      0
      hugepages-2Mi:      0
      hugepages-32Mi:     0
      hugepages-64Ki:     0
      memory:             7996676Ki
      pods:               110
    Allocatable: // 스케쥴러가 보는 것.
      cpu:                4
      ephemeral-storage:  56466737878
      hugepages-1Gi:      0
      hugepages-2Mi:      0
      hugepages-32Mi:     0
      hugepages-64Ki:     0
      memory:             7894276Ki
      pods:               110

    제공하는 값은 Capacity, Allocatable이다. 각각은 다음을 의미한다

    • Capacity : 노드의 총 리소스를 나타냄. 특정 리소스는 쿠버네티스의 구성 요소로 스케쥴링 된 것일 수 있기 때문에 파드가 모두 사용 가능한 것은 아님.
    • Allocatable : 파드가 사용 가능한 자원.  

    이런 의미를 가지기 때문에 스케쥴러는 Allocatable 리소스를 기준으로 스케쥴링을 한다. 


    스케쥴링 되지 않는 파드

    노드가 배정할 수 있는 CPU보다 더 많은 CPU를 요청(request)하는 경우를 살펴보자. 이 경우에는 파드가 어떤 노드에 스케쥴링 되지 않을 것이다. 아래와 같이 파드를 만들고 배포를 해보자. 

    apiVersion: v1
    kind: Pod
    metadata:
      name: c14-3-pod
    spec:
      containers:
        - image: busybox
          command: ["dd", "if=/dev/zero", "of=/dev/null"]
          name: main
          resources:
            requests:
              cpu: "10" # 많은 CPU 요청.
              memory: 10Mi

    아래 상태를 살펴보자.

    1. 배포한 파드의 상태를 살펴보면 Pending이다. Pending인 이유를 살펴보자.
    2. Pending인 이유는 insufficient CPU(CPU가 불충분함)이라는 문구가 나온다. Request 조건을 만족할 수 있는 Pod가 없기 때문이다.
    3. 노드의 정보를 살펴보자. 현재 해당 노드에는 c14-1-pod라는 녀석(이전에 배포)이 있다. 그렇지만 뿐만 아니라 다른 네임 스페이스에는 쿠버네티스 기본 구성요소들이 배포되어있고, 이 녀석들 역시 CPU + Memory Requests를 하고 있는 상태다. 

    정리해보면 다음과 같다. 

    1. Request를 만족하는 노드가 없는 경우라면, Pod는 스케쥴링 되지 않고 Pending 상태가 된다.
    2. 노드 정보를 살펴보면, 내가 배포한 파드 말고 쿠버네티스의 기본 컴포넌트들이 배포되어서 리소스를 어느정도 점유하고 있다. 
    # 파드 상태 보기
    $ kubectl get pod
    NAME        READY   STATUS    RESTARTS   AGE
    c14-3-pod   0/1     Pending   0          46m
    
    # 파드 살펴보기
    $ kubectl describe pod c14-3-pod
    Events:
      Type     Reason            Age                  From               Message
      ----     ------            ----                 ----               -------
      Warning  FailedScheduling  4m41s (x9 over 44m)  default-scheduler  0/4 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 1 node(s) had untolerated taint {node.kubernetes.io/unreachable: }, 2 Insufficient cpu. preemption: 0/4 nodes are available: 2 No preemption victims found for incoming pod, 2 Preemption is not helpful for scheduling..
      
    # 워커 살펴보기
    $ kubectl describe node ubuntu-worker1  
      Non-terminated Pods:          (9 in total)
      Namespace                   Name                                                   CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
      ---------                   ----                                                   ------------  ----------  ---------------  -------------  ---
      kube-system                 calico-node-m6748                                      250m (6%)     0 (0%)      0 (0%)           0 (0%)         15d
      kube-system                 kube-proxy-wrkcf                                       0 (0%)        0 (0%)      0 (0%)           0 (0%)         15d
      longhorn-system             engine-image-ei-f9e7c473-84rrl                         0 (0%)        0 (0%)      0 (0%)           0 (0%)         14d
      longhorn-system             instance-manager-e-7780a16b00b054091a0729d84c907491    480m (12%)    0 (0%)      0 (0%)           0 (0%)         86m
      longhorn-system             instance-manager-r-7780a16b00b054091a0729d84c907491    480m (12%)    0 (0%)      0 (0%)           0 (0%)         86m
      longhorn-system             longhorn-csi-plugin-qnmzd                              0 (0%)        0 (0%)      0 (0%)           0 (0%)         14d
      longhorn-system             longhorn-manager-bzvzr                                 0 (0%)        0 (0%)      0 (0%)           0 (0%)         14d
      metallb-system              metallb-speaker-jwv6l                                  0 (0%)        0 (0%)      0 (0%)           0 (0%)         15d
      study                       c14-1-pod                                              200m (5%)     0 (0%)      10Mi (0%)        0 (0%)         81m
    Allocated resources:
      (Total limits may be over 100 percent, i.e., overcommitted.)
      Resource           Requests     Limits
      --------           --------     ------
      cpu                1410m (35%)  0 (0%)
      memory             10Mi (0%)    0 (0%)
      ephemeral-storage  0 (0%)       0 (0%)
      hugepages-1Gi      0 (0%)       0 (0%)
      hugepages-2Mi      0 (0%)       0 (0%)
      hugepages-32Mi     0 (0%)       0 (0%)
      hugepages-64Ki     0 (0%)       0 (0%)
    Events:              <none>

    14.1.3 CPU 요청이 CPU 시간 공유에 미치는 영향

    CPU Request만 있고, CPU Limit가 없는 상황이라면 CPU Request는 다음 동작을 관리한다.

    1. Request 한 것만큼의 자원이 있는 노드에 스케쥴링 한다. 
    2. 파드 간의 CPU 최대 사용량을 분배하는 기준이 된다. 

    2번은 무엇을 의미하는 것일까? 아래 그림으로 이해할 수 있다.

    1. Node A는 CPU 2 Cord를 가진다.
    2. 파드 A는 200m, 파드 B는 800m CPU 코어를 요청한다. 이는 1:4의 비율로 요청하는 것을 의미한다.

    만약 각 파드가 가능한한 최대의 CPU를 점유해야하는 상황이 온다면, 이 비율로 미할당된 CPU를 분배하기 시작한다. 현재 1코어를 점유하고 있다. 파드 A도 1코어가 더 필요하고, 파드 B도 1코어가 더 필요한 상황일 때, 남은 1코어를 1:4의 비율로 각 파드에 분배하기 시작한다. 결과적으로 파드 A는 200밀리코어, 파드 B는 800 밀리코어를 더 점유할 수 있게 된다. 


    14.2 컨테이너에 사용 가능한 리소스 제한 (Limit)

    요청(Request)는 파드에 필요한 최소한의 컴퓨팅 파워를 얻는 작업이었다. 제한(Limit)을 하면 파드가 사용할 수 있는 최대 컴퓨팅 파워를 제한할 수 있다. 


    14.2.1 컨테이너가 사용 가능한 리소스 양을 엄격한 제한으로 설정

    Limit을 이용한 특정 파드가 사용 가능한 CPU, 메모리 량을 제한할 수 있다. 각 컴퓨팅 파워는 조금 다르게 동작한다.

    • CPU : 압축 가능한 리소스. 
    • 메모리 : 압축 불가능한 리소스. 

    특정 파드에서 CPU를 너무 많이 사용하고 있는 경우, 다른 파드에 영향을 주지 않고 CPU 사용량을 안전하게 줄일 수 있다. 프로세스에 메모리가 할당되면, 프로세스가 메모리를 해제하지 않는 한 다른 파드가 가져갈 수 없다. 

    이것은 특정 파드에 적절한 메모리 제한이 걸려있지 않으면, 노드가 배정할 수 있는 메모리를 그 파드가 모두 할당받을 수 있음을 의미한다. 스케쥴러는 파드를 스케쥴링 할 때, etcd에 기록된 request / limit를 보고 배정하며, 실제 메모리 사용률은 신경쓰지 않는다. 

    그런데 Limit을 지정하지 않는 다는 것은 etcd에 기록되지 않은 채, 특정 파드의 메모리 사용률이 무한정 올라갈 수 있다는 것이다. 이렇게 되면, request / limit만 보고 스케쥴링한 노드에서 파드가 정상적으로 생성/동작하지 않을 수 있음을 의미한다.


    리소스 제한(Limit)을 갖는 파드 생성

    파드를 생성할 때, 사용가능한 컴퓨팅 파워를 'Limit'를 제한할 수 있다. 이를 통해 컴퓨팅 파워를 설정할 수 있다. 또한 Limit을 지정했을 때, Request가 없다면 Request는 자동적으로 Limit과 동일한 값으로 지정된다. 

    # 리소스 요청(Request)를 지정하지 않았음. 따라서 Request는 Limit과 동일하게 지정됨.
    apiVersion: v1
    kind: Pod
    metadata:
      name: c14-7-pod
    spec:
      containers:
        - name: main
          image: busybox
          command: ["dd", "if=/dev/zero", "of=/dev/null"]
          resources:
            limits:
              cpu: "1" # CPU 최대 사용 제한
              memory: 20Mi # 메모리 최대 사용 제한

    리소스 제한(Limit) 오버커밋

    리소스 요청(Request)와 리소스 제한(Limit)은 분배할 때 있어서 방식이 다르다.

    • 요청 : 노드의 사용 가능한 자원을 기준으로 봄. 따라서 오버커밋 불가능.
    • 제한 : 사용 가능한 자원을 기준으로 보지 않음. 따라서 오버커밋 가능함. 

    오버커밋은 가능하지만, 파드가 노드에서 분배 가능한 메모리보다 더 많은 메모리를 요구하는 경우라면 특정 파드는 제거되어야 한다. 14.3절에서 어떤 파드가 제거될지를 공부한다. 


    14.2.2 리소스 제한 초과

    기본적으로 리소스 제한(Limit)은 파드를 선언할 때 오버 커밋이 가능하다고 했다. 그렇다면 노드에서 배포된 파드 각각이노드가 제공 가능한 리소스보다 더 많은 리소스를 요청한다면? 

    • CPU : 압축 가능하다. 따라서 파드끼리 적절히 나눠서 사용한다
    • 메모리 : 압축 불가능하다. OOM Killed가 발생함.

    파드의 프로세스가 Limit보다 많은 메모리를 요구하면, 프로세스는 OOM(Out Of Memory)로 죽게 된다. 이 때 CrashLoopBackOff라는 상태로 보일 수도 있다. 

    $ kubectdl get pod 
    Name        READY    STATUS            RESTARTS    AGE
    memoryhog   0/1      CrashLoopBackOff  3           1m

    CrashLoopBack은 각 크래시 이후,  Kubelet이 파드를 재시작하는 간격이 늘어나는 것을 의미한다. CrashLoopBackOff일 때는 다음과 같이 동작한다.

    • 즉시 시작 → 10초 기다린 후 시작 → 20초 → 40초 → ... → 300초 → 300초 → ...

    여기서는 특정 파드에서 프로세스가 더 많은 메모리를 요청하는 경우 OOM이 발생한다고 했다. 그렇지만 실제로는 적절한 메모리를 사용하고 있어도 OOM으로 종료될 수도 있다. 이것은 14.3.2절에서 살펴볼 것이다. (QOS 때문임)


    14.2.3 컨테이너의 어플리케이션 제한을 바라보는 방법 

    아래 파드를 생성하고 Pod에 접속해서 top 명령어를 통해 현재 사용중인 시스템 리소스를 살펴보자. 아래 사항을 살펴보면 뭔가 이상할 것이다

    • 메모리 제한을 20Mi로 했음. 따라서 메모리 요청도 20Mi임.
    • 파드에서 실제로 사용중인 메모리는 3GB이며, 전체는 8GB임. 

    이것은 무슨 일이 발생한 것일까? 결론부터 이야기하면, 파드에서 바라보는 컴퓨팅 리소스는 '노드'가 제공하는 것을 기준으로 바라본다. 

    # 리소스 요청(Request)를 지정하지 않았음. 따라서 Request는 Limit과 동일하게 지정됨.
    apiVersion: v1
    kind: Pod
    metadata:
      name: c14-7-pod
    spec:
      containers:
        - name: main
          image: busybox
          command: ["dd", "if=/dev/zero", "of=/dev/null"]
          resources:
            limits:
              cpu: "1" # CPU 최대 사용 제한
              memory: 20Mi # 메모리 최대 사용 제한
    
    
    
    $ kubectl exec -it c14-7-pod top
    >>> # 사용중 메모리 3GB, 전체 메모리 8GB. 뭔가 이상함. 
    Mem: 3144424K used, 4852252K free, 5312K shrd, 93472K buff, 2332840K cached
    CPU: 11.3% usr 18.1% sys  0.0% nic 70.2% idle  0.2% io  0.0% irq  0.0% sirq
    Load average: 0.58 0.28 0.26 3/531 12
      PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
        1     0 root     R     3828  0.0   2 24.9 dd if /dev/zero of /dev/null
        6     0 root     R     3828  0.0   0  0.0 top

    컨테이너는 항상 노드의 컴퓨팅 파워를 바라봄. 

    앞서 있었던 문제는 '파드'는 항상 '노드'의 컴퓨팅 파워를 바라본다는 것에서 기인한다. 이것은 무엇을 의미할까? 우리가 파드를 생성할 때 Limit으로 값을 제한하는 것은 Pod를 생성할 때만 이용된다. 파드가 생성되고 난 이후에는 파드가 읽는 컴퓨팅 파워는 항상 '노드'기준이 된다. 이것은 어떤 문제를 초래할까? 

    • 사용 가능한 메모리를 읽어서 할당하는 어플리케이션
    • 사용 가능한 코어 갯수를 읽어서 할당하는 어플리케이션 

    이런 어플리케이션은 큰 문제를 야기할 수 있다. 

    메모리는 자바 어플리케이션과 관련된 문제가 있다. -Xms 옵션은 힙 메모리의 사이즈를 설정하는 옵션이다. 만약 이 옵션이 설정되어 있지 않다면, 자바 어플리케이션은 노드에서 사용 가능한 메모리를 기준으로 최대 힙 사이즈를 설정할 것이다. 만약 파드의 Limit이 2GB이고 위의 문제로 배정된 힙 메모리 사이즈가 4GB라면, 이 파드는 생성될 때 OOM 에러를 받게 될 것이다. 

    마찬가지로 노드의 코어 갯수를 쓰레드 갯수를 지정하는 어플리케이션 역시 큰 문제를 가져올 수 있다. 노드에 64개의 코어가 있으며, 파드에 1개의 코어를 배정한 경우를 가정해보자. 그런데 실제로 쓰레드는 1개만 만들어져야 하지만, 파드의 어플리케이션은 64개의 코어가 있는 것으로 판단하고 64개의 쓰레드를 만들 수 있다. 

    많은 쓰레드가 생성되어 메모리 문제가 발생할 수 있으며, 64개의 쓰레드가 1개의 코어를 사용하기 위해서 계속 경합하며 데드락이 발생하는 문제가 발생할 수 있다. 이런 경우라면 Downward API 같은 것을 이용해서 Limit의 값을 전달한 다음, 어플리케이션이 이 값을 읽고 동작하도록 하는 것이 필요할 수 있다. 


    14.3 파드 QoS 클래스 이해

    리소스 제한(Limit)은 오버커밋이 가능하다. 여기서 CPU는 오버커밋의 상황에 다다르더라도, 비율만큼 CPU 시간을 나눠서 사용한다. 하지만 메모리는 CPU 처럼 사용할 수 없기 때문에 파드가 종료되며 메모리를 해제해야만 한다. 그렇다면 어떤 파드가 우선순위를 가질까? 이것은 Qos(Quality Of Sevice)로 정해진다.

    • BestEffort (최하위 우선순위)
    • Burstable 
    • Guaranteed (최상위 우선순위) 

    14.3.1 파드의 QoS 클래스 정의

    QoS 클래스는 Request / Limit의 조합에서 파생된다. 

    BestEffort 클래스

    • BestEffort는 최하위 우선순위임. 
    • 파드에 선언된 컨테이너 중에 Request + Limit이 하나도 없는 경우임. 
    • 만약 메모리가 필요할 때 가장 먼저 제거됨. 반면 Limit이 없으니, 메모리가 충분할 때는 메모리를 많이 사용할 수도 있음. 

     

    Guaranteed 클래스

    • Guaranteed는 최상위 클래스
    • 다음 세 가지 조건을 모두 만족해야 함. 
      • CPU / 메모리에 모드 리소스 Request / Limit이 설정되어야 함. 
      • 리소스 Request / Limit이 모두 동일해야 함. 
      • 파드에 있는 모든 컨테이너에 설정되어야 함. 

    모든 컨테이너에 Limit만 설정되어 있는 경우, Guranteed 클래스가 된다. Limit만 설정되어 있으면, Request는 자동으로 Limit을 따라간다. 그렇기 때문에 위 세 가지 조건을 모두 만족하기 때문에 Guranteed 클래스가 됨. 

     

    Busrtable QoS 클래스

    • BestEffort / Guaranteed 클래스에 속하지 않는 경우임. 

     

     

    QoS 클래스 파악

    처음부터 파드 수준으로 QoS를 파악하려고 하면 어렵다. 따라서 먼저 컨테이너 수준 별로 따진 후, 멀티 컨테이너 수준으로 따지는게 좋다. 

    CPU 요청 대 제한 메모리 요청 대 제한 컨테이너 QoS 클래스
    미설정 미설정 BestEffort
    요청 < 제한 Burstable
    요청 = 제한 Burstable
    요청 < 제한 미설정 Burstable
    요청 < 제한 Burstable
    요청 = 제한  Burstable
    요청 = 제한 미설정 Burstable
    요청 < 제한 Burstable
    요청 = 제한 Guaranteed

    먼저 각 컨테이너의 QoS를 다음과 같이 살펴본다. 이렇게 하나의 컨테이너에 대한 QoS를 살펴보았으면, 이제 다중 컨테이너의 QoS를 살펴보면 된다. 

    컨테이너 1 QoS 클래스 컨테이너 2 QoS 클래스 파드 QoS 클래스
    BestEffort BestEffort BestEffort
    Burstable Burstable
    Guaranteed Burstable
    Burstable BestEffort Burstable
    Burstable Burstable
    Guaranteed Burstable
    Guaranteed BestEffort Burstable
    Burstable Burstable
    Guaranteed Guaranteed

    위의 표는 살짝 복잡할 수도 있다. 파드 레벨로 살펴보면 다음만 기억하면 된다.

    • 모든 컨테이너가 BestEffort → 파드는 BestEffort
    • 모든 컨테이너가 Guaranteed → 파드는 Guaranteed 
    • 나머지는 Burstable 

    14.3.2 메모리가 부족할 때 어떤 프로세스가 종료되는지 이해

    쿠버네티스는 메모리가 부족할 때, 파드를 하나씩 먼저 종료시킨다. 그렇다면 어떤 파드부터 종료가 될 것인가?

    • 서로 다른 QoS를 가진 경우
      • BestEffort < Burstable < Guaranteed 순으로 제거됨. BestEffort가 가장 먼저 제거 됨. 
    • 같은 QoS를 가진 경우
      • PriorityClass + 생성 시간을 고려함.
      • PriorityClass가 낮은 우선순위를 가진 파드부터 제거됨.
      • 같은 PriorityClass를 가진 경우, 가장 오래된 파드가 먼저 제거됨

    14.4 네임스페이스별 파드에 대한 기본 요청과 제한 설정

    네임 스페이스별로 파드에 대한 기본 요청 / 제한의 최소 / 최대값을 설정하고 싶은 경우가 있을 수 있다. 또한, 요청값이 설정되지 않은 녀석들에게 기본값을 설정해주고 싶은 경우도 있다. 이럴 때, LimitRange라는 오브젝트를 사용할 수 있다. 

     


    14.4.1 LimitRange 리소스 소개

    LimitRange는 쿠버네티스에서 제공하는 오브젝트이며 다음 기능을 제공한다

    • 파드별 요청/제한의 값을 설정할 수 있음. (필터링 기능)
    • 컨테이너별 요청/제한의 값을 설정할 수 있음. (필터링 기능)
    • PVC의 요청 / 제한값을 설정할 수 있음. (필터링 기능)
    • 파드별 / 컨테이너별 기본 요청 / 제한 값을 설정할 수 있음. (기본값 제공)

    만약 LimitRange 오브젝트가 등록되면 API 서버에게 파드 생성 요청이 왔을 때, LimitRanger 어드미션 컨트롤 플러그인을 통해 검증된다. 다음 순서로 동작한다.

    1. 생성 요청온 파드의 Limit / Request 값이 LimitRange 오브젝트에 선언된 범위 안에 들어오는지 확인 (필터링). 안되면 생성 하지 않음. 
    2. 생성 요청온 파드에 Limit / Request 기본 값이 없으면, LimitRange 오브젝트에 선언된 Default 값으로 선언해 줌.

    LimitRange가 생성되면 기존에 생성되어있던 파드에는 영향을 미치지 않는다. 이것은 LimitRange가 API 서버의 Admission Plugin으로 동작하기 때문이다. 


    14.4.2 LimitRange 오브젝트 생성하기

    LimitRange 오브젝트는 아래와 같이 선언할 수 있다. 사용 가능한 내용은 다음과 같다. 

    • 파드의 Limit, Request의 최소 / 최대값 설정
    • 컨테이너의 Limit, Request의 최소 / 최대 범위 설정
    • 파드, 컨테이너의 Limit, Request 기본값 설정.
    • 파드, 컨테이너의 Limit, Request 최대 비율 
    • PV의 범위 
    apiVersion: v1
    kind: LimitRange
    metadata:
      name: c-14-10-limit-range
    spec:
      limits:
        - type: Pod
          min:
            cpu: 50m
            memory: 50Mi
          max:
            cpu: "1"
            memory: 1Gi
        - type: Container
          # Request를 지정하지 않은 경우, 기본 Request
          defaultRequest:
            cpu: 100m
            memory: 10Mi
          # Limit를 지원하지 않은 경우, 기본 Limit
          default:
            cpu: 200m
            memory: 100Mi
          min:
            cpu: 50m
            memory: 5Mi
          max:
            cpu: "1"
            memory: 1Gi
          # 각 리소스의 (Reqeust / Limit)의 최대 비율
          maxLimitRequestRatio:
            cpu: "4"
            memory: "10"
        - type: PersistentVolume
          min:
            storage: 1Gi
          max:
            storage: 10Gi

    14.4.3 강제 리소스 제한

    LimitRange를 네임 스페이스에 배포한 후, LimitRange를 벗어나는 파드 생성을 요청해보자. 어떻게 동작할까? 

    # limits-pod-too-big.yaml
    resources:
      requests:
        cpu: "2"

    컨테이너 1개를 가진 파드 하나를 생성한다고 해보자. 이 때, 컨테이너 1개는 CPU 코어 2개를 요청한다. 그러면 어떻게 될까? 

    $ kubectl create -f limits-pod-too-big.yaml
    >>>
    Error from server (Forbidden): error when creating "limits-pod-too-big.yaml": 
    pods "too-big" is forbidden: [
      maximum cpu usage per Pod is 1, but request is 2.,
      maximum cpu usage per container is 1, but request is 2.]

    API 서버에 요청을 보내면, API 서버는 요청을 Adminssion Plugin을 통해서 필요한 조건을 만족하는지 확인한다. 그런데 이 때, LimitRange 어드미션 플러그인에서 받아들일 수 없는 요청인 것을 알기 때문에 API 서버는 파드의 생성 요청을 거부한다. 즉, 파드 생성 요청 자체가 etcd에 기록되지 않고 클라이언트는 다음 응답을 받게 된다. 


    14.4.4 Default 리소스 요청과 제한 적용

    LimitRange 오브젝트에는 Default Request / Limit을 설정하는 것도 존재한다고 했다. 파드를 생성할 때 request / limit을 설정하지 않으면 LimitRanger 어드미션 플러그인을 통과할 때, LimitRange에 선언된 기본값을 바탕으로 파드가 셋팅된다. 


    14.5 네임스페이스의 사용 가능한 총 리소스 제한하기 

    LimitRange는 파드 1개에만 적용되는 오브젝트다. 만약 사용자가 파드를 수백 개 만든다면, 클러스터에서 사용 가능한 리소스를 모두 사용해 버릴 수 있다. 쿠버네티스는 특정 네임 스페이스에서 사용 가능한 전체 Resource 제한을  ResourceQuota라는 기능을 통해 해결한다. 


    14.5.1 ResourceQuota 오브젝트 소개

    ResourceQuota는 특정 네임스페이스에 사용할 수 있는 자원의 양을 제한한다. 구체적으로는 다음을 제한한다. 

    • 컴퓨팅 리소스 양.
    • PVC가 사용할 수 있는 총 용량.
    • 네임 스페이스에서 사용할 수 있는 Pod, PVC, API 오브젝트의 숫자 

    ResourceQuota 오브젝트는 LimitRange와 마찬가지로 ResourceQuota 어드미션 플러그인을 통해 기능한다. 이 말은, ResourceQuota 오브젝트가 생성된 이후에 만들어지려는 녀석들에게만 적용된다는 것이다. 기존 파드가 ResourceQuota를 초과해서 사용하고 있다고 하더라도 해당 Pod가 Evict 되거나 하지는 않는다. 


    CPU + 메모리 Quota 설정 → 파드 생성 시, 값 설정 필요함.

    아래처럼 Yaml 파일을 작성해서 배포하면, 네임스페이스에서 동작하는 ResourceQuota가 생성된다. 여기서는 네임 스페이스에서 사용할 수 있는 Cpu, Memory의 request, limit의 총합을 지정할 수 있다. 

    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: c14-13
    spec:
      hard:
        requests.cpu: 400m
        requests.memory: 200Mi
        limits.cpu: 600m
        limits.memory: 500Mi

    그런데 한 가지 주의해야할 점은 ResourceQuota에서 설정한 값이 있다면, 파드를 생성할 때 항상 그 값을 채워줘야한다. 예를 들어 위에서 cpu, memory에 대해 request, limits를 설정했기 때문에 다음부터 파드를 생성한다면 반드시 아래의 예시처럼 request, limit를 설정해야한다. 

    그 이유는 네임 스페이스에서 각 컴퓨팅 파워에 대해서 request, limit이 얼마나 사용되었는지를 쿠버네티스가 판별해야한다. 그런데 request, limit이 없는 파드가 요청되고 그게 배포된다면, 네임 스페이스에서 사용하고 있는 request, limit의 값을 정확하게 관리할 수 없기 때문이다. 

    apiVersion: v1
    kind: Pod
    metadata:
      name: c14-13-pod
    spec:
      containers:
        - name: main
          image: busybox
          command: ["dd", "if=/dev/zero", "of=/dev/null"]
          resources:
            requests:
              cpu: 20m
              memory: 20Mi
            limits:
              cpu: 20m
              memory: 20Mi

    만약 ResourceQuota에 명시된 값이 있고, 파드를 생성할 때 이 값과 관련된 항목들을 명시하지 않았다면 아래와 같은 에러를 보게 된다. 

    Error from server (Forbidden): error when creating "p.yaml": pods "c14-13-pod" is forbidden: failed quota: c14-13: must specify limits.cpu for: main; limits.memory for: main; requests.cpu for: main; requests.memory for: main

    14.5.2 Persistent 스토리지에 관한 쿼터 지정하기

    쿠버네티스는 ResourceQuota에서 Persistent 스토리지에 관한 쿼터를 지정할 수 있다. PVC는 여러 StorageClass를 이용할 수 있기 때문에 ResourceQuota에서 Persistent 스토리지 쿼터를 지정할 때는 StorageClass 별로 지정할 수 있다.

    • 전체 요청 가능한 스토리지 용량의 총합
    • StorageClass 별로 요청 가능한 용량 총합 지정 
    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: c14-15
    spec:
      hard:
        request.storage: 500Gi # 요청 가능한 스토리지의 전체 용량
        ssd.storageclass.storage.k8s.io/requests.storage: 300Gi # SSD 스토리지 클래스에서 요청 가능한 스토리지 용량
        standard.storageclass.storage.k8s.io/requests.storage: 1Ti # HDD 스토리지 클래스에서 요청 가능한 스토리지 용량

    14.5.3 생성 가능한 오브젝트 수 제한

    ResourceQuota는 생성 가능한 리소스의 갯수도 제한할 수 있다. 예를 들어 다음이 제한 가능하다. 아직까지는 Deployment, StatefulSet은 지원이 안된다고 한다. 

    • Pod
    • Secret
    • ConfigMap
    • PVC
    • Service
    • 로드밸런서 서비스
    • 노드포트 서비스 

    그리고 다음과 같은 형태로 선언할 수 있다. 

    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: c14-15
    spec:
      hard:
        pods: "10"
        replicationcontrollers: "5"
        secrets: "10"
        configmaps: "10"
        persistentvolumeclaims: "4"
        services: "5"
        services.loadbalancers: "1"
        services.nodeports: "2"
        ssd.storageclass.storage.k8s.io/persistentvolumeclaims: "2"

    14.5.4 특정 파드 상태나 QoS 클래스에 대한 쿼터 지정

    ResourceQuota를 지정할 때, 특정 QoS 클래스에만 지정될 수 있도록 Scope을 지정할 수 있다. 현재 지원하는 Scope은 총 4가지다. 

    • BestEffort : BestEffort QoS 클래스를 가진 파드에만 지정
    • NotBestEffort : Burstable, Guaranteed QoS 클래스를 가진 파드에만 지정
    • Terminating : 파드 스펙의 activeDeadlineSeconds가 적용된 파드.
    • NotTerminating : activeDeaelineSeconds가 적용되지 않은 파드. 

    Scope은 여러 곳에도 지정할 수 있고, 여러 Scope에 지정한다면 And 조건을 가지고 동작한다. 아래 예시를 살펴보면 BestEffort QoS 파드 중에서 activeDeadlineSeconds가 지정되지 않은 그룹은 10개의 파드만 존재할 수 있다.

    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: c14-17
    spec:
      scopes:
        - BestEffort
        - NotTerminating
      hard:
        pods: "10"
        

    요약

    • 리소스 요청(Request)를 하지 않으면, 최소한의 자원도 분배받지 못할 수 있다. 
    • 컨테이너에 접속해서 top을 했을 때 보이는 값들은 노드 기준이다. 
    • CPU Limit이 정해져있지 않을 때, 각 파드가 최대로 사용가능한 CPU는 각 파드의 request 비율로 결정된다. 
    • Limit을 지정하고 Request를 지정하지 않았을 때, Request의 값은 Limit의 값과 같아짐. 
    • Request는 오버커밋 불가능. Limit는 오버커밋 가능.
    • CPU는 초과 사용해도 나눠서 사용하기 때문에 문제 없음. 메모리는 초과 사용가능한 경우, 몇몇 파드가 제거 되어야 함. 
    • CrahsLoopBackOff는 Kubelet이 파드를 시작하는 간격을 점차 늘려가면서 재시도 하는 상태임 (0 → 10 → 20 → 40 → ... 300 → 300...)
    • CPU Limit이 하는 일은 파드가 사용할 수 있는 CPU 시간을 제한하는 것이다. 예를 들어 노드가 64개의 코어, Limit을 1로 하면, 이 파드는 최대 1/64시간만큼 CPU를 점유할 수 있음.
    • 파드가 OOM으로 죽는 경우
      • 파드의 프로세스가 요청한 메모리가 Limit보다 큰 경우
      • 다른 파드가 많은 메모리를 요청해서, QOS에서 특정 파드가 죽는 것으로 결정된 경우.
    • 컨테이너는 항상 노드의 컴퓨팅 파워를 바라본다.
      • 파드가 생성 + 스케쥴링 할 때는 request, limit 기준으로 생성됨.
      • 생성된 파드에서 Top을 했을 때는 노드 기준으로 바라봄. 파드에서 실행되는 어플리케이션은 request, Limit 기준이 아니라 노드 기준의 컴퓨팅 파워를 바라볼 수 있고 이게 문제를 야기할 수 있음. 
    • LimitRange를 이용해 네임스페이스별로 파드 / 컨테이너 / PV의 컴퓨팅 파워 사용량을 제한할 수 있음. 
      • LimitRange는 이미 생성된 파드들에는 영향을 주지 않음. (API 서버를 통해서 동작하기 때문임) 
    • LimitRange는 파드 단위로 적용된다.
      • 만약 수십, 수백 개의 파드를 만든다면 클러스터에서 사용 가능한 모든 리소스를 써버릴 수 있음. 
    • ResourceQuota는 네임 스페이스 단위로 적용된다.
      • 사용 가능한 컴퓨팅 파워를 지정할 수 있음.
      • 생성될 수 있는 오브젝트의 갯수를 지정할 수 있음. 
    • 리소스 limit을 지정하면, 파드가 다른 파드의 리소스를 고갈시키지 않음.

    댓글

    Designed by JB FACTORY