본문 바로가기

Work/책 정리

[쿠버네티스패턴] 2장 예측 범위 내의 요구사항

 

    

공유 클라우드 환경에서 애플리케이션 배포, 관리 및 공용(Coexistence)을 성공적으로 수행하려면 애플리케이션 자원 요구사항과 런타임 의존성을 명확히 식별하고 정의해야 한다.
  • 여기서 공용(Coexistence)이란?
    • Coexistence를 찾아보면 쿠버네티스 보다는 주파수, 블루투스와 같은 신호나 기기 간의 충돌 현상을 이야기하는 글들이 많다.
    • 위 의미와 빗대어 쿠버네티스에서의 Coexistence를 생각해보면 다양한 어플리케이션이 Pod의 형태로 같은 노드에 배치되는 것이라 볼 수 있다.
  • 자원 요구사항은 쿠버네티스 위에서 동작하는 어플리케이션들의 CPU와 메모리 사용량을 의미하고, 런타임 의존성은 Pod 실행 시점에 결정되는 볼륨이나 ConfigMap 과 같은 의존성을 의미한다. 이 의존성에 따라 Pod 스케쥴에 영향이 간다.

 

  • 예측 범위 내의 요구사항 패턴이란 물리적으로 필요한 하드 런타임 의존성이나 자원 요구사항과는 상관 없이 애플리케이션 요구사항을 선언하는 방법에 관한 것이며 적합한 노드를 찾기 위해 반드시 필요하다.

 

문제

쿠버네티스 상에서 실행되는 각 어플리케이션들은 언어에 따라 각기 요구사항이 다르다.
  • 컴파일 언어 : 실행하기 전에 프로그램 코드를 기계어로 번역
  • 인터프리터 언어 : 실행 중 프로그래밍 언어를 읽어가면서 해당 기능에 대응하는 기계어 코드를 실행
  • JIT(just-in-time) 런타임 : 프로그램을 실제 실행하는 시점에 기계어로 번역
    • JIT은 컴파일언어와 인터프린터 언어를 혼합한 방식으로 생각하면 된다.
    • 인터프리터처럼 매번 실행 시점에 기계어로 번역하는 것이 아니라 코드를 캐싱하여 여러번 불릴 떄 매번 기계어 코드를 생성하는 것을 방지한다.
    • JIT은 자바에서 사용되고 있는데 컴파일을 통해 바이트 코드가 작성되면 실행 시점에 JIT 컴파일러가 바이트 코드를 기계어로 번역한다.
  • 언어의 종류 보다는 도메인이나 애플리케이션의 비즈니스 로직, 실제 세부적인 구현 사항이 훨씬 더 중요하다.

 

컨테이너가 최적의 기능을 수행하는데 필요한 자원량을 예측하기가 어렵다.
  • 개발자가 테스트를 수행한 후에야 비로소 서비스 구현을 위한 자원 필요량을 알 수 있다.
    • 고정된 CPU, 메모리 소비
    • 스파이크 치는 경우
    • 영구적인 스토리지의 필요성
    • 특정 포트 번호 사용
  • 모든 애플리케이션 특성을 정의하고 이를 쿠버네티스와 같은 관리 플랫폼으로 전달하는 것은 클라우드 네이티브 애플리케이션의 기본 전제조건이다.
  • 자원 요구사항 외에도 애플리케이션 런타임은 데이터 스토리지 또는 애플리케이션 설정 같은 플랫폼 관리 기능이 필요하다.
    • 데이터 스토리지 = PV
    • 애플리케이션 설정 = ConfigMap, Secret

     

해결책

모든 런타임 의존성을 정의한다.

 

