「시작하세요! 도커/쿠버네티스」 학습 정리 - 11장 ~ 13장.

toto9602·2024년 2월 9일
0

Docker 공부하기

목록 보기
10/10

용찬호. 「 시작하세요! 도커/쿠버네티스」. 위키북스 를 읽으며, 학습한 내용을 정리하는 글입니다.
→ 본문의 내용은 모두 책의 내용에 대한 직/간접적 인용임을 밝힙니다.

11. 애플리케이션 배포를 위한 고급 설정

11.1 포드의 자원 사용량 제한

  • 컨테이너 오케스트레이션 툴의 장점 중 하나는 여러 대의 서버를 묶어 리소스 풀로 사용할 수 있다는 점!
    • 클러스터의 CPU나 메모리 등의 자원이 부족할 때, 필요한 용량만큼의 서버를 동적으로 추가하여 수평적 확장이 가능!
    • 이와 함께, 클러스터 내부의 컴퓨팅 자원 활용률(Utilization)을 높여야 한다!

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

  • 쿠버네티스는 기본적으로 도커를 컨테이너 런타임으로 사용하기 때문에, 포드를 생성할 때 도커와 동일한 원리로 CPU, 메모리의 최대 사용량을 제한할 수 있다.
    • 도커는 cgroup을 사용해 컨테이너의 자원 사용량을 제한
    • 자원 할당량을 설정하지 않으면, 포드의 컨테이너가 노드의 물리 자원을 모두 사용할 수 있기에, 노드의 자원이 모두 고갈될 수 있음

→ 포드 자체에 자원 사용량을 명시적으로 설정하기

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" 

docker run --memory 256m과 같다.
cpu: 1000m 은 1개의 CPU, 즉 1000밀리코어를 의미

  • docker run --cpus 1과 같다.

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

  • 쿠버네티스의 자원 관리에서 Requests는 '적어도 이 만큼의 자원은 컨테이너에게 보장되어야 한다'를 의미!
    • 자원의 오버커밋(OverCommit)을 가능하게 만드는 기능

cf. 자원의 오버커밋(OverCommit)이란?

  • 한정된 컴퓨팅 자원을 효율적으로 사용하기 위한 방법
  • 사용할 수 있는 자원보다 더 많은 양을 가상 머신이나 컨테이너에게 할당함으로써, 전체 자원의 사용률(Utilization)을 높이는 방법이다.
    • 가령, 서버의 메모리 용량이 1GB밖에 되지 않아도, 메모리 제한이 750MB인 포드를 두 개 생성할 수도 있다!

→ 이런 경우에, 한 컨테이너가 500MB를 사용 중일 때, 다른 컨테이너가 750MB를 사용하려 한다면,
메모리 충돌 및 비정상적 에러 발생!

각 컨테이너가 사용을 보장받을 수 있는 경계선을 함께 정해야 한다.

[ Request 지정하여 최소 보장 자원 정하기 ]

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

→ requests에서 128Mi를, limits에서 256Mi를 설정했으므로
'최소한 128Mi의 메모리 사용은 보장되지만, 유휴 메모리 자원이 있다면 최대 256Mi까지 사용할 수 있다'는 뜻!

** requests는 컨테이너가 보장받아야 하는 최소한의 자원이므로,
노드의 총 크기보다 더 많은 양의 requests를 할당할 수는 없다.
→ 포드를 할당할 때 사용되는 자원 할당 기준은 limit이 아닌 requests

11.1.3 CPU 자원 사용량의 제한 원리

  • 쿠버네티스에서는 CPU를 m(밀리코어) 단위로 제한하고, 1개의 CPU는 1000m이다.
...
	resources:
    	limits:
        	memory: "256Mi"
            cpu: "1000m"     # 최대 1개의 CPU만큼 사용 가능
        requests:
        	memory: "128Mi"
            cpu: "500m"  # 최소한 0.5개의 CPU만큼의 사용을 보장

resources.requests.cpu 1000m은 포드의 컨테이너는 최대 1개 CPU를 사용함을 의미
= 아래 명령어와 같다!
docker run --cpus 1
docker run --cpu-period 100000 --cpu-runtime 100000

