티스토리 뷰

카테고리 없음

11-app-setting

효팍이 2022. 7. 18. 08:08
728x90
반응형

포드의 자원 사용량 제한

쿠버네티스는 컴퓨팅 자원을 컨테이너에 할당하기 위한 여러 기능을 제공합니다. 이 블로깅에서는 포드나 컨테이너에 CPU, 메모리 등의 자원을 할당하는 기본적인 방법을 먼저 알아보고, 쿠버네티스 클러스터 자원의 활용률을 높이기 위한 오버커밋(Overcommit) 방법을 설명합니다. 다음 ResourceQuota와 LimitRanger라는 쿠버네티스 오브젝트의 사용 방법을 다룹니다.

컨테이너와 포드의 자원 사용량 제한: Limit

쿠버네티스는 기본적으로 도커를 컨테이너 런타임으로 사용하기 때문에 포드를 생성할 때 docker와 동일한 원리로 CPU, 메모리의 최대 사용량을 제한할 수 있습니다.

# vi resource-limit-pod.yaml
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"
$ kubectl apply -f resource-limit-pod.yaml

포드를 정의하는 스펙에 새롭게 spec.containers.resources.limits 항목을 정의했습니다. memory 항목에는 256Mi를 입력했는데, 이 설정값은 도커 명령어에서 docker run --memory 256m과 같습니다. 즉 이 포드의 컨테이너 최대 메모리 사용량은 256Mi로 제한됩니다.

cpu에는 1개의 CPU를 뜻하는 1000m(밀리코어)라는 값을 입력했으며, 이는 도커 명령어에서 docker run --cpus 1과 같습니다. 따라서 이 포드의 컨테이너는 최대 1개 CPU만큼의 사이클을 사용할 수 있습니다.

노드의 가용 자원을 간단하게 확인해봅시다.

# 어디 노드에 배포 됐는지 확인
$ kubectl get po -o wide

$ kubectl describe node {배포된 노드 이름}

example



컨테이너와 포드의 자원 사용량 제한: Request

쿠버네티스의 자원 관리에서 Requests는 '적어도 이 만큼의 자원은 컨테이너에게 보장돼야 한다'는 것을 의미합니다. Requests는 쿠버네티스에서 자원의 오버커밋(Overcommit)을 가능하게 만드는 역할을 합니다.

오버커밋은 한정된 컴퓨팅 자원을 효율적으로 사용하기 위한 방법으로, 사용할 수 있는 자원보다 더 많은 양을 가상 머신이나 컨테이너에게 할당함으로써 전체 자원의 사용률(Utilization)을 높이는 방법입니다. 서버의 메모리 용량이 1GB밖에 되지 않더라도 메모리 제한이 750MB인 포드를 두 개 생성할 수 있다고 생각할 수 있습니다. 쿠버네티스에서는 이러한 자원 제한 설정을 Limit라고 부릅니다. 방금 생성했던 포드의 Limit 값이 이에 해당합니다.

example

다른 방향으로는 컨테이너가 하나가 500MB를 사용하고 있을 때 다른 컨테이너가 750MB를 사용하려 시도하는 상황을 방지하기 위해 각 컨테이너가 '사용을 보장 받을 수 있는 경계선'을 정해야 합니다. 이러한 경계선을 쿠버네티스에서는 Request라고 부릅니다. 즉, Request는 컨테이너가 최소한으로 보장받아야 하는 자원의 양을 뜻합니다.

example

포드에 설정될 Request 자원의 값은 Limit처럼 YAML 파일에서 정의할 수 있습니다.

vi resource-limit-with-request-pod.yamlx
apiVersion: v1
kind: Pod
metadata:
  name: resource-limit-with-requests-pod
  labels:
    name: resource-limit-with-requests-pod
spec:
  containers:
  - name: nginx
    image: nginx:latest
    resources:
      limits:
        memory: "256Mi"
        cpu: "1000m"
      requests:
        memory: "128Mi"
        cpu: "500m"

requests는 컨테이너가 보장받아야 하는 최소한의 자원을 뜻하기 때문에 노드의 총 자원의 크기보다 더 많은 양의 requests를 할당할 수는 없습니다. 따라서 쿠버네티스의 스케줄러는 포드의 requests만큼 여유가 있는 노드를 선택해 포드를 생성합니다. 즉, 포드를 할당할 때 사용되는 자원 할당 기준은 limit이 아닌 requests입니다.

example



CPU 자원 사용량의 제한 원리

CPU의 Limit을 의미하는 resources.limits.cpu를 설정해 CPU가 보장받아야 하는 최소한의 CPU 자원을 설정하면 docker run의 --cpu-shares옵션의 설정이 적용되게 됩니다.

--cpu-shares 옵션과 --cpus(Limit) 옵션을 함께 사용하면 CPU 자원에 오버커밋을 적용할 수 있습니다.

이번에는 Request보다 더 많은 CPU 자원을 사용하려 할 때, 즉 자원의 경합(Contention)이 발생하는 상황을 생각해봅시다. 다른 컨테이너가 CPU를 사용하고 있지 않아 유휴 CPU 자원이 발생한다면 다른 컨테이너는 Limit에 설정된 CPU 값만큼 사용할 수 있습니다.

example

컨테이너 A가 Request보다 더 많은 CPU를 사용하고 있는데, 컨테이너 B가 Request만큼 CPU를 사용하려고 시도하면 컨테이너 A에는 CPU 스로틀이 발생합니다.

아래와 같이 Request에 할당되지 않은 여유 CPU 자원이 노드에 남아있는 경우도 생각해 볼 수 있습니다. Limit만큼의 CPU를 사용할 수 있는 상황이라면 컨테이너는 Limit까지 CPU를 사용할 수 있습니다.

example

하지만 위 그림과 같이 두 컨테이너가 동시에 CPU를 최대한 사용하려고 시도한다면 남은 자원 또한 Request(--cpu-shares)의 비율에 맞춰서 각 컨테이너가 나눠 사용합니다.

정리하자면 포드의 컨테이너는 최대 Limit만큼의 자원을 사용할 수 있지만, 최소한 Request 만큼의 자원을 사용할 수 있도록 보장받습니다. 이 때 Request보다 더 많은 자원을 사용하는 것을 오버커밋이라고 부르며, Request를 넘어서 자원을 사용하려 시도하다가 다른 컨테이너와 자원 사용이 충돌하게 되면 CPU 스로틀과 같은 원리에 의해 자원 사용이 실패할 수 있습니다. CPU의 사용량에 경합이 발생하면 일시적으로 컨테이너 내부의 프로세스에 CPU 스로틀이 걸릴 뿐, 컨테이너 자체에는 큰 문제가 발생하지 않습니다.



QoS 클래스와 메모리 자원 사용량 제한 원리

프로세스의 메모리는 이미 데이터가 메모리에 적재돼 있기 때문에 CPU와 달리 메모리는 압축 불가능한 자원으로 취급됩니다. 따라서 하나의 노드에서 여러 개의 컨테이너가 Request보다 더 많은 자원을 사용하려고 시도해도 이미 메모리에 적재된 데이터를 압축할 수는 없습니다. 이러한 상황에서 쿠버네티스는 가용 메모리를 확보하기 위해 우선순위가 낮은 포드 또는 프로세스를 강제로 종료하도록 설계돼 있습니다. 강제로 종료된 포드는 다른 노드로 옮겨가게 되는데, 쿠버네티스에서는 이를 퇴거(Eviction)라고 표현합니다.

example

노드에 메모리 자원이 부족해지면 쿠버네티스는 포드의 컨테이너에 설정된 Limit과 Request의 값에 따라 내부적으로 우선순위를 계산합니다. 그뿐만 아니라 쿠버네티스는 포드의 우선순위를 구분하기 위해 3가지 종류의 QoS(Quality Of Service) 클래스를 명시적으로 포드에 설정합니다.



쿠버네티스에서의 메모리 부족과 OOM(Out of Memory)

쿠버네티스의 노드에는 각종 노드의 이상 상태 정보를 의미하는 Conditions라는 값이 존재합니다. 이 값은 kubectl describe nodes 명령어로도 확인할 수 있습니다. 이 값들은 쿠버네티스 에이전트인 kubelet은 노드의 자원 상태를 주기적으로 확인하면서 Conditions의 MemoryPressure, DiskPressure 등의 값을 갱신합니다.

$ kubectl describe nodes | grep -A9 Conditions

MemoryPressure는 기본적으로 노드의 가용 메모리가 100Mi 이하일 때 발생하도록 kubelet에 설정돼 있습니다. MemoryPressure가 발생하면 쿠버테티스는 해당 노드에서 실행 중이던 모든 포드에 대해 순위를 매긴 다음, 가장 우선순위가 낮은 포드를 다른 노드로 퇴거(Evict)시킵니다. 그뿐만 아니라 MemoryPressure의 값이 True인 노드에는 더 이상 포드를 할당하지 않습니다. 이 때 포드의 우선순위는 뒤에서 설명할 QoS 클래스 및 메모리 사용량에 따라 정렬되어 매겨집니다.

MemoryPressure와 같은 이상 상태를 감지하기 위한 임계치를 Hard Eviction Threshold라고 부르며, kubelet의 실행 옵션에서 설정값을 변경할 수도 있습니다. 또 다른 예시로는 DiskPressure가 있으며, DiskPressure의 상태가 True가 되면 쿠버네티스는 사용 중이지 않은 도커 이미지를 자동으로 삭제하기도 합니다.

만약 kubelet이 MemoryPressure 상태를 감지하기 전에 급작스럽게 메모리 사용량이 많아질 경우, 리눅스 시스템의 OOM(Out of Memory) Killer라는 기능이 우선순위 점수가 낮은 컨테이너의 프로세스를 강제로 종료해 사용 가능한 메모리를 확보할 수도 있습니다. OOM의 우선순위 점수에는 두 가지가 있는데, 첫 번째는 oom_score_adj이고, 두 번째는 oom_score 입니다. OOM Killer는 oom_score의 값에 따라서 종료할 프로세스를 선정합니다.