가장 일반적인 런타임 의존성은 파일 스토리지이다.
  • 컨테이너의 기본 파일시스템은 컨테이너가 종료되면 삭제된다.
  • 가장 간단한 볼륨 타입은 emptyDir이며 Pod가 살아있는 동안 유지된다.
    • 나는 로그 수집 시 사용했음
  • 영구적으로 데이터를 보존해야하는 경우에는 다른 종류의 스토리지 매커니즘을 지원하는 볼륨이 필요하다.
    • Node의 파일 시스템을 사용하는 hostPath나 AWS 환경에라면 EBS, EFS 등의 파일시스템도 연동할 수 있다.
  • 볼륨을 사용하려면 먼저 PersistentVolume을 생성하고 해당 볼륨에 얼만큼의 볼륨 사이즈를 사용할지 요청하는 PersistentVolumeClaim을 생성하여 아래와 같이 Pod 매니페스트에 설정한다.
    apiVersion: v1
    kind: Pod
    metadata:
      name: random-generator
    spec:
      containers:
      - image: k8spatterns/random-generator:1.0
        name: random-generator
        volumeMounts:
        - mountPath: "/logs"
          name: log-volume
      volumes:
      - name: log-volume
        persistentVolumeClaim:
          claimName: random-generator-log
    
  • 스케줄러는 파드가 요청한 볼륨 종류를 판단하여 파드가 실행될 위치에 영향을 미친다.
    • 만일 파드가 hostPath로 지정되어 있고 이미 다른 Pod가 동일한 pvc로 대상 노드에 마운트 되었다면 새로 스케쥴된 Pod는 노드에 스케쥴 될 수 없다.
    • 또한 노드가 지원하지 않는 pvc를 요청한 경우에도 파드는 스케줄 될 수 없다.

 

다른 타입의 의존성으로 설정(Configuration)이 있다.
  • 쿠버네티스에서 권장하는 것은 ConfigMap
  • 애플리케이션 설정에는 환경변수를 사용하거나 파일시스템을 통한 설정을 사용한다.
    • 두가지 모두 ConfigMap으로 지원가능하고, 런타임 의존성을 적용한다.
    • 컨테이너에 설정된 ConfigMap이 존재하지 않는다면 파드는 스케줄 되지 않는다.
  • 아래와 같이 환경 변수가 설정된 ConfigMap을 컨테이너에 적용하여 사용할 수 있다.
    apiVersion: v1
    kind: Pod
    metadata:
      name: random-generator
    spec:
      containers:
      - image: k8spatterns/random-generator:1.0
        name: random-generator
        env:
        - name: PATTERN  # 컨테이너 안에서 사용될 환경변수 명
          valueFrom:
            configMapKeyRef:
              name: random-generator-config  # ConfigMap 이름
              key: pattern  # ConfigMap에 정의된 값을 참조하기 위한 key 값
    
  • Secret도 ConfigMap과 유사하며 Secret은 데이터를 암호화하여 안전하게 저장한다.

 

자원 요구사항을 계산한다.

 

쿠버네티스 컨텍스트 내에서의 컴퓨팅 자원 (Compute Resources)을 구별해서 관리해야 한다.
  • 압축 가능 자원 (compressible resource)
    • CPU나 네트워크 대역폭처럼 제어 가능한 리소스를 의미한다.
    • 너무 많이 소비한다면 병목현상이 나타난다.
  • 압축 불가능 자원 (incompressible resource)
    • 메모리처럼 제어 불가능한 자원을 의미한다.
    • 너무 많이 사용한다면 그 컨테이너는 죽어버린다.
      • 애플리케이션에 할당된 메모리 해제를 요청할 수 있는 방법이 없기 때문

 