+) CPU의 Requests는 --cpu-shares 옵션과 같다.

[ 자원의 경합(Contention) 상황 ]

  • 컨테이너 A가 Request보다 더 많은 CPU를 사용 중에, 컨테이너 B가 Request만큼 CPU를 사용하고자 하는 상황
    • 컨테이너 A에 CPU 스로틀(throttle)이 발생
    • 컨테이너 B는 Request에 명시한 만큼의 CPU 비율을 사용할 수 있다.

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

  • CPU 사용량에 경합이 발생하면 일시적으로 컨테이너 내부의 프로세스에 CPU 스로틀이 걸릴 뿐, 컨테이너 자체엔 문제가 없다.

  • 메모리의 사용량에 경합이 발생하는 건 문제!

    • 메모리는 CPU와 달리 압축 불가능한 자원임
    • 쿠버네티스는, 가용 메모리를 확보하기 위해 우선순위가 낮은 포드 또는 프로세스를 강제로 종료한다.
    • 강제로 종료된 포드는 다른 노드로 옮겨가는데, 이를 퇴거(Eviction)이라고 한다.
  • 그렇다면, "노드에 메모리 자원이 부족해지면 어떤 포드나 프로세스가 먼저 종료돼야 하는가"가 중요!
    → 쿠버네티스는 포드의 컨테이너에 설정된 Limit과 Request의 값에 따라 우선순위를 계산
    → 내부적으로 포드의 우선순위를 구분하기 위해 3가지 종류의 QoS(Quality of Service) 클래스를 포드에 설정!

[ 1. Guaranteed 클래스 ]

  • 포드의 컨테이너에 설정된 Limit과 Request의 값이 완전히 동일할 때 부여됨
  • Requests와 Limit의 값이 동일하므로, 할당받은 자원을 문제없이 사용 가능!
  • Guaranteed 클래스의 포드 내부에서 실행되는 프로세스들은 모두 기본 OOM(oom_score_adj)가 -998로 설정
    • 도커 데몬이나 kubelet의 프로세스와 거의 동일한 레벨로 보호받으므로, 노드에서 메모리가 고갈되어도 시스템 컴포넌트가 요구하지 않는 한 포드나 프로세스가 강제로 종료되지 않음.

[ 2. BestEffort 클래스 ]

  • Request와 Limit을 아예 설정하지 않은 포드에 설정됨
  • Limit 값을 설정하지 않았으므로 노드에 유휴 자원이 있다면, 제한 없이 모든 자원을 사용 가능!
  • 그렇지만 Request 또한 설정하지 않았으므로, 사용을 보장받을 수 있는 자원이 존재하지 않는다.
    • 때에 따라 자원을 전혀 사용하지 못할 수 있음!

[ 3. Burstable 클래스 ]

  • request와 Limit이 설정되어 있지만, Limit의 값이 Request보다 큰 포드를 의미
  • Request에 지정된 자원만큼 사용을 보장받을 수 있으나, 상황에 따라 Limit까지 자원을 사용할 수 있음.
    • 필요에 따라 순간적으로 자원의 한계를 확장해 사용할 수 있음! = burstable

기본적으로 포드의 우선순위는 Guaranteed가 가장 높고, Burstable과 BestEffort가 그 뒤이다.
하지만, Burstable과 BestEffort 클래스의 포드는 현재 메모리를 얼마나 사용하고 있는지에 따라 우선순위가 역전될 수 있음!
→ 포드가 메모리를 많이 사용하면 할수록 우선순위가 낮아진다.

11.1.5 ResourceQuota와 LimitRanger

  • 쿠버네티스를 여러 사람 또는 개발팀이 함께 사용하고 있다면, 각 네임스페이스에서 할당할 수 있는 자원의 최대 한도 또는 범위를 설정해야 함.
    → 이를 위해 ResourceQuote와 LimitRanger라는 오브젝트를 이용해 자원 사용량 관리를 가능케 하는 기능을 제공

  • ResourceQuota는 네임스페이스에 종속되는 오브젝트이기에, 네임스페이스별로 생성해야 한다.

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"
  • 1500m : 위 예시에서는 500m의 limits.cpu를 가지는 3개의 포드까지는 생성할 수 있으나, 그 뒤로는 limits.cpu를 가지는 포드를 더 생성할 수 없음.