OOM Killer는 리눅스에 기본적으로 내장된 기능이기 때문에 아무것도 설정하지 않아도 모든 프로세스에 자동으로 OOM 점수가 매겨집니다. OOM 점수가 높으면 높을수록 강제로 종료될 가능성이 커지기 때문에 절대로 종료되지 말아야 하는 핵심 프로세스는 일반적으로 매우 낮은 값을 부여받습니다.

쿠버네티스를 설치함으로써 실행되는 도커 데몬은 기본적으로 기본 OOM 점수가 -999로 설정돼 있습니다. 이 점수는 쿠버네티스의 노드 중 하나에 접속하면 확인할 수 있습니다.

$ ps aux | grep dockerd
$ cat /proc/{dockerd 프로세스 번호}/oom_score_adj



QoS 클래스의 종류 - (1) Guaranteed 클래스

포드의 Limit과 Request 값에 따라서 'QoS 클래스'라는 것을 모든 포드에 대해서 설정합니다. QoS 클래스에는 BestEffort, Burstable, Guaranteed 총 3가지 종류가 있습니다.

$ kubectl describe po resource-limit-pod | grep QoS
QoS Class:                   Guaranteed

yaml에서 Requests 없이 Limit만 정의하면 Requests의 값 또한 Limit으로 동일하게 설정되고, Requests와 Limit의 값이 동일하면 Guaranteed 클래스로 분류됩니다. Guaranteed 클래스는 자원의 사용을 안정적으로 보장 받을 수 있습니다.

Guaranteed 클래스의 포드 내부에서 실행되는 프로세스들은 모두 기본 OOM 점수(oom_score_adj)가 -998로 설정됩니다. 도커 데본이나 kubelet의 프로세스와 거의 동일한 레벨로 프로세스가 보호받기 때문에 노드에서 메모리가 고갈되더라도 시스템 컴포넌트가 요구하지 않는 한 Guaranteed 클래스의 포드나 프로세스가 강제로 종료되는 일은 없습니다.

포드 내에 컨테이너가 여러 개 존재한다면 모든 컨테이너의 Request와 Limit의 값이 완전히 같아야만 포드가 Guaranteed 클래스로 분류됩니다.



QoS 클래스의 종류 - (2) BestEffort 클래스

BestEffort는 Request와 Limit을 아예 설정하지 않은 포드에 설정되는 클래스입니다. BestEffort 클래스의 포드는 Limit 값을 설정하지 않았기 때문에 노드에 유휴 자원이 있다면 제한 없이 모든 자원을 사용할 수 있습니다. 그렇지만 Request 또한 설정하지 않았기 때문에 BestEffort 클래스의 포드는 사용을 보장받을 수 있는 자원이 존재하지 않습니다.



QoS 클래스의 종료 - (3) Burstable 클래스

Burstable 클래스는 Request와 Limit이 설정돼 있지만, Limit의 값이 Request보다 큰 포드를 의미합니다.



Qos 클래스와 메모리 부족

kubelet이 메모리가 부족한 상황을 감지하면 우선순위가 가장 낮은 포드를 종료한 뒤 다른 노드로 내쫓아내는 퇴거(Evict)를 수행합니다. 메모리 사용량이 갑작스럽게 높아지면 리눅스의 OOM Killer는 OOM 점수가 가장 낮은 컨테이너의 프로세스를 강제로 종료할 수도 있습니다. OOM Killer에 의해포드 컨테이너의 프로세스가 종료되면 해당 컨테이너는 포드의 재시작 정책(restartPolicy)에 의해 다시 시작됩니다.

기본적으로 포드의 우선순위는 Guaranteed가 가장 높으며, 그 뒤로 Burstable와 BestEffort순입니다.



ResourceQuota와 LimitRange

ResourceQuota로 자원 사용량 제한

ResourceQuota는 특정 네임스페이스에서 사용할 수 있는 자원 사용량의 합을 제한할 수 있는 쿠버네티스 오브젝트입니다.

  • 네임스페이스에서 할당할 수 있는 자원의 총합을 제한할 수 있습니다.
  • 네임스페이스에서 생성할 수 있는 리소스의 개수를 제한할 수 있습니다.

ResourceQuota는 네임스페이스에 종속되는 오브젝트이기 때문에 네임스페이스별로 ResourceQuota 리소스를 생성해야 합니다.

default 네임스페이스의 CPU와 메모리의 Request, Limit을 제한하는 간단한 ResourecQuota를 생성해 보겠습니다.

# vi resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: resource-quota-example
  namespace: default
spec:
  hard:
    requests.cpu: "1000m"
    requests.memory: "500Mi"
    limits.cpu: "1500m"
    limits.memory: "1000Mi"

ResourceQuota의 정보에는 현재 default 네임스페이스에 생성된 포드들의 자원 할당량 합이 출력됩니다. 새롭게 생성되는 포드가 한계치보다 더 많은 자원을 사용하려고 하면 포드를 생성하는 API 요청은 실패합니다.

# vi more-memory-nginx-pod.yaml
apiVersion: v1
kind: Pod
metatdata:
  name: more-memory-nginx-pod
  # namespace: henry-playground
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        limits:
          memory: "1500Mi"
          cpu: "200m"
        requests:
          memory: "300Mi"
          cpu: "200m"

한계치보다 더 많은 자원을 사용하는 디플로이먼트를 사용하면 정상적으로 생성은 되지만 포드는 생기지 않습니다. 왜냐면 포드를 생성하는 주체는 디플로이먼트가 아니라 레플리카셋이기 때문입니다.

# vi more-memory-nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-over-memory
  # namespace: henry-playground
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          resources:
            limits:
              memory: "1500Mi"
              cpu: "1000m"
            requests:
              memory: "128Mi"
              cpu: "500m"
$ kubectl get po
$ kubectl get deploy
$ kubectl get rs
$ kubectl describe rs {name}

example



ResourceQuota로 리소스 개수 제한하기

ResourceQuota는 아래의 쿠버네티스 오브젝트의 개수를 제한할 수 있습니다.

  • deploy, po, svc, secret, cm, pvc 등의 개수
  • NodePort 타입의 서비스 개수, LoadBalancer 타입의 서비스 개수
  • QoS 클래스 중에서 BestEffot 클래스에 속하는 포드의 개수
# vi quota-limit-pod-svc.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: resource-quota-example
  # namespace: henry-playground
spec:
  hard:
    requests.cpu: "1000m"
    requests.memory: "500Mi"
    limits.cpu: "1500m"
    limits.memory: "1000Mi"
    count/pods: 3
    count/services: 5

example

example

리소스의 개수를 제한할 때, 정의되는 쿠버네티스 오브젝트 이름은 count/{오브젝트 이름}.{API 그룹 이름}입니다. 예를 들어 pods나 secretes 등은 코어 API 그룹("")에 해당하기 때문에 별도로 API 그룹명을 명시하지 않지만, apps에 속하는 디플로이먼트의 개수를 제한하려면 count/deployments.apps: 0과 같이 사용해야 합니다.



ResourceQuota로 BestEffort 클래스의 포드 개수 제한하기

# vi quota-limit-besteffort.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: besteffort-quota
  namespace: default
spec:
  hard:
    count/pods: 1
  scopes:
    - BestEffort

이전의 resourcequota와 달리 scopes라는 항목을 새롭게 정의했습니다. scopes는 필수 항목은 아니지만, BestEffort 및 Terminating, NotTerminating, NotBestEffort와 같은 포드의 상태를 값으로 입력할 수 있습니다.

BestEffort 클래스의 포드는 아무런 자원 할당을 설정하지 않은 경우에 해당하기 때문에 hard 항목에서 limit.cpu나 limit.memroy와 같은 자원 제한 설정과 연결되어 사용하지 않습니다. BestEffort 클래스의 포드 개수를 제한할 때는 위의 YAML 파일처럼 포드의 개수를 제한하는 항목(count/pods)만 유효하게 작동합니다.

scope에서 NotBestEffort는 BestEffort 클래스가 아닌 다른 종류의 모든 QoS 클래스를 의미하며, Terminating은 포드의 종료 시간이 명시적으로 설정된 경우를 의미합니다. 보통 잡(Job)이라는 쿠버 오브젝트에서 사용되기 때문에 대부분의 일반적인 포드는 NotTerminating에 속한다고 생각하면 됩니다.

위의 ResourceQuota를 생성한 다음 BestEffort 클래스의 포드를 여러 개 생성하면 요청이 거절됨을 알 수 있습니다.

$ kubectl apply -f quota-limit-besteffort.yaml
$ kubectl run besteffort-nginx1 --image=nginx
$ kubectl run besteffort-nginx2 --image=nginx
Error from server (Forbidden): pods "besteffort-nginx2" is forbidden: exceeded quota: besteffort-quota, requested: count/pods=1, used: count/pods=1, limited: count/pods=1

example

또 한가지 알아둬야 할 것은 BestEffort 포드의 개수를 허용하지 않은 채로 ResourceQuota에서 메모리나 CPU를 제한하면 BestEffort 포드의 생성은 실패합니다. BestEffort는 최대 효율로 자원을 사용하는데 ResourceQuota에서 제한을 걸었기 때문입니다.

# 지금까지의 리소스가 다 없어야됨

# cpu, memory 제한하는 resource quota
$ kubectl -f {resourcequota.yaml}

$ k run besteffort-nginx --image=nginx
Error from server (Forbidden): pods "besteffort-nginx" is forbidden: failed quota: resource-quota-example: must specify limits.cpu,limits.memory,requests.cpu,requests.memory

이렇기 때문에 ResourceQuota에서 cpu, memory 자원을 제한했다면 포드를 생성할 때 BestEffort는 생성하지 못하므로 자원 제한을 필수적으로 해줘야 합니다. 이를 위해서 쿠버네티스에서는 포드의 자원 사용량을 기본적으로 제한할 수 있는 LimitRange라는 기능을 제공합니다.



LimitRange로 자원 사용량 제한