최소 자원량(requests)과 최대 자원량(limits)을 지정해야 한다
  • 각 컨테이너 정의에는 요청과 제한의 형태로 필요한 CPU 양과 메모리양을 지정할 수 있다.
  • requests는 스케줄러가 파드를 노드에 배치시킬 때 사용된다.
    apiVersion: v1
    kind: Pod
    metadata:
      name: random-generator
    spec:
      containers:
      - image: k8spatterns/random-generator:1.0
        name: random-generator
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
          limits:
            cpu: 200m
            memory: 200Mi
    
    • CPU 1 Core = 1000m
  • 스케줄러는 해당 파드와 파드 안의 모든 컨테이너 요청 자원량을 합산해 충분히 수용할 용량이 있는 노드들만 고려한다.
  • 서비스 품질 (Quality of Service, QoS)
    • 최선적(Best-Effort) 파드
      • requests와 limits를 지정하지 않은 경우이며, 가장 낮은 우선순위로 고려된다.
      • 파드가 위치한 노드의 압축 불가능 자원이 전부 사용되어 없어지면 가장 먼저 죽는다.
    • 확장 가능(Burstable) 파드
      • requests와 limits 값을 달리 설정한 경우이며, 노드가 압축 불가능 자원에 대한 압박을 받는 경우 이 파드는 최선적 파드가 남아있지 않다면 죽을 확률이 높다.
      • 보통 limits는 requests보다 값이 크다.
        • 최소한의 자원 보장을 받지만 가능한 경우 limits까지 더 많은 자원을 소비하려고 한다.
    • 보장(Guaranteed) 파드
      • requests와 limits 자원량을 동일하게 갖고 있는 경우이며, 가장 우선순위가 높은 파드이다. 즉, 가장 나중에 죽는다.
  • 컨테이너에 대한 자원 값을 정의하느냐 생략하느냐에 따라 서비스 품질에 직접적인 영향을 미치기 때문에 이런 사항들을 고려해서 파드 자원 요구사항을 결정해야한다.

 

파드 우선순위를 고려한다.

  • 파드 우선순위(Priority)를 사용하면 다른 파드와 비교해 상대적으로 파드의 중요성을 지정할 수 있다.
  • 파드 우선순위는 파드가 스케줄되는 순서에 영향을 준다.

 

apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
  name: high-priority

# 우선순위 값. 이 값이 상대적으로 파드의 중요성을 나타내는 값이기 때문에 
#높을 수록 더 중요한 파드임을 의미한다.
value: 1000  

globalDefault: false
description: This is a very high priority Pod class
---
apiVersion: v1
kind: Pod
metadata:
  name: random-generator
	labels:
		env: random-generator
spec:
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
	priorityClassName: high-priority
  • 우선순위 기능이 활성화되면 스케줄러가 파드를 노드에 배치하는 순서에 영향을 준다. 다수의 파드가 배치되기를 기다리는 경우 아래와 같은 순서로 파드가 배치된다.
    1. 우선순위 어드미션 컨트롤러(priority admission controller)는 priorityClassName 필드를 사용해 새로운 파드의 우선순위 값을 채운다.
    2. 스케줄러는 보류 중인 파드들이 저장된 큐에서 우선순위가 가장 높은 파드를 맨 처음에 오도록 정렬한다.
    3. 스케줄링 큐 안에 우선순위가 높은 보류 중인 파드가 선택되고, 다른 스케줄링 제약사항이 없는지 확인한다.
    4. 파드를 배치하기에 충분한 용량을 가진 노드가 하나도 없다면 스케줄러는 자원을 확보하고 우선순위가 높은 파드를 배치하기 위해 노드에서 실행되고 있는 우선순위가 낮은 파드를 제거한다.
    5. 스케줄러에 의해 파드가 대상 노드에 배치된다.
  • 위와 같은 알고리즘을 통해 클러스터 관리자는 더 중요한 워크로드 파드를 효과적으로 제어할 수 있다.
  • 낮은 우선순위 파드를 축출해 워커 노드에 공간을 확보함으로써 우선순위가 높은 파드를 먼저 배치할 수 있다.
  • 서비스 품질은 사용 가능한 컴퓨팅 자원이 낮을 때 노드의 안정성을 유지하기 위해 큐블릿에 의해 주로 사용된다.
    • 큐블릿은 파드를 축출(eviction) 하기 전에 먼저 서비스 품질을 고려하고 그 다음으로 파드의 PriorityClass를 고려한다.
    • 큐블릿은 주기적으로(기본 10초) 노드의 리소스 상태를 모니터링하여 리소스가 부족할 경우 축출 대상의 파드를 선별한다.
  • 스케줄러가 파드를 축출 할 때는 Descheduler가 동작하게 되는데 파드 축출에 다음과 같은 파드는 제거하지 않는다.
    • priorityClassName이 설정된 중요 파드
    • 다시 생성되지 않는 독립 실행형 파드
    • DaemonSet으로 실행된 파드
    • 로컬 저장소가 있는 파드
    • Pod Disruption Budget이 적용되어 있는 파드
      • 한번에 작동해야하는 복제본의 최소 수(또는 백분율)를 설정
  • 우선순위는 파드의 배치와 축출에 영향을 주는 설정이기 때문에 설정에 따라 예기치 못하게 파드를 제거해버릴 수도 있다. 이는 예측 범위 내의 SLA(Service level agreements)를 제공하지 못하게 할 수 있다.
    • 악의적인 사용자가 가장 높은 우선순위의 파드를 배치하여 그 밖의 모든 파드를 축출해버릴 수도 있기 때문에 주의해야한다.
    • 이를 방지하기 위해서 리소스 쿼터가 PriorityClass를 지원하도록 확장되었다.
      • 선점 또는 축출되어서는 안 되는 중요한 시스템 파드를 위해 더 큰 우선순위 번호가 예약되어 있다.

       

