공유 클라우드 환경에서 애플리케이션 배포, 관리 및 공용(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 자원량을 동일하게 갖고 있는 경우이며, 가장 우선순위가 높은 파드이다. 즉, 가장 나중에 죽는다.
- 최선적(Best-Effort) 파드
- 컨테이너에 대한 자원 값을 정의하느냐 생략하느냐에 따라 서비스 품질에 직접적인 영향을 미치기 때문에 이런 사항들을 고려해서 파드 자원 요구사항을 결정해야한다.
파드 우선순위를 고려한다.
- 파드 우선순위(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
- 우선순위 기능이 활성화되면 스케줄러가 파드를 노드에 배치하는 순서에 영향을 준다. 다수의 파드가 배치되기를 기다리는 경우 아래와 같은 순서로 파드가 배치된다.
- 우선순위 어드미션 컨트롤러(priority admission controller)는 priorityClassName 필드를 사용해 새로운 파드의 우선순위 값을 채운다.
- 스케줄러는 보류 중인 파드들이 저장된 큐에서 우선순위가 가장 높은 파드를 맨 처음에 오도록 정렬한다.
- 스케줄링 큐 안에 우선순위가 높은 보류 중인 파드가 선택되고, 다른 스케줄링 제약사항이 없는지 확인한다.
- 파드를 배치하기에 충분한 용량을 가진 노드가 하나도 없다면 스케줄러는 자원을 확보하고 우선순위가 높은 파드를 배치하기 위해 노드에서 실행되고 있는 우선순위가 낮은 파드를 제거한다.
- 스케줄러에 의해 파드가 대상 노드에 배치된다.
- 위와 같은 알고리즘을 통해 클러스터 관리자는 더 중요한 워크로드 파드를 효과적으로 제어할 수 있다.
- 낮은 우선순위 파드를 축출해 워커 노드에 공간을 확보함으로써 우선순위가 높은 파드를 먼저 배치할 수 있다.
- 서비스 품질은 사용 가능한 컴퓨팅 자원이 낮을 때 노드의 안정성을 유지하기 위해 큐블릿에 의해 주로 사용된다.
- 큐블릿은 파드를 축출(eviction) 하기 전에 먼저 서비스 품질을 고려하고 그 다음으로 파드의 PriorityClass를 고려한다.
- 큐블릿은 주기적으로(기본 10초) 노드의 리소스 상태를 모니터링하여 리소스가 부족할 경우 축출 대상의 파드를 선별한다.
- 스케줄러 축출 로직은 축출 대상을 선택할 때 파드의 서비스 품질을 고려하지 않고, 파드 우선순위를 고려한다.
- 배치를 기다리는 높은 우선순위 파드의 요구를 충족시키는 가장 낮은 우선순위 파드를 고르려고 시도한다.
- 참고
- https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#interactions-of-pod-priority-and-qos
- https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/
- https://zetawiki.com/wiki/K8s_Pod_Evicted
- https://kubernetes.io/ko/docs/concepts/configuration/pod-priority-preemption/
- 스케줄러가 파드를 축출 할 때는 Descheduler가 동작하게 되는데 파드 축출에 다음과 같은 파드는 제거하지 않는다.
- priorityClassName이 설정된 중요 파드
- 다시 생성되지 않는 독립 실행형 파드
- DaemonSet으로 실행된 파드
- 로컬 저장소가 있는 파드
- Pod Disruption Budget이 적용되어 있는 파드
- 한번에 작동해야하는 복제본의 최소 수(또는 백분율)를 설정
- 우선순위는 파드의 배치와 축출에 영향을 주는 설정이기 때문에 설정에 따라 예기치 못하게 파드를 제거해버릴 수도 있다. 이는 예측 범위 내의 SLA(Service level agreements)를 제공하지 못하게 할 수 있다.
- 악의적인 사용자가 가장 높은 우선순위의 파드를 배치하여 그 밖의 모든 파드를 축출해버릴 수도 있기 때문에 주의해야한다.
- 이를 방지하기 위해서 리소스 쿼터가 PriorityClass를 지원하도록 확장되었다.
- 선점 또는 축출되어서는 안 되는 중요한 시스템 파드를 위해 더 큰 우선순위 번호가 예약되어 있다.
프로젝트 자원을 고려한다.
- 공유 멀티테넌트 플랫폼에서의 작업은 특정 경계와 일부 사용자가 플랫폼의 모든 자원을 소비하지 못하게 하는 제어 장치가 있어야한다.
- ResourceQuota
- LimitRange
- 리소스 쿼터는 네임스페이스 내 집계된 자원 소비를 제한하기 위한 제약 조건을 제공한다. 아래 내용들을 제한할 수 있다.
- CPU나 메모리 등의 컴퓨팅 자원 전체 사용량
- 스토리지 전체 사용량
- 컨피그맵, 시크릿, 파드, 서비스와 같은 객체의 총 개수
- LimitRange는 각 자원 종류마다 최소 및 최대 자원량을 설정할 수 있고, 기본 값도 지정할 수 있다.
- requests와 limits 사이의 비율을 제어할 수도 있다. (overcommit level)
- requests와 limit 사이가 너무 클 경우 이와 같이 설정된 다수의 파드가 부하를 받아 더 많은 자원을 동시에 필요로 할 경우 (overcommit) 워커 노드에 부담을 줄 수 있다.
- LimitRange를 활용하면 어떤 컨테이너도 클러스터 노드가 제공하는 것보다 큰 자원을 요청하지 못하도록 제어할 수 있다.
- requests와 limits 사이의 비율을 제어할 수도 있다. (overcommit level)
용량을 계획한다.
- 운영 환경이 아닌 개발/스테이지 환경에서 하드웨어를 최적으로 사용하려면 최선적(Best-Effort)과 확장 가능(Burstable) 컨테이너를 주로 사용할 수 있다.
- 이런 동적인 환경에서는 많은 컨테이너가 동시에 시작되고 멈출 수 있다.
- 자원 부족으로 플랫폼이 컨테이너를 죽인다고 하더라도 그다지 치명적이지 않다.
- 안정적이고 예측 가능해야 하는 운영 환경에서는 주로 보장(Guaranteed) 컨테이너를 사용하고 약간의 확장 가능 컨테이너를 이용하면 된다.
- 이렇게 설정했음에도 컨테이너가 죽는다면 그것은 클러스터 용량을 확장하라는 신호일 확률이 높다.
- 환경이 달라지면 컨테이너 수도 달라지므로 오토스케일링, 빌드 잡, 인프라스트럭처 컨테이너 등을 위한 여분을 남겨둬야 할 수도 있다.
- 성공적인 클러스터 관리를 위해 서비스 자원 프로파일과 용량 계획은 장기적으로 함께 진행해야 한다.
'Work > 책 정리' 카테고리의 다른 글
[쿠버네티스패턴] 7장 배치 잡 (0) | 2020.09.27 |
---|---|
[리뷰] 데이터 과학을 위한 통계 (0) | 2020.09.27 |
[리뷰] 유닉스의 탄생 (0) | 2020.08.23 |
[리뷰] 팀 개발을 위한 Git, Github 시작하기 (0) | 2020.07.06 |
[리뷰] Head First Go (0) | 2020.04.30 |
댓글