LimitRange는 특정 네임스페이스에서 할당되는 자원의 범위 또는 기본값을 지정할 수 있는 쿠버네티스 오브젝트입니다.

  • 포드의 컨테이너에 CPU나 메모리 할당량이 설정돼 있지 않은 경우, 컨테이너에 자동으로 기본 Requet 또는 Limit 값을 설정할 수 있습니다.
  • 포드 또는 컨테이너의 CPU, 메모리, 퍼시스턴트 볼륨 클레임 스토리지 크기의 최솟값/최댓값을 설정할 수 있습니다.

LimitRange도 네임스페이스별로 적용할 수 있는 기능이므로 네임스페이스에 종속되는 오브젝트입니다.

# vi limitrange-example.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: mem-limit-range
  # namespace: henry-playground
spec:
  limits:
    - default:
        memory: 256Mi
        cpu: 200m
      defaultRequest:
        memory: 128Mi
        cpu: 100m
      max:
        memory: 500Mi
        cpu: 500m
      min:
        memory: 16Mi
        cpu: 50m
      type: Container
  1. default: 포드의 컨테이너에 Limit 값이 설정돼 있지 않다면 자동으로 이 값을 Limit으로 설정합니다.
  2. defaultRequest: 포드의 컨테이너에 Request 값이 설정돼 있지 않다면 자동으로 이 값을 Request로 설정합니다.
  3. max: 포드의 컨테이너에 설정될 수 있는 Limit 값의 최대치를 의미합니다. 만약 max에 설정된 값보다 더 많은 자원을 사용하려고 시도하면 포드의 생성은 실패합니다.
  4. min: 포드의 컨테이너에 설정될 수 있는 Request 값의 최소치를 의미합니다. 만약 min에 설정된 값보다 자원을 더 적게 사용하려고 시도하면 포드의 생성은 실패합니다.
  5. 이러한 자원 할당에 대한 설정이 컨테이너 단위로 적용될 것임을 나타냅니다. 컨테이너 외에도 포드, 퍼시스턴트 볼륨 클레임을 입력할 수 있습니다.
$ kubectl apply -f limitrange-example.yaml
$ kubectl run limitrange-test-nginx --image=nginx
$ kubectl describe po limitrange-test-nginx
$ kubectl describe limitrange limitrange-example

example

그리고 min, max의 범위를 벗어나는 포드의 컨테이너는 생성할 수 없습니다.


example

LimitRange에서 maxLimitRequestRatio 항목을 사용하면 포드의 컨테이너에서 오버커밋되는 자원에 대한 비율을 제한할 수도 있습니다.

# vi limitrange-ratio.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: limitrange-ratio
  # namespace: henry-playground
spec:
  limits:
    - maxLimitRequestRatio:
        memory: 1.5
        cpu: 1
      type: Container

위의 YAML에서는 memory를 1.5로 설정했으므로 새롭게 생성되는 포드의 Limit, Request의 비율은 1.5보다 작아야 합니다.

예시로 Limit이 200Mi이고 Request 100Mi로 설정된 포드의 컨테이너는 LimitRequestRatio 값이 200Mi/100Mi=2이지만, LimitRange에는 1.5까지만 혀용하기 때문에 포드의 생성은 거절됩니다.


example

maxLimitRequestRatio는 오버커밋을 얼마나 허용할 수 잇는지 제어할 수 있을 뿐만 아니라, 이 값을 1로 설정함으로써 반드시 Guaranteed 클래스의 포드만을 생성하도록 강제하는 용도로도 사용 할 수 있습니다.

포드 단위로 자원 사용량의 범위를 제한 할 수도 있습니다.

# vi limitrange-ex
apiVersion: v1
kind: LimitRange
metadata:
  name: pod-limit-range
  namespace: henry-playground
spec:
  limits:
    - max:
        memory: 1Gi
      min:
        memory: 200Mi
      type: Pod

ResourceQuota에서 네임스페이스의 Limit, Request를 설정하면 기본적으로 BestEffort 클래스의 포드 생성이 거부되지만, LimitRange를 사용하면 포드의 컨테이너에 일괄적으로 기본 자원 할당량을 설정할 수 있습니다.



ResourceQuota, LimitRanger의 원리 : Admission Controller

어드미션 컨트롤러에 대해 간단하게 설명하자면 사용자의 API 요청이 적절한지 검증하고, 필요에 따라 API 요청을 변형하는 단계라고 말할 수 있습니다. 만약 kubectl 등으로부터 전송된 API가 부적절하다면 어드미션 컨트롤러 단계에서 해당 API 요청을 거절할 수도 있고, 필요하다면 API 요청에 포함된 파라미터를 변경할 수도 있습니다.

ServiceAccount, ResourceQuota, LimitRange는 어디미션 컨트롤러의 한 종류입니다.

쿠버네티스에는 총 두 단계의 어드미션 컨트롤러가 있습니다. API 요청을 검사하는 것을 검증(Validatiing) 단계라고 부르며, API 요청을 적절히 수정하는 것을 변형(Mutating) 단계라고 부릅니다. 아래는 어드미션 컨트롤러의 동작 과정 예시입니다. (4, 5번에서 어드미션 컨트롤러 동작)

  1. 사용자가 kubectl apply -f pod.yaml와 같은 명령어로 API 서버에 요청을 전송했습니다.
  2. 서비스 어카운트 등을 통해 인증 단계를 거칩니다.
  3. 롤, 클러스터 롤 등을 통해 인가 단계를 거칩니다.
  4. 어드미션 컨트롤러인 ResourceQuota는 해당 포드의 자원 할당 요청이 적절한지 검증(Validation)합니다. 만약 해당 포드로 인해 ResourceQuota에 설정된 네임스페이스의 최대 자원 할당량을 초과한다면 해당 API 요청은 거절합니다.
  5. 해당 API 요청에 포함된 포드 데이터에 자원 할당이 설정되지 않은 경우, 어드미션 컨트롤러인 LimitRange는 포드 데이터에 CPU 및 메모리 할당의 기본값을 추가함으로써 원래의 포드 생성 API의 데이터를 변형합니다.

어드미션 컨트롤러는 직접 구현해 쿠버네티스에 등록할 수도 있습니다. 예를 들어 Nginx 포드를 생성하는 API 요청이 제출됐지만, 개발자의 실수로 인해 Nginx 컨테이너가 80 포트가 아닌 다른 포트를 사용하도록 YAML 파일에 정의돼 잇다면 이를 자동으로 수정해주는 어드미션 컨트롤러를 직접 구현할 수도 있습니다.

또한, 포드의 어노테이션에 따라서 별도의 사이드카 컨테이너를 자동으로 추가하는 주입(Injection) 패턴을 어드미션 컨트롤러를 통해 구현할 수도 있습니다. 유명한 서비스 메쉬 솔루션인 Istio 또한 어드미션 컨트롤러를 통해 포드에 프록시 사이드카 컨테이너를 주입하는 방법을 사용합니다.



쿠버네티스 스케줄링

쿠버네티스와 같은 클라우드 시스템에서 스케줄링이 중요한 이유는 상황에 따라서 매우 다양합니다. 간단한 예시로 특정 컨테이너가 빠른 파일 입출력을 위해 SSD를 사용해야 한다면 SSD를 가지고 있는 서버에 컨테이너를 할당할 수 있습니다. 또한 컨테이너를 모든 서버에 최대한 고르게 배포함으로써 서버에 장애가 발생해도 애플리케이션의 무중단, 즉 고가용성을 확보해야할 수도 있습니다.



포드가 실제로 노드에 생성되기까지의 과정

사용자가 kubectl이나 API 서버로 포드 생성 요청을 전송하면 어떠한 일이 일어나는지 정리해봅시다.

  1. ServiceAccount, RoleBinding 등의 기능을 이용해 포드 생성을 요청한 사용자의 인증 및 인가 작업을 수행합니다.
  2. ResourceQuota, LimitRange와 같은 어드미션 컨트롤러가 해당 포드 요청을 적절히 변형하거나 검증합니다.
  3. 어드미션 컨트롤러의 검증을 통과해 최종적으로 포드 생성이 승인됐다면 쿠버네티스는 해당 포드를 워커 노드 중 한 곳에 생성합니다.

포드의 스케줄링은 위 단계 중에서 3번에서 수행됩니다.

kube-system 네임스페이스에는 스케줄링에 관여하는 kube-scheduler와 etcd 컴포넌트가 있습니다. kube-scheduler는 쿠버네티스 스케줄러에 해당하며, etcd는 쿠버네티스 클러스터의 전반적인 상태 데이터를 저장하는 일종의 데이터베이스 역할을 담당합니다.

etcd는 분산 코디네이터라고 불리는 도구의 일종으로, 클라우드 플랫폼 등의 환경에서 여러 컴포넌트가 정상적으로 상호 작용할 수 있도록 데이터를 조정하는 역할을 담당합니다. etcd에는 현재 생성된 디프롤이먼트나 포드의 목록과 정보, 클러스터 자체 정보 등 대부분의 데이터가 etcd에 저장돼 있습니다.

etcd에 저장된 포드의 데이터에는 해당 포드가 어느 워커 노드에서 실행되는지를 나타내는 nodeName 항목이 존재합니다.

$ kubectl get po -n kube-system {pod name} -o yaml | grep -F3 nodeName

인증, 인가, 어드미션 컨트롤러 등의 단계를 모두 거쳐 포드 생성 요청이 최종적으로 승인됐다면 API 서버는 etcd에 포드의 데이터를 저장합니다. 하지만 API 서버는 포드의 데이터 중에서 nodeName 항목을 설정하지 않은 상태로 etcd에 저장합니다.(아직 스케줄링이 되지 않았기 때문)

쿠버네티스 스케줄러 컴포넌트에 해당하는 kube-scheduler는 API 서버의 watch를 통해 nodeName이 비어 잏ㅆ는 포드 데이터가 저장됐다는 사실을 전달받습니다. 스커줄러는 nodeName이 설정되지 않은 해당 포드를 스케줄링 대상으로 판단하고, 포드를 할당할 적절한 노드를 선택한 다음 API 서버에게 해당 노드와 포드를 바인딩할 것을 요청하게 됩니다. 그러고 나면 포드의 nodeName 항목의 값에는 선택된 오드의 이름이 설정됩니다.