[ cf. 디플로이먼트 ]

apiVersion: apps/v1
kind: Deployment
...
	resources:
    	limits:
        	memory: "3000Mi"
            cpu: "1000m"
            requests:
            memory: "128Mi"
            cpu: "500m"
  • ResourceQuote가 namespace에서 최대 100Mi의 메모리만 사용할 수 있도록 설정한 상황
    • 이 상황에도 위 디플로이먼트는 생성이 됨!
    • 하지만 해당 디플로이먼트로부터 생성된 포드가 없다.

포드를 생성하는 주체는 디플로이먼트가 아니라 레플리카셋

  • 디플로이먼트는 포드를 생성하기 위한 레플리카 셋의 메타데이터를 선언적으로 가지고 있을 뿐, 디플로이먼트 리소스가 직접 포드를 생성하지는 않는다.
  • 포드 생성 거부에 대한 에러 로그는 레플리카 셋에 남는다.

[ 11.1.5.2 LimitRange로 자원 사용량 제한 ]

  • 포드의 컨테이너에 CPU나 메모리 할당량이 설정돼 있지 않은 경우, 컨테이너에 자동으로 기본 Request 또는 Limit 값을 설정할 수 있다.
  • 포드 또는 컨테이너의 CPU, 메모리, 퍼시스턴트 볼륨 클레임 스토리지 크기의 최솟값/최댓값 설정 가능
apiVersion: v1
kind: LimitRange
metadata:
	name: mem-limit-range
spec:
	limits:
    - default: # 자동으로 설정될 기본 Limit 값
      memory: 256Mi
      cpu: 200m
    defaultRequest:  # 자동으로 설정될 기본 Request 값
    	memory: 128Mi
        cpu: 100m
    max:   # 자원 할당량의 최댓값
    	memory: 1Gi
        cpu: 1000m
    min:   # 자원 할당량의 최솟값
    	memory: 16Mi
        cpu: 50m
    type: Container    # 각 컨테이너에 대해서 적용

11.1.6 ResourceQuote, LimitRanger의 원리 : Admission Controller

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

cf. 유명한 서비스 메쉬 솔루션인 Istio는 어드미션 컨트롤러를 통해 포드에 프록시 사이드카 컨테이너를 주입하는 방법을 사용

11.2 쿠버네티스 스케줄링

  • 스케줄링이란, 컨테이너나 가상 머신과 같은 인스턴스를 새롭게 생성할 때, 이를 어느 서버에 생성할 것인지 결정하는 일!

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

  • 사용자가 kubectl이나 API 서버로 포드 생성 요청을 전송하면 어떠한 일이 일어나는가?
  1. ServiceAccount, RoleBinding 등의 기능을 이용해 포드 생성을 요청한 사용자의 인증 및 인가 작업을 수행합니다.
  2. ResourceQuota, LimitRanger와 같은 어드미션 컨트롤러가 해당 포드 요청을 적절히 변형(Mutate)하거나 검증(Validate)합니다.
    3. 어드미션 컨트롤러의 검증을 통과해 최종적으로 포드 생성이 승인됐다면 쿠버네티스는 해당 포드를 워커 노드 중 한 곳에 생성합니다.
    → 3번에서 포드의 스케줄링이 실행된다!

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

  • 노드 필터링 : 포드를 할당할 수 있는 노드와 그렇지 않은 노드를 분리해 걸러내는 단계
  • 노드 스코어링 : 쿠버네티스의 소스코드에 미리 정의된 알고리즘의 가중치에 따라서 노드의 점수를 계산

→ 대부분은 스케줄링 조건을 포드의 YAML 파일에 설정함으로써 노드 필터링 단계에 적용할 수 있도록 구성

  • NodeSelector와 Node Affinity, Pod Affinity
    • nodeName과 nodeSelector를 사용한 스케줄링 방법
    • Node Affinity를 이용한 스케줄링 방법
    • Pod Affinity를 이용한 스케줄링 방법
    • Pod Anti-affinity를 이용한 스케줄링 방법