프로젝트 자원을 고려한다.

  • 공유 멀티테넌트 플랫폼에서의 작업은 특정 경계와 일부 사용자가 플랫폼의 모든 자원을 소비하지 못하게 하는 제어 장치가 있어야한다.
    • ResourceQuota
    • LimitRange
  • 리소스 쿼터는 네임스페이스 내 집계된 자원 소비를 제한하기 위한 제약 조건을 제공한다. 아래 내용들을 제한할 수 있다.
    • CPU나 메모리 등의 컴퓨팅 자원 전체 사용량
    • 스토리지 전체 사용량
    • 컨피그맵, 시크릿, 파드, 서비스와 같은 객체의 총 개수
  • LimitRange는 각 자원 종류마다 최소 및 최대 자원량을 설정할 수 있고, 기본 값도 지정할 수 있다.
    • requests와 limits 사이의 비율을 제어할 수도 있다. (overcommit level)
      • requests와 limit 사이가 너무 클 경우 이와 같이 설정된 다수의 파드가 부하를 받아 더 많은 자원을 동시에 필요로 할 경우 (overcommit) 워커 노드에 부담을 줄 수 있다.
    • LimitRange를 활용하면 어떤 컨테이너도 클러스터 노드가 제공하는 것보다 큰 자원을 요청하지 못하도록 제어할 수 있다.

     

용량을 계획한다.

  • 운영 환경이 아닌 개발/스테이지 환경에서 하드웨어를 최적으로 사용하려면 최선적(Best-Effort)과 확장 가능(Burstable) 컨테이너를 주로 사용할 수 있다.
    • 이런 동적인 환경에서는 많은 컨테이너가 동시에 시작되고 멈출 수 있다.
    • 자원 부족으로 플랫폼이 컨테이너를 죽인다고 하더라도 그다지 치명적이지 않다.
  • 안정적이고 예측 가능해야 하는 운영 환경에서는 주로 보장(Guaranteed) 컨테이너를 사용하고 약간의 확장 가능 컨테이너를 이용하면 된다.
    • 이렇게 설정했음에도 컨테이너가 죽는다면 그것은 클러스터 용량을 확장하라는 신호일 확률이 높다.
  • 환경이 달라지면 컨테이너 수도 달라지므로 오토스케일링, 빌드 잡, 인프라스트럭처 컨테이너 등을 위한 여분을 남겨둬야 할 수도 있다.
  • 성공적인 클러스터 관리를 위해 서비스 자원 프로파일과 용량 계획은 장기적으로 함께 진행해야 한다.