클러스터의 각 노드에서 실행 중인 kubelet은 API 서버에 걸어 둔 watch를 통해 포드의 데이터에서 nodeName이 설정됐다는 소식을 전달받습니다. 해당 nodeName에 해당하는 노드의 kubelet이 컨테이너 런타임을 통해 포드를 생성합니다.



포드가 생성될 노드를 선택하는 스케줄링 과정

스케줄러는 크게 노드 필터링, 노드 스코어링 단계를 거쳐 최종적으로 노드를 선택합니다.

노드 필터링 : 포드를 할당할 수 있는 노드와 그렇지 않은 노드를 분리해 걸러내는 단계입니다. CPU나 메모리의 Request 만큼의 가용 자원이 존재하지 않거나 status가 ready가 아닌 노드 등이 제외됩니다. 노드 필터링 단계에서 선정된 노드의 목록은 노드 스코어링 단계로 전달됩니다.

노드 스코어링 : 쿠버네티스의 소스코드에 미리 정의된 알고리즘의 가중치에 따라서 노드의 점수를 계산합니다. 예를 들어, 포드가 사용하는 도커 이미지가 이미 노드에 존재할 때는 빠르게 포드를 생성할 수 있기 때문에 해당 노드의 점수가 증가합니다. 또는 노드의 가용 자원이 많으면 많을수록 노드의 점수가 높게 평가될 수도 있습니다. 이러한 알고리즘들의 값을 합산함으로써 후보 노드들의 점수를 계산한 다음, 가장 점수가 높은 노드를 죄종적으로 선택합니다.

아래에서 학습할 내용들은 노드 필터링 단계에서 사용할 수 있는 방법들입니다.



NodeSelector와 Node Affinity, Pod Affinity

nodeName과 nodeSelector를 사용한 스케줄링 방법

특정 워커 노드에 포드를 할당하는 가장 확실한 방법은 포드의 YAML 파일에 노드의 이름을 직접 명시하는 것입니다.

$ kubectl get nodes
# vi nodename-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  # namespace: henry-playground
spec:
  nodeName: ip-172-29-3-69.ap-northeast-2.compute.internal
  containers:
    - name: nginx
      image: nginx:latest
$ kubectl get po -o wide
# Node 이름 확인

이런 방식은 YAML 파일을 보편적으로 사용하기 어렵고, 노드에 장애가 발생하면 유연한 대처가 어렵습니다.

nodeName 대신에 노드의 라벨을 사용하면 특정 라벨이 존재하는 노드에만 포드를 할당할 수 있습니다.

$ kubectl label nodes ip-172-29-3-69.ap-northeast-2.compute.internal mylabel/disk=ssd
$ kubectl label nodes ip-172-29-2-40.ap-northeast-2.compute.internal mylabel/disk=hdd
$ kubectl label nodes ip-172-29-3-171.ap-northeast-2.compute.internal mylabel/disk=hdd

노드의 라벨을 삭제할때는 -(대시)를 추가하면 됩니다.

$ kubectl label nodes ip-172-29-3-171.ap-northeast-2.compute.internal -mylabel/disk=hdd

mylabel/disk 키의 값이 hdd인 노드에 포드를 할당하려면 포드의 YAML 파일에 아래와 같이 nodeSelector를 정의합니다.

# nodeselector-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-nodeselector
  # namespace: henry-playground
spec:
  nodeSelector:
    mylabel/disk: hdd
  containers:
    - name: nginx
      image: nginx:latest



Node Affinity를 이용한 스케줄링 방법

위의 라벨셀렉터 방식은 활용 방법이 다양하지 않아 이를 보완하기 위해 Node Affinity라는 스케줄링 방법을 제공합니다. Node Affinity는 nodeSelector에 좀 더 확장된 라벨 선택 기능을 제공하며, 반드시 충족해야 하는 조건(Hard)과 선호하는 조건(Soft)을 별도로 정의할 수도 있습니다.

  • requiredDuringSchedulingignoredDuringExecution
  • preferredDuringSchedulingignoredDuringExecution
# vi nodeaffinity-required.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-nodeaffinity-required
  # namespace: henry-playground
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: mylabel/disk
                operator: In
                values:
                  - ssd
                  - hdd
  containers:
    - name: nginx
      image: nginx:latest

key의 라벨이 value의 값 중 하나를 만족하는 노드에 포드를 스케줄링한다는 뜻입니다.

Operator에는 In 외에도 NotIn, Exists, DoesNotExist, Gt(값이 큼), Lt(값이 작음)를 사용할 수 있어서 nodeSelector보다 더욱 다양하게 활용할 수 있다는 것이 특징입니다.

위에서는 requiredDuringSchedulingIgnoredDuringExecution 옵션을 사용했는데 이건 반드시 만족해야만 하는 제약 조건을 정의할 때 쓰입니다.

하지만 preferredDuringSchedulingIgnoredDuringExecution 옵션은 required~ 옵션과 다른데 이건 선호하는 제약조건을 의미합니다. 따라서 해당 옵션 아래에서 정의한 라벨의 키-값 조건은 반드시 만족해야 할 필요는 없으며, 만약 해당 조건을 만족하는 노드가 있다면 그 노드를 좀 더 선호하겠다는 뜻입니다.

# vi nodeaffinity-preferred.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-nodeaffinity-preferred
  # namespace: henry-playground
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 80
          preference:
            matchExpressions:
              - key: mylabel/disk
                operator: In
                values:
                  - ssd
  containers:
    - name: nginx
      image: nginx:latest

이전과는 다르게 weight라는 값을 추가했는데, 이 값은 조건에 해당한느 노드를 얼마나 선호할지를 나타내는 가중치입니다. weight 값은 1에서 100까지의 값을 사용할 수 있으며, 이 가중치는 할당 가능한 모든 노드를 필터링한 뒤 수행하는 노드 스코어링 단계에서 적용합니다. 이런 제약 조건을 소프트(soft) 제약 조건이라고 합니다.

이러한 스케줄링 조건은 포드를 할당할 당시에만 유효합니다. 따라서 일단 포드가 할당된 뒤에 노드의 라벨이 변경되더라도 다른 노드로 포드가 옮겨가는 퇴거(Eviction)가 발생하지는 않습니다.

"스케줄링 과정에서는 유효하지만(required During Scheduling), 일단 실행이 된 다음에는 무시된다(Ignored During Execution).

반대되는 옵션으로 requiredDuringSchedulingRequiredDuringExecution처럼 접미어를 바꿔서 사용할 수도 있는데 이 때는 스케줄링에 영향을 주는 노드 라벨이 포드가 실행된 뒤에 변경됐다면 포드가 다른 노드로 옮겨가게 됩니다.



Pod Affinity를 이용한 스케줄링 방법

Node Affinity가 특정 조건을 만족하는 노드를 선택하는 방법이라면, Pod Affinity는 특정 조건을 만족하는 포드와 함께 실행되도록 스케줄링합니다.

# vi podaffinity-required.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-podaffinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
          matchExpressions:
            - key: mylabel/database
              operator: In
              values:
                - mysql
          topologyKey: failure-domain.beta.kubernetes.io/zone
  containers:
    name: nginx
    image: nginx:latest

Node Affinity를 사용했을 때와 같지만, topologyKey라는 새로운 항목을 정의했습니다. 이건 labelSelector의 해당 라벨을 가지는 포드와 무조건 같은 노드에 무조건 할당하라는 뜻이 아니고, topologyKey에 적혀있는 라벨을 가지는 토폴로지 범위의 노드를 선택하게됩니다.

example

topologyKey에 설정된 라벨의 키-값에 따라 여러 개의 그룹으로 분류된다고 생각해보겠습니다. 키가 2a, 2b 2개의 그룹으로 분류돼 있는데 이 때 matchExpression의 라벨 조건을 만족하는 포드가 위치한 그룹의 노드 중 하나에 포드를 할당합니다. 따라서 조건을 만족하는 포드와 동일한 노드에 할당될 수도 있지만, 해당 노드와 동일한 그룹에 속하는다른 노드에 포드가 할당될 수도 있습니다. 위의 그림에서는 노드 A, B에 포드가 스케줄링 될 수 있습니다.



Pod Anti-affinity를 이용한 스케줄링 방법

Pod Anti-affinity는 특정 포드와 같은 토폴로지의 노드를 선택하지 않는 방법입니다. 이걸 사용해서 포드를 여러 가용 영역 또는 리전에 멀리 퍼뜨리는 전략을 세울 수 있습니다.

# vi pod-antiaffinity-required.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-antiaffinity
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: mylabel/database
                operator: In
                values:
                  - mysql
          topologyKey: failure-domain.beta.kubernetes.io/zone
  containers:
    - name: nginx
      image: nginx:latest
# 해당 라벨을 가지는 팟 우선 생성
$ kubectl run nginx-labels-mysql --image nginx --labels="mylabel/database=mysql"
# 이 팟의 노드가 아닌 다른 노드에 팟이 만들어짐
$ kubectl apply -f pod-antiaffinity-required.yaml

중요
Pod Affinity와 Anti-affinity는 Node Affinity와 동일하게 소프트 제한을 사용할 수 있습니다. requiredDuringSchedulingignoredDuringExecution을 사용하면 각 토폴로지의 노드에 포드를 여러 개 할당할 수도 있습니다.

# vi pod-antiaffinity-preferred.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod-antiaffinity-preferred
  namespace: henry-playground
spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - podAffinityTerm:
            labelSelector:
              matchExpressions:
                - key: mylabel/database
                  operator: In
                  values:
                    - mysql
            topologyKey: failure-domain.beta.kubernetes.io/zone
          weight: 80
  containers:
    - name: nginx
      image: nginx:latest