11.2.4 Taints와 Tolerations 사용하기

  • 특정 노드에 얼룩(Taint)를 지정함으로써 해당 노드에 포드가 할당되는 것을 막는 기능

  • 해당 Taints에 대응하는 Tolerations를 포드에 설정하면 Taints가 설정된 노드에도 포드를 할당할 수 있다.

  • 쿠버네티스는 기본적으로 다양한 Taint를 노드에 설정한다!

    • 대표적으로, 마스터 노드에 기본적으로 설정된 Taint가 있음.
    • 마스터 노드에 Taints를 설정함으로써 포드가 할당되는 것을 방지한다.

11.2.5 Cordon, Drain 및 PodDistributionBudget

[ cordon ]
kubectl cordon <노드 이름>
→ 해당 노드에 더 이상 포드가 스케줄링되지 않는다.
→ 해당 노드에서 이미 실행 중인 포드가 종료되지는 않음!

[ drain ]

  • cordon처럼 해당 노드에 스케줄링을 금지한다는 점은 같다.
  • 노드에서 기존에 실행 중이던 포드를 다른 노드로 옮겨가도록 퇴거(Eviction)을 수행한다는 점이 다름!

[ PodDistributionBudget ]

  • kubectl drain 명령어 등으로 인해 포드에 퇴거(Eviction)이 발생할 때, 특정 개수 또는 비율만큼의 포드는 반드시 정상적인 상태를 유지하기 위해서 사용

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

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

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

  • 일시적으로 사용자의 요청을 처리하지 못해도 괜찮은 애플리케이션이라면 쿠버네티스에서 제공하는 Recreate 사용 가능

    • 기존 버전의 포드를 모두 삭제한 뒤, 새로운 버전의 포드를 생성하는 방식
  • 이 방식은 일시적으로 요청을 처리할 수 없으므로, 쿠버네티스는 포드를 조금씩 삭제하고 생성하는 롤링 업데이트 기능을 제공

    • 디플로이먼트의 버전 업데이트 시에는 기본적으로 롤링 업데이트를 사용함
    • 세부 옵션을 설정하려면 디플로이먼트 정의하는 YAML 파일에서 strategy 타입 항목을 명시적으로 RollingUpdate로 지정
...
spec:
	replicas: 3
    strategy: 
    	type: RollingUpdate
        rollingUpdate:
        	maxSurge: 2
            maxUnavailable: 2
  • maxUnavailable : 롤링 업데이트 도중 사용 불가능한 상태가 되는 포드의 최대 개수를 설정
    • 즉, 롤링 업데이트 도중에도 요청이 처리될 수 있도록 실행 중인 포드의 개수가 일정 값 이하로 내려가지 않도록 유지
  • maxSurge : 롤링 업데이트 도중 전체 포드의 개수가 디플로이먼트의 replicas 값보다 얼마나 많이 존재할 수 있는지 설정
    • 새로운 버전의 포드가 얼마나 많이 생성될 수 있는가

[ 블루 그린 배포 사용하기 ]

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

11.3.2 포드의 생애 주기(Lifecycle)

[ 11.3.2.1 포드의 상태와 생애 주기 ]

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

12. 커스텀 리소스와 컨트롤러

  • 쿠버네티스에서는 포드와 같이 기본적인 리소스 외에도 직접 리소스의 종류를 정의해 사용할 수도 있는데, 이를 커스텀 리소스라고 함.
  • 커스텀 리소스를 제대로 사용하려면 컨트롤러라는 별도의 컴포넌트를 이해 & 구현해야 한다.

12.1 쿠버네티스 컨트롤러의 개념과 동작 방식

[ 명령형(Imperative) Vs. 선언적(Declarative) ]

< 명령형 >

  • docker run 처럼 특정 명령을 처리하는 주체와 통신해 그 작업을 수행하고 결괏값을 돌려받는 방식
  • 도커 데몬을 익히기 위해 사용했던 대부분의 명령어!
    -kubectl create -f 또한 명령형 방식의 대표적인 예
    • 새로운 리소스를 생성해라!라는 구체적인 동작을 의미