이런 원리들을 활용해서 모든 노드에 포드를 하나씩 할당하는 마치 데몬셋과 비슷한 디플로이먼트를 생성할 수 있습니다. 디플로이먼트를 생성할 때, 포드 템플릿에서 Pod Anti-affinity를 위한 topologyKey를 kubernetes.io/hostname으로 설정하면 됩니다.

# vi deployment-exclusive.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-nginx
  # namespace: henry-playground
spec:
  replicas: 3
  selector:
    matchLabels:
      app: deployment-nginx
  template:
    metadata:
      name: deployment-nginx
      labels:
        app: deployment-nginx
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - deployment-nginx
              topologyKey: "kubernetes.io/hostname"
      containers:
        - name: deployment-nginx
          image: nginx:latest



Taints와 Tolerations 사용하기

Taints와 Tolerations를 이용한 포드 스케줄링

Taints라는 이름이 의미하는 것처럼 특정 노드에 얼룩(Taint)을 지정함으로써 해당 노드에 포드가 할당되는 것을 막는 기능이라고 생각하면 쉽게 이해할 수 있습니다. 하지만 해당 Taints에 대응하는 Tolerations를 포드에 설정하면 Taints가 설정된 노드에도 포드를 할당할 수 있습니다. 말하자면 노드에 얼룩이 졌지만, 이를 용인(Tolerations)할 수 있는 포드만 해당 노드에 할당할 수 있는 것입니다.

Taints를 노드에 별도로 설정할 수도 있고, 특정 이벤트로 인해 쿠버네티스가 자동으로 Taints를 설정하기도 합니다.

# taint 설정
$ kubectl taint node ubuntu-ihp001-k8s-worker1 \
ihp001/my-taint=dirty:NoSchedule
# taint 해제
$ kubectl taint nodes ubuntu-ihp001-k8s-worker1 ihp001/my-taint-

taint는 key=value 뒤에 effect(Taint 효과)를 추가로 명시합니다. Taint 효과는 Taint가 노드에 설정됐을 때 어떠한 효과를 낼 것인지 결정합니다. Taints 효과에는 Noschedule(포드를 스케줄링하지 않음), NoExecute(포드의 실행 자체를 허용하지 않음), PreferNoSchedule(가능하면 스케줄링하지 않음) 총 3가지가 있습니다.

Taint가 설정된 노드에 포드를 할당하려면 해당 Taint를 용인할 수 있도록 Toleration을 별도로 정의해야 합니다.

apiVersion: v1
kind: Pod
metadata:
  name: nginx-toleration-test
  # namespace: app-setting
spec:
  tolerations:
    - key: ihp001/my-taint
      value: dirty
      operator: Equal
      effect: NoSchedule
  containers:
    - name: nginx
      image: nginx:latest

(tolerations는 반드시 taint가 설정된 노드에 포드가 할당된다는 뜻은 아닙니다.)

쿠버네티스에서도 기본적으로 다양한 Taint를 노드에 설정합니다. 하나의 예시로는 마스터 노드에 기본적으로 설정된 Taint를 들 수 있습니다. 쿠버네티스가 기본적으로 마스터 노드에 Taints를 설정함으로써 포드가 할당되는 것을 방지하여서 워커 노드에 포드가 할당되게 되는 것입니다.

마스터 노드에 설정된 Taint 또한 Toleration을 이용해 용인할 수 있습니다. 포드 스크립트 작성시에 마스터 노드의 Toleration을 정의하면 됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: nginx-master-toleration
spec:
  # taint 설정이 2개여서 2개다 toleration 시킴
  tolerations:
    - key: node-role.kubernetes.io/master
      effect: NoSchedule
      operator: Equal
      value: ""
    - key: node-role.kubernetes.io/control-plane
      effect: NoSchedule
      operator: Equal
      value: ""
  nodeSelector:
    # master 노드를 지정하기 위해 hostname을 박아버림
    kubernetes.io/hostname: "ubuntu-ihp001-k8s-master"
  containers:
    - name: nginx
      image: nginx:latest

마스터 노드에서 실행 중인 api-server 포드를 살펴보겠습니다.

$ kubectl get po -n kube-system | grep api
$ kubectl describe po -n kube-system {po name}

API 서버 포드에는 :NoExecute라는 Toleration이 설정돼 있습니다. 이 Toleration은 Taint의 키와 값이 무엇이든지 상관없이 모든 :NoExecute 종류의 Taint를 용인할 수 있음을 의미합니다. 서버 포드의 정보를 YAML 포맷으로 보면 YAML 파일에서 실제로 어떻게 설정돼 있는지 알 수 있습니다.

$ kubectl get pod -n kube-system kube-apiserver-ubuntu-ihp001-k8s-master -o yaml | grep -F2 toleration
      type: RuntimeDefault
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    operator: Exists

Toleration에서 Operator의 값은 Equal 외에도 Exists를 사용할 수 있습니다. 위처럼 Toleration의 Operator 항목이 Exists로 설정된 경우에는 Taint에 대한 와일드카드로서 동작합니다. 즉. key, value 및 effect 항목을 명시하지 않았다면 해당 항목의 값에 상관없이 모두 용인할 수 있습니다.

example



NoExecute와 tolerationSeconds

Taint의 효과는 Noschedule 외에도 PreferNoSchedule과 NoExecute가 있습니다. 이 중에서 NoExecute는 포드를 해당 노드에 스케줄링하지 않을 뿐만 아니라 해당 노드에서 아예 포드를 실행할 수 없도록 설정합니다. Noschedule은 노드에 설정하더라도 기존에 실행 중이던 포드는 정상적으로 동작하는 반면 NoExecute는 해당 노드에서 실행 중인 포드를 종료시킵니다.

단 포드가 디플로이먼트나 레플리카셋 등과 같은 리소스로부터 생성됐다면 NoExecute에 의해 포드가 종료됐더라도 포드는 다른 노드로 옮겨가는, 포드의 퇴거(Eviction)가 발생합니다. NoExecute에 의해 포드가 종료되면 레플리카셋은 라벨 셀렉터가 일치하는 포드의 개수가 replicas에 지정된 값보다 적다는 것을 감지할 것이고, 다른 노드에 새롭게 포드를 생성할 것이기 때문입니다.

포드를 생성하면 쿠버네티스는 자동으로 NoExecute에 대한 Toleration을 추가합니다.

Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s

노드가 준비되지 않았거나 네트워크 통신이 불가능한 상활일 때를 위한 Toleration입니다. 쿠버네티스는 특정 문제가 발생한 노드에 대해서는 자동으로 Taint를 추가합니다(NotReady, Unreachable, memory-pressure, disk-pressure). NotReady, Unreachable은 노드 자체에 장애가 생긴 경우일 수 있어서 노드에 아레의 Taint를 추가합니다.

  • node.kubernetes.io/not-ready:NoExecute
  • node.kubernetes.io/unreachable:NoExecute

모든 포드에 자동으로 추가된 node.kubernetes.io/not-ready:NoExete for 300s와 같은 Toleration은 노드에 장애가 발생해 not-ready나 unreachable 상태의 Taint가 발생하더라도 300초 동안은 해당 Taint를 용인하겠다는 뜻입니다. 300초 이내에 노드가 정상 상태로 돌아와 Taint가 삭제되지 않으면 포드는 그대로 해당 노드에서 정상적으로 뜨게됩니다.

이러한 옵션을 tolerationSeconds라고 부르며, 포드가 실행 중인 노드에 Taint를 용인할 수 있는 최대 시간을 의미합니다.

$ kubectl get po {pod name} -o yaml | grep -F4 tolerationSeconds

위의 2개의 Taint에 대한 Toleration은 DefaultTolerationSeconds라는 이름의 어드미션 컨트롤러에 의해서 포드에 추가됩니다.



Cordon, Drain 및 PodDistributionBudget

cordon을 이용한 스케줄링 비활성화

Taint와 Toleration 말고, cordon 명령어를 사용해서 노드에 더 이상 포드가 스케줄링 되지 않게 할 수 있습니다.

$ kubectl cordon {노드 이름}
$ kubectl get nodes
# 해제
$ kubectl uncordon {노드 이름}
$ kubectl describe node {노드 이름} | grep -F3 Taints:
# Taints가 추가되어 있음, Unschedulable true

cordon 명령어는 NoExecute가 아닌 NoSchedule 효과가 있는 Taint를 노드에 추가하기 때문에 해당 노드에서 이미 실행 중인 포드가 종료하지는 않습니다.



drain 명령어로 노드 비활성화하기

drain 명령어는 cordon에 기존에 실행 중이던 포드르 다른 노드로 옮겨가도록 퇴거(Eviction)를 수행한다는 점이 다릅니다. 해당 노드에 포드가 실행되진 않기 때문에 커널 버전 업그레이드, 유지 보수를 할 때 유용하게 사용할 수 있습니다.

데몬셋 포드 및 단일 포드가 존재할 경우 drain은 에러가 뜹니다. 데몬셋을 무시하고 노드를 drain하려면 --ignore-daemonsets, --force 옵션을 함께 사용합니다.



PodDisruptionBudget으로 포드 개수 유지하기

drain은 노드에서 실행 중이던 포드를 모두 종료시키는 퇴거(Eviction) 기능을 포함하고 있습니다. 퇴거 작업은 일반적으로 포드를 종료시키는 것만을 의미하지만, 특정 개수의 포드 개수를 유지하려는 디플로이먼트나 레플리카셋 등에 의해 생성된 포드라면 포드가 퇴거되더라도 다른 노드에서 다시 생성되는 것이 일반적입니다.

그런데 drain과 같이 포드 퇴거 작업이 수행될 때는 포드가 다른 노드로 옮겨가는 사이에 애플리케이션이 중단될 수 있는 문제가 있습니다.

이러한 상황에 대처하기 위해 PodDisruptionBudget이라는 기능을 제공합니다 . PodDisruptionBudget은 kubectl drain 명령어 등으로 포드에 퇴거가 발생할 때, 특정 개수 또는 비율만큼의 포드는 반드시 정상적인 상태를 유지하기 위해서 사용됩니다.

$ kubectl api-resources | grep PodDis
$ kubectl get pdb
# vi simple-pdb-example.yaml
apIVersion: policy/v1beta1
kind: PodDisruptionBudget
metatdata:
  name: simple-pdb-example
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: webserver

PodDisruptionBudget에는 maxUnavailable과 minAvailable 두 가지 중 하나를 사용할 수 있습니다. maxUnabailable은 kubectl drain 등에 의해 노드의 포드가 종료될 때, 최대 몇 개까지 동시에 종료될 수 있는지를 뜻합니다. maxUnavailable 값으로 1을 설정하면 kubectl drain 명령어를 사용한 노드의 포드가 하나씩 종료되어 다른 노드에서 다시 생성됩니다. 값을 숫자나 전체 포드의 비율(%)로도 설정할 수 있습니다.

minAvailable은 포드의 퇴거가 발생할 때, 최소 몇 개의 포드가 정상 상태를 유지해야 하는지를 의미합니다. maxUnavilable, minAvailable 중 한가지 속성만 사용해야 합니다.

selector에서 항목에서 주의해야 할 점은 디플로이먼트의 라벨이 아닌 포드의 라벨을 입력해야 한다는 것 입니다.

# vi deployment-pdb-test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployemnt-pdb-test
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webserver
  template:
    metadata:
      name: my-webserver
      labels:
        app: webserver
    spec:
      containers:
        - name: my-webserver
          image: alicek106/rr-test:echo-hostname
          ports:
            - containerPort: 80

PodDisruptionBudget이 생성된 상태에서 PodDisruptionBudget의 라벨 셀렉터에 일치하는 포드가 kubetl drain 등에 의해 퇴거돼야 한다면 PodDisruptionBudget에 정의된 값에 따라 정상 상태의 포드 개수가 일정하게 유지됩니다.



커스텀 스케줄러 및 스케줄러 확장

커스텀 스케줄러 구현

쿠버네티스는 kube-system 네임스페이스에 존재하는 기본 스케줄러(kube-scheduler) 외에도 여러 개의 스케줄러를 동시에 사용할 수 있도록 지원합니다.

포드를 생성하면 기본적으로 기본 스케줄러를 사용하게 되며, 노드 필터링과 노드 스코어링 단계를 거쳐 스케줄링 작업을 수행합니다. 포드를 생성한 뒤에 자동으로 추가되는 schedulerName 항목에서 기본 스케줄러 이름을 확인할 수 있습니다.

$ kubectl get po {pod 이름} -o yaml | grep scheduler

YAML파일에서 schedulerName의 값을 별도로 명시하면 커스텀 스케줄러로 설정할 수 있습니다.



쿠버네티스 스케줄러 확장하기

직접 커스텀 스케줄러를 구현하기보다는, 쿠버네티스 소스코드를 내려받아 스케줄러 부분만을 수정해 직접 빌드하는 것도 하나의 방법이 될 수 있습니다. 또는 쿠버네티스 스케줄링 프레임워크를 사용해 스케줄러를 개발하거나, 스케줄러 Extender 등을 이용해 기본 스케줄러에 추가적인 로직을 덧붙일 수 있습니다. (스케줄러 확장은 신중히 결정하는 것이 좋다~)



쿠버네티스 애플리케이션 상태와 배포

쿠버네티스는 애플리케이션을 안정적으로 배포할 수 있도록 몇 가지 기능을 제공하고 있습니다. 대표적으로는 새로운 버전의 애플리케이션이 점진적으로 배포될 수 있도록 디플로이먼트에서 롤링 업데이트 기능을 사용할 수 있으며, 배포된 애플리케이션의 버전을 내부적으로 저장함으로써 언제든지 원하는 버전의
디플로이먼트로 되돌릴 수도 있습니다. 그뿐만 아니라 새롭게 배포되는 포드의 애플리케이션이 사용자의 요청을 처리할 준비가 됐는지 확인할 수도 있고, 삭제될 포드가 애플리케이션을 우아하게 종료할 수 있도록 별도의 설정을 추가할 수도 있습니다. 이러한 디플로이먼트의 롤링 업데이트를 사용하는 방법과 포드가 생성되어 실행되고 종료되기까지의 생애 주기에 대해서 공부해봅시다.



디플로이먼트를 통해 롤링 업데이트

디플로이먼트를 이용한 레플리카 셋의 버전 관리

디플로이먼트는 레플리카 셋의 변경 사항을 저장하는 리비전을 디플로이먼트에서 관리함으로써 애플리케이션의 배포를 쉽게 할 수 있습니다.

디플로이먼트에서 변경 사항이 생기면 새로운 레플리카 셋이 생성되고, 그에 따라 새로운 버전의 애플리케이션이 배포됩니다. 이 때 --record 옵션을 추가해 디플로이먼트의 변경 사항을 적용하면 이전에 사용하던 레플리카 셋의 정보는 디플로이먼트의 히스토리에 기록됩니다. 그리고 이러한 리비전을 이용해 언제든지 원하는 버전의 애플리케이션으로 롤백할 수 있습니다.

# deployment-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.15
          ports:
            - containerPort: 80
# deployement-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.16
          ports:
            - containerPort: 80
$ kubectl apply -f deployemnt-v1.yaml --record
$ kubectl apply -f deployemnt-v2.yaml --record
$ kubectl get po
$ kubectl rollout history deployment nginx-deployment

기본적으로 레플리카 셋의 리비전은 10개까지만 히스토리에 저장되지만, 필요하다면 디플로이먼트를 생성할 때 revisionHistoryLimit이라는 항목을 직접 설정함으로써 리비전의 최대 개수를 지정할 수 있습니다.

kind: Deployemnt
metadata:
  name: example
spec:
  revisionHistoryLimit: 3
  ...



디플로이먼트를 통한 롤링 업데이트 설정

여러가지 배포 방법중 일시적으로 사용자의 요청을 처리하지 못해도 괜찮은 애플리케이션이라면 쿠버네티스에서 제공하는 ReCreate 방법을 사용할 수 있습니다. 이 방법은 기존 버전의 포드를 모두 삭제한 뒤, 새로운 버전의 포드를 생성하는 방식입니다. 이렇나 배포 전략은 디플로이먼트의 YAML 파일에 있는 strategy의 type 항목에서 설정할 수 있습니다.

# vi deployment-recreate-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-recreate
spec:
  replicas: 3
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.15
          ports:
            - containerPort: 80
# vi deployment-recreate-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-recreate
spec:
  replicas: 3
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx;1.16
          ports:
            - containerPort: 80
$ kubectl apply -f deployment-recreate-v1.yaml
$ kubectl get po
$ kubectl apply -f deployment-recreate-v2.yaml
kubectl get po
# v1 포드 내려간뒤에 v2뜸.

또 다르게 쿠버네티스에서는 포드를 조금씩 삭제하고 생성하는 롤링 업데이트 기능을 제공합니다. 롤링 업데이트를 사용하면 디플로이먼트를 업데이트하는 도중에도 사용자의 요청을 처리할 수 있는 포드가 계속 존재하기 때문에 애플리케이션의 중단이 발생하지 않습니다.

YAML 파일에서 별도의 설정을 하지 않아도 디플로이먼트의 버전을 업데이트할 때는 기본적으로 롤링 업데이트를 사용하도록 설정돼 있습니다. 이 때 롤링 업데이트 도중에 기존 버전의 포드를 몇 개씩 삭제할 것인지, 새로운 버전의 포드는 몇 개씩 생성할 것인지는 직접 설정할 수 있습니다. 이런 세부 옵션을 설정할 경우에는 YAML 파일에 명시해야 하고, strategy의 type 항목을 RollingUpdate로 설정해야 합니다.

# vi deployment-rolling-update.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-rolling-update
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
  spec:
    containers:
      - name: nginx
        image: nginx:1.15
        ports:
          - containerPort: 80

롤링 업데이트의 세부 옵션에는 maxSurce, maxUnavailable 두 가지가 있으며, 이 옵션을 적절히 섞어 사용하면 롤링 업데이트의 속도를 조절할 수 있습니다. 옵션의 값은 숫자나 비율을 값으로 사용할 수 있으며, 비율을 사용하면 전체 포드의 개수를 기준으로 값이 결정됩니다. 퍼센트를 사용할 때 maxSurge의 소수점 값은 반올림되고, maxUnavailable의 소수점 값은 버려집니다. 두 옵션 모두 기본값은 25%입니다.

  • maxUnavailable : 롤링 업데이트 도중 사용 불가능한 상태가 되는 포드의 최대 개수를 설정합니다.
  • maxSurge : 롤링 업데이트 도중 전체 포드의 개수가 디플로이먼트의 replicas 값보다 얼마나 더 많이 존재할 수 있는지 설정합니다.



블루 그린 배포 사용하기

블루 그린 배포는 기존 버전의 포드를 그대로 놔둔 상태에서 새로운 버전의 포드를 미리 생성해 둔 뒤 서비스의 라우팅만 변경하는 배포 방식을 의미합니다. 블루 그린 배포는 롤링 업데이트와 달리 특정 순간에 두 버전의 애플리케이션이 공존하지 않으며, ReCreate 전략처럼 중단 시간이 발생하지도 않는다는 장점이 있습니다.



포드의 생애 주기(Lifecycle)

포드의 상태와 생애주기

기본적인 포드의 상태는 아래와 같습니다.

  • Pending : 포드를 생성하는 요청이 API 서버에 의해 승인됐지만, 어떠한 이유로 인해 아직 실제로 생성되지 않은 상태
  • Running : 포드들이 모두 생성돼 포드가 정상적으로 실행된 상태
  • Completed : 포드가 정상적으로 실행돼 종료됐음을 의미
  • Error : 포드가 정상적으로 실행되지 않은 상태로 종료됐음을 의미. 포드 컨테이너의 init 프로세스가 0이 아닌 종료 코드를 반환했을 때에 해당
  • Terminating : 포드가 삭제 또는 퇴거(Eviction) 되기 위해 삭제 상태에 머물러 있는 경우에 해당


Completed, Error와 restartPolicy