< 선언형 >

  • 쿠버네티스가 지향하는 방식!
  • 최종적으로 도달해야 하는 바람직한 상태(Desired State)를 정의한 뒤, 현재 상태(Current State)가 바람직한 상태와 다를 경우 이를 일치하도록 만드는 방법
    • kubectl apply 명령어가 대표적인 예시이다.
    • kubectl apply -f 뒤에 따라오는 YAML 파일은 '최종적으로 도달해야 하는 상태'를 의미하며, 쿠버네티스는 현재 상태가 해당 YAML 파일과 일치하도록 특정 동작을 수행한다.
    • 최종적으로 완성돼야 하는 상태가 되기 위해 어떠한 동작을 취할지는 쿠버네티스에서 컨트롤러라고 불리는 개체가 내부적으로 결정한다.
    • "바람직한 상태"를 kubectl apply로 정의하면 컨트롤러가 현재 상태가 바람직한 상태가 되도록 만드는 것!

12.2 커스텀 리소스에 대한 개념

  • 직접 정의해 사용할 수 있는 사용자 정의 리소스

[ 커스텀 리소스를 사용하기 위한 단계 ]
1. 현재 상태를 커스텀 리소스에 대한 바람직한 상태로 변화시킬 수 있는 컨트롤러를 구현하고, 컨트롤러를 실행합니다.
2. 커스텀 리소스의 상세 정보를 정의하는 CRD(Custom Resource Definition) 리소스를 생성합니다.
3. CRD에 정의된 데이터에 맞춰 커스텀 리소스를 생성합니다.
4. 1번에서 실행한 컨트롤러는 커스텀 리소스의 생성을 감지하고, 커스텀 리소스가 원하는 바람직한 상태가 되도록 적절한 작업을 수행합니다.

12.4 커스텀 리소스와 컨트롤러

  • 커스텀 리소스 그 자체는 etcd에 저장된 단순한 데이터로, 실제로 동작하고 있는 포드나 서비스는 아님
  • 커스텀 리소스를 생성했을 때 특정 동작을 수행하도록 정의하는 컨트롤러를 별도로 구현해야만 커스텀 리소스가 의미를 갖는다!

13. 포드를 사용하는 다른 오브젝트들

13.1 잡(Jobs)

  • 특정 동작을 수행하고 종료해야 하는 작업을 위한 오브젝트
  • 잡에서 원하는 최종 상태는 '특정 개수의 포드가 실행 중인 것'이 아닌, '포드가 실행되어 정상적으로 종료되는 것'
apiVersion: batch/v1
kind: Job
metadata:
	name: job-hello-world
spec:
	template:
    	spec:
        	restartPolicy: Never
            containers:
            - image: busybox
              args: ["sh", "-c", "echo Hello, World && exit 0"]
              name: job-hello-world

[ 크론잡(CronJobs)으로 잡을 주기적으로 실행하기 ]

  • 리눅스에서 흔히 쓰이는 크론(Cron)의 스케줄 방법을 그대로 사용!
apiVersion: batch/v1beta1
kind: CronJob
metadata:
	name: cronjob-example
spec:
	schedule: "*/1 * * * *"   # Job의 실행 주기 
    jobTemplate:  # 실행될 Job의 설정 내용(spec)
    	spec:	
        	template:
            	spec:
                	restartPolicy: Never
                    containers:
                    - name: cronjob-example
                      image: busybox
                      args: ["sh", "-c", "date"]

13.2 데몬셋(DaemonSets)

  • 쿠버네티스의 모든 노드에 동일한 포드를 하나씩 생성하는 오브젝트
  • 로깅, 모니터링, 네트워킹 등을 위한 에이전트를 각 노드에 생성해야 할 때 유용

13.3 스테이트풀셋(StatefulSets)

  • 상태를 갖는(Stateful) 포드를 관리하기 위한 오브젝트
  • 디플로이먼트에서 생성된 포드는 랜덤한 이름이 붙여지지만, 스테이트풀셋으로부터 생성된 포드들의 이름에는 0,1,2..처럼 숫자가 붙어있다.
    • 이 숫자를 통해 각 포드를 고유하게 식별함!
profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글