컨테이너 내부의 프로세스가 종료될 때 종료 코드를 반환하는데, 컨테이너의 init 프로세스가 어떠한 값을 반환하느냐에 따라 포드의 상태가 Completed 또는 Error로 설정됩니다. 컨테이너에서는 Dockerfile 등에 의해 설정된 커맨드(CMD)와 Entrypoint의 조합이 init 프로세스가 됩니다.

밑의 Pod에 대해서 살펴 봅시다.

# vi completed-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: completed-pod-example
spec:
  containers:
    - name: completed-pod-example
      image: busybox
      command: ["sh"]
      args: ["-c", "sleep 10 && exit 0"]
$ kubectl apply -f completed-pod.yaml
$ kubectl get po --watch

처음에는 정상적으로 Running 상태가 되고, 0을 종료 코드로 반환한 뒤에는 Completed 상태로 전환했습니다. 하지만 포드가 Completed 상태가 된 뒤에도 계속해서 다시 실행됨과 동시에 RESTARTS 횟수 또한 증가하는데, 이는 기본적으로 포드의 재시작 정책을 설정하는 restartPolicy 속성이 Always로 설정돼 있기 때문입니다.

$ kubectl get po completed-pod-example -o yaml | grep restartPolicy

restartPolicy의 값에는 Always 외에도 Never 또는 OnFailure를 사용할 수 있습니다. Never는 포드가 종료되어도 절대로 다시 시작하지 않도록 설정하지만, onFailure는 포드의 컨테이너가 실패했을 때, 즉 0이 아닌 종료 코드를 반환했을 때만 포드를 다시 재시작합니다.

# vi completed-pod-restart-never.yaml
apiVersion: v1
kind: Pod
metadata:
  name: completed-pod-restart-never
spec:
  restartPolicy: Never
  containers:
    - name: error-pod-restart-never
      image: busybox
      command: ["sh"]
      args: ["-c", "sleep 10 && exit 0"]

포드가 종료된 뒤에도 다시 재시작하지 않고 계속해서 Completed 상태에 머물러 있는 것을 확인할 수 있습니다. 포드가 종료된 뒤 다시 시작되지 않도록 restartPolicy를 Never나 OnFailure로 설정하는 것은 쿠버네티스의 Job이나 Cronjob 오브젝트로부터 생성된 포드의 작업이 완료되어 다시 실행될 필요가 없을 때 유용하게 사용할 수 있습니다.

포드의 컨테이너가 0이 아닌 종료 코드 1을 반환하게(위의 YAML파일에서 exit 1만 수정)해서 포드를 배포하면 포드의 상태가 Error로 출력됩니다.

example

포드가 종료될 때마다 즉시 재시작이 되지는 않습니다. CrashLoopBackOff라는 상태가 있는데 쿠버네티스에서는 어떠한 작업이 잘못돼 실패했을 때, 일정 간격을 두고 해당 작업을 다시 시도하게 됩니다. 그리고 실패하는 횟수가 늘어날수록 재시도하는 간격이 지수 형태로 늘어나게 되는데, 그 중간 상태가 CrashLoopBackOff입니다. 즉 실패를 반복할수록 재시도하는 간격입니다.



Running 상태가 되기 위한 조건

포드를 생성했다고 해서 무조건 Running 상태가 되는 것은 아닐뿐더러, 포드가 Running 상태에 머물러 있다고 해서 컨테이너 내부의 애플리케이션이 재대로 동작하고 있을 것이라는 보장은 없습니다. 이를 위해서 쿠버네티스는 다음과 같은 기능을 제공하고 있습니다.

  • init Container
  • postStart
  • livenessProbe, readinessProbe


Running 상태가 되기 위한 조건 - init 컨테이너

Init 컨테이너는 포드의 컨테이너 내부에서 애플리케이션이 실행되기 전에 초기화를 수행하는 컨테이너입니다. Init 컨테이너는 포드의 애플리케이션 컨테이너와 거의 동일하게 사용할 수 있지만, 포드의 애플리케이션 컨테이너보다 먼저 실행된다는 점이 다릅니다. 따라서 포드의 애플리케이션 컨테이너가 실행되기 전에 특정 작업을 미리 수행하는 용도로 사용할 수 있습니다.

# vi init-container-example.yaml
apiVersion: v1
kind: Pod
metadata:
  name: init-container-example
spec:
  initContainers:
    - name: my-init-container
      image: busybox
      command: ["sh", "-c", "echo Hello World!"]
  containers:
    - name: nginx
      image: nginx

위의 YAML을 실행하게 되면 initContainers 항목에 정의한 컨테이너가 먼저 실행된 뒤, containers 항목에 정의한 컨테이너가 생성됩니다. 이 때 Init 컨테이너가 하나라도 실패하게 된다면 포드의 애플리케이션 컨테이너는 실행되지 않으며, 포드의 restartPolicy에 따라서 Init 컨테이너가 다시 재시작됩니다. 이런 점을 이용해서 Init 컨테이너 내부에서 dig나 nslookup 명령어 등을 이용해 다른 디플로이먼트가 생성되기를 기다리거나, 애플리케이션 컨테이너가 사용할 설정 파일 등을 미리 준비해 둘 수도 있습니다.

Running 상태가 되기 위한 조건 - postStart

포드의 컨테이너가 실행되거나 삭제될 때, 특정 작업을 수행하도록 라이프사이클 훅(Hook)을 YAML 파일에서 정의할 수 있습니다. 이 훅에는 두 가지 종류가 있는데, 컨테이너가 시작될 때 실행되는 postStart와 컨테이너가 종료될 때 실행되는 preStop입니다.

postStart는 두가지 방식으로 사용할 수 있습니다.

  • HTTP : 컨테이너가 시작한 직후, 특정 주소로 HTTP 요청을 전송합니다.
  • Exec : 컨테이너가 시작한 직후, 컨테이너 내부에서 특정 명령어를 실행합니다.
# vi poststart-hook.yaml
apiVersion: v1
kind: Pod
metadata:
  name: poststart-hook
spec:
  containers:
    - name: poststart-hook
      image: nginx
      lifecycle:
        postStart:
          exec:
            command: ["sh", "-c", "touch /myfile"]

lifecycle.postStart 라는 항목을 새롭게 정의했고, 컨테이너가 시작될 때 실행할 명령어를 함께 설정했습니다. postStart는 컨테이너의 Entrypoint와는 비동기적으로 실행됩니다.

이 때 postStart의 명령어나 HTTP 요청이 제대로 시행되지 않으면 컨테이너는 Running 상태로 전환되지 않으며, Init 컨테이너와 마찬가지로 restartPolicy에 의해 해당 컨테이너가 재시작됩니다.



애플리케이션의 상태 검사 - livenessProbe, readinessProbe

example

쿠버네티스는 애플리케이션이 사용자의 요청을 처리할 수 있는 상태인지를 판별하기 위해 livenessProbe와 readinessProbe 두 가지 방법을 제공합니다. 이 두 가지 방법은 포드가 Running 상태가 되기 위한 필수 조건은 아니지만, 포드 내부의 애플리케이션이 실제로 사용자의 요청을 처리할 수 있는 상태인지 확인하기 위해 사용할 수 있습니다.

  • livenessProbe : 컨테이너 내부의 애플리케이션이 살아있는지 검사하고, 검사에 실패하면 restartPolicy에 따라서 재시작됩니다.
  • readinessProbe : 컨테이너 내부의 애플리케이션이 사용자 요청을 처리할 준비가 됐는지 검사합니다. 검사에 실패하면 컨테이너는 서비스의 라우팅 대상에서 제외됩니다.
apiVersion: v1
kind: Pod
metadata:
  name: livenessprobe-pod
spec:
  containers:
    - name: livenessprobe-pod
      image: nginx
      livenessProbe:
        httpGet:
          port: 80
          path: /
  • httpGet : HTTP 요청을 전송해 상태를 검사합니다. HTTP 요청의 종료 코드가 200, 300번 계열이 아닌 경우 애플리케이션의 상태 검사가 실패한 것으로 간주합니다.
  • exec : 컨테이너 내부에서 명령어를 실행해 상태를 검사합니다. 명령어의 종료 코드가 0이 아닌 경우에 애플리케이션의 상태 검사가 실패한 것으로 간주합니다.
  • tcpSocket : TCP 연결이 수립될 수 있는지 체크함으로써 상태를 검사합니다.

위의 포드를 생성하면 주기적으로 포드의 IP로 HTTP 요청을 전송해 상태 검사를 합니다. 그래서 일부러 nginx의 index.html을 삭제하여 livenessProbe가 실패하도록 만들면 시간이 어느 정도 지난 뒤, 포드의 RESTART 횟수가 증가하고 컨테이너가 재시작되어 index.html이 다시 만들어져 livenessProbe는 다시 성공하게 됩니다.

$ kubectl apply -f {파일 이름}
$ kubectl get po
$ kubectl exec livenessprobe-pod -- rm /usr/share-nginx/html/index.html
$ kubectl get po -w
...
...
$ kubectl describe po livenessprobe-pod

readinessProbe로 애플리케이션의 상태 검사하기

readinessProbe는 상태 검사에 실패했더라도 시간이 지남에 따라 초기화 작업 등이 완료되어 준비 상태가 될 수 있기 때문에 일시적으로 포드를 서비스의 라우팅 대상에서 제외하는 작업만을 수행합니다.

apiVersion: v1
kind: Pod
metadata:
  name: readinessprobe-pod
  labels:
    my-readinessprobe: test
spec:
  containers:
    - name: readinessprobe-pod
      image: nginx
      readinessProbe:
        httpGet:
          port: 80
          path: /
---
apiVersion: v1
kind: Service
metadata:
  name: readinessprobe-svc
spec:
  ports:
    - name: nginx
      port: 80
      targetPort: 80
  selector:
    my-readinessprobe: test
  type: ClusterIP

적용해서 pod이 뜬 뒤,

$ kubectl run -i --tty --rm debug --image=alicek106/ubuntu:curl \
--restart=Never -- curl readinessprobe-svc
$ kubectl get ep

ep를 확인해보면 접근하는 요청이 포드의 IP로 라우팅되고 있음을 알 수 있습니다.

$ kubectl exec readinessprobe-pod -- rm /usr/share/nginx/html/index.html
$ kubectl get po -w

Nginx의 index.html을 삭제해서 readinessProbe를 실패하게 합니다. livenessProbe와 달리 RESTARTS 횟수가 증가하지 않았으며, 단순히 READY 상태인 컨테이너가 하나 줄어들기만 합니다. 서비스 리소스는 사용자 요청을 이 포드로 전달하지 않습니다.

$ kubectl run -i --tty --rm debug --image=alicek106/ubuntu:curl \
--restart=Never -- curl --connect-timeout 5 readinessprobe-svc
$ kubectl get ep

초기화 시간이 어느 정도 필요한 경우에는 디플로이먼트에서 minReadySeconds를 사용할 수 있습니다. minReadySeconds는 디플로이먼트의 업데이트시 컨테이너가 준비되기 위한 최소 대기 시간을 의미하며, 새로운 포드가 생성된 뒤 minReadySeconds의 시간이 지난 뒤에야 포드의 삭제 및 생성이 계속됩니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: minreadyseconds-v1
spec:
  replicas: 1
  minReadySeconds: 30
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: minready-test
  template:
    metadata:
      labels:
        app: minready-test
    spec:
      containers:
        - name: minreadyseconds-v1
          image: alicek106/rr-test:echo-hostname
---
apiVersion: v1
kind: Service
metadata:
  name: myservic
spec:
  ports:
    - name: web-port
      port: 80
      targetPort: 80
  selector:
    app: minready-test
  type: NodePort

livenessProbe와 readinessProbe의 세부 옵션

apiVersion: v1
kind: Pod
metadata:
  name: probe-options-example
  labels:
    my-readinessprobe: test
spec:
  containers:
    - name: probe-options-example
      image: nginx
      readinessProbe:
        httpGet:
          port: 80
          path: /
        periodSeconds: 5
        initialDelaySeconds: 10
        timeoutSeconds: 1
        successThreshold: 1
        failureThreshold: 3
  • periodSeconds : 상태 검사를 진행할 주기를 설정합니다.
  • initialDelaySeconds : 포드가 생성된 뒤 상태 검사를 시작할 때까지의 대기 시간을 설정합니다.
  • timeoutSeconds : 요청에 대한 타임아웃 시간을 설정합니다. 기본 값은 1초입니다.
  • successThreshold : 상태 검사에 성공했다고 간주할 검사 성공 횟수를 설정합니다. 기본 값은 1초입니다.
  • failureThreshold : 상태 검사가 실패했다고 간주할 검사 실패 횟수를 설정합니다. 기본 값은 3입니다.



Terminating 상태와 애플리케이션의 종료

새로운 애플리케이션으로 업데이트하는 것뿐만 아니라, 기존에 실행 중이던 애플리케이션을 어떻게 우아하게 종료할 수 있을지에 대해서도 고민해 볼 필요가 있습니다.

포드를 삭제할 때 어떤 일들이 발생하는지 알아봅시다.(kubectl delete 명령어)

  1. 리소스가 삭제될 예정이라는 의미의 deletionTimestamp 값이 포드의 데이터에 추가되고, 포드는 Terminating 상태로 바뀝니다.
  2. 아래 3가지 작업이 동시에 실행됩니다.
  • 포드에 preStop 라이프사이클 훅이 설정돼 있다면 preStop이 실행됩니다.
  • 포드가 레플리카 셋으로부터 생성된 경우 해당 포드는 레플리카 셋의 관리 영역에서 벗어나며, 레플리카 셋은 새로운 포드를 생성하려고 시도합니다.
  • 포드가 서비스 리소스의 라우팅 대상에서 제외됩니다.
  1. preStop 훅이 완료되면 리눅스 시그널 중 SIGTERM이 포드의 컨테이너에 전달됩니다. 컨테이너의 init 프로세스는 SIGTERM을 수신한 뒤 종료돼야 합니다.
  2. 특정 유예 기간이 지나도 컨테이너 내부의 프로세스가 여전히 종료되지 않으면 프로세스로 SIGKILL 시그널이 전달됩니다. 이 유예 기간은 기본적으로 30초로 설정돼 있으며, 포드의 terminationGracePeriodSeconds라는 항목을 통해 설정할 수 있습니다.

example

위 단계 중에서 애플리케이션이 우아하게 종료될 수 있도록 별도의 장치를 마련할 수 있는 부분은 2번의 첫 번쨰 preStop 라이프사이클 훅과 3번의 SIGTERM 시그널 전달 단계입니다.

preStop 라이프사이클 훅은 포드의 컨테이너가 종료되기 전에 실행되는 작업으로, postStart처럼 exec나 HTTP 요청을 통해 사용할 수 있습니다. 서버 종료를 실행하는 /abort와 같은 경로로 HTTP 요청을 보낼 수도 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: prestop-hook
spec:
  containers:
    - name: prestop-hook
      image: nginx
      lifecycle:
        preStop:
          exec:
            command: ["/user/sbin/nginx", "-s", "quit"]

preStop 훅이 실행되고 나면 포드의 컨테이너들에 SIGTERM 시그널을 보냄으로써 포드가 곧 종료될 것이라고 알립니다. 이 때 애플리케이션의 소스코드에서는 SIGTTERM 시그널을 수신했을 때 어떠한 행동을 취할 것인지 별도로 구현해 놓아야 합니다. 만약 SIGTERM 시그널을 처리하는 별도의 로직을 구현하지 않은 상태로 포드가 종료된다면 클라이언트는 해당 포드로부터 응답을 제대로 수신하지 못할 수도 있습니다.

만약 포드 내부의 애플리케이션이 SIGTERM을 수신했는데도 특정 유예 시간 이내에 종료되지 않으면 쿠버네티스는 SIGKILL 시그널을 전송해 강제로 프로세스를 종료하게 됩니다. 이 유예 기간은 terminationGracePeriodSeconds 값을 명시하거나 kubectl delete 명령어에서 --grace-period 옵션을 사용해 따로 설정할 수도 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: termination-grace-period-seconds
spec:
  terminationGracePeriodSeconds: 10
  contaienrs:
    - name: termination-grace-period-seconds
      image: nginx
      lifecycle:
        preStop:
          exec:
            command: ["/usr/sbin/nginx", "-s", "quit"]

그래서 terminationGracePeriodSeconds 값을 0으로 설정하면 포드의 컨테이너를 강제로 종료한다는 것을 의미합니다.



HPA를 활용한 오토스케일링

쿠버네티스에서는 리소스 사용량에 따라 디프롤이먼트의 포드 개수를 자동으로 조절하는 HPA(Horizontal Pod Autoscaler)라는 기능을 제공합니다.

HPA는 CPU나 메모리 사용량 정보를 어디선가 제공받아야하기 때문에 metrics-server인 리소스 메트릭 수집 도구를 설치해야 합니다. 모니터링 블로깅을 참고해주세요.

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: simple-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hostname-deployment # simple-deployment 자원 사용량에서
  targetCPUUtilizationPercentage: 50 # CPU 활용률이 50% 이상인 경우
  maxReplicas: 5 # 포드를 최대 5개까지 늘립니다.
  minReplicas: 1 # 최소 1개까지 줄어듭니다.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostname-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webserver
  template:
    metadata:
      name: my-webserver
      labels:
        app: webserver
    spec:
      containers:
        - name: my-webserver
          image: alicek106/ingress-annotation-test:0.0
          resources:
            requests:
              memory: "128Mi"
              cpu: "200m"
          ports:
            - containerPort: 5000
              name: flask-port
---
apiVersion: v1
kind: Service
metadata:
  name: hostname-svc-clusterip
spec:
  ports:
    - name: web-port
      port: 8080
      targetPort: 5000
  selector:
    app: webserver
  type: ClusterIP

해맸었던 부분은 컨테이너쪽에 requests를 속성을 적어야지만 hpa가 동작합니다. kubectl get hpa 명령어를 날렸을 때, TARGET쪽에 0%가 뜬다면 정상적으로 연동이 된 것이라고 보면 됩니다.

이젠 생성된 포드로 요청을 보내 CPU 사용률을 늘려봅시다. apache benchmark를 사용합니다.

$ kubectl run --image alpine -it -- sh

# apk add apache2-utils
# for i in $(seq 1 5); do ab -c 5 -n 100000 http://hostname-svc-clusterip:8080/; done;
$ kubectl get hpa
$ kubectl get po -w -o wide

터미널을 하나 더 연 다음 HPA의 상태 및 포드의 목록을 확인해보면 포드 개수가 5개로 늘어나 있는 것을 볼 수 있습니다. 이후에는 다시 1개로 줄어들게 됩니다.

HPA는 metrics-server가 제공하는 API로부터 메트릭 정보를 받아오는데, 이 주기는 쿠버네티스 컨트롤러에서 --horizontal-pod-autoscaler-sync-period 옵션을 통해 조정할 수 있으며, 기본값은 15초로 설정돼 있습니다. 하지만 애초에 metrics-server가 kubeletdml CAdvisor로부터 메트릭을 가져오는 주기가 크게 설정돼 있다면 HPA가 반응하는 속도가 늦어질 수 있습니다. metrics-server의 --metrics-resolution 옵션 값을 조절하면 메트릭을 수집하는 주기도 조절할 수 있으며, 이 값은 기본적으로 60초입니다.

HPA가 모든 상황에서 좋지는 않습니다. 애플리케이션을 초기화할 때 잠시 동안만 CPU를 과도하게 소모하는 JVM 기반 애플리케이션을 생각해보면 시작할 때 계속 포드가 스케일 아웃되고, 이는 계속해서 포드가 증식하는 연쇄 작용을 불러 일으킬 수도 있습니다.





출처
시작하세요! 도커/쿠버네티스(용찬호 저, 위키북스)
example

728x90
반응형
댓글
반응형
250x250
글 보관함
최근에 달린 댓글
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Total
Today
Yesterday
링크