우리가 클라우드 환경에서 어플리케이션을 배포하고 관리할 때 주의해야 할 점은
어플리케이션의 자원 요구사항과 런타임 의존성 입니다.
우리가 사용하는 워커 노드의 자원은 한정되어있습니다.
하나의 파드만 사용한다면 상관 없겠지만 여러 개의 파드를 사용한다고 가정해보겠습니다.
하나의 파드에 사용되는 어플리케이션의 메모리와 CPU 사용량이
지원하는 워커 노드의 자원보다 더 크다면 최악의 경우에는 어플리케이션이 종료 될 수도 있을 것입니다.
예측 범위 내의 요구사항 패턴은 하드 런타임 의존성이나 자원 요구사항과는 상관 없이,
어플리케이션 요구사항을 선언하는 방법에 관한 것입니다.
이를 통해서 쿠버네티스가 클러스터 내에서 적합한 워커노드를 잘 찾을 수 있을 것입니다.
어플리케이션을 컨테이너 이미지로 만들고 실행할 수만 있다면,
쿠버네티스는 프로그래밍 언어에 관계없이 어플리케이션을 관리할 수 있습니다.
하지만 언어에 따라 필요한 자원과 소비량은 다르기 때문에
클러스터의 용량을 초과하거나 비슷해질 때 어플리케이션은 느려지거나 종료 될 수도 있습니다.
또한 자원에 대한 요구사항 이외에도 런타임에 사용할수있는 데이터 스토리지 및 어플리케이션 설정을 관리할 수 있는 방법이 필요합니다.
이러한 문제를 해결하기 위해 우리는 컨테이너의 런타임 요구사항을 파악하고 자원 프로파일을 만들어야할 필요가 있습니다.
먼저 컨테이너의 런타임 요구사항을 알아야 하는 이유는 다음과 같습니다.
👍 쿠버네티스에서는 런타임 의존성이 정의되고 자원 요구사항이 계산되면 효율적인 하드웨어 사용을 위해 클러스터 내에 컨테이너 실행 위치를 결정할 수 있습니다.
자원 프로파일이 필수적인 이유는 다음과 같습니다.
👍 서비스 요구사항과 총 서비스 수에 근거해 용량 계획을 세우면, 전체 클러스터에 대해 비용 효율이 높은 운영을 할 수 있습니다.
이를 달성하기 위해 먼저 런타임 의존성을 선언하는 방법을 알아보겠습니다.
런타임 의존성을 선언하는 방법 다양하지만 이번 글에서는 3가지 정도를 알아보도록 하겠습니다.
가장 일반적인 런타임 의존성은 볼륨을 사용하는 것입니다.
어플리케이션이 볼륨을 사용해야하는 경우 다음 yaml과 같이 명시적으로 의존성을 선언해야 합니다.
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- image: k8s.gcr.io/test-webserver
name: test-container
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
스케줄러는 파드가 요청한 볼륨의 종류를 판단하여 노드에 배치하기 때문에
클러스터에서 제공하지 않는 종류의 볼륨이라면 파드는 실행되지 않습니다.
따라서 볼륨은 런타임 의존성의 예로 파드가 스케줄링 될 수 있는지 여부에 영향을 줍니다.
쿠버네티스는 hostPort를 통해서 호스트 시스템의 특정 포트로 컨테이너 포트 노출을 요청할 수 있습니다.
이때 hostPort를 사용하면 노드에 런타임 의존성이 생기게 됩니다.
hostPort는 각 클러스터 노드 당 하나의 포트를 예약하기 때문에 클러스터 노드 수 만큼만 파드를 확장할 수 있습니다.
어플리케이션에는 몇 가지 설정 정보가 필요할 수 있습니다.
쿠버네티스에서는 컨피그맵과 시크릿을 통해서 어플리케이션 설정을 제어할 수 있습니다.
컨피그맵과 시크릿으로 명명된 컨테이너는 런타임 의존성을 적용합니다.
만약 요청한 모든 컨피그맵과 시크릿이 생성되지 않으면, 컨테이너는 노드에 스케줄링 될 수 있지만 시작되지는 않습니다.
따라서 볼륨과 포트 번호 의존성은 파드가 스케줄링되는 위치를 제한하며,
컨피그맵과 시크릿 의존성은 파드가 시작하는 것을 막을 수도 있습니다.
그렇기 때문에 컨테이너화 된 어플리케이션을 설계할 때 런타임 제약사항을 잘 고려해야합니다.
쿠버네티스에서 컴퓨팅 자원은 압축 가능 자원과 압축 불가능 자원으로 분류됩니다.
압축 가능 자원은 CPU나 네트워크 대역폭처럼 제어가 가능한 자원을 의미하며,
압축 불가능 자원은 메모리처럼 제어가 불가능한 자원을 얘기합니다.
만약 컨테이너가 압축 가능 자원을 많이 소비한다면 병목 현상이 일어날 것이며,
압축 불가능 자원을 많이 소비한다면 컨테이너는 죽어버릴 수 있습니다.
따라서 어플리케이션의 특성과 세부 구현사항에 근거하여 최소 자원량(requests)과 최대 자원량(limits) 을 지정해야합니다.
다음 yaml을 보면서 분석해보도록 하겠습니다.
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: app
image: images.my-company.example/app:v4
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
requests 는 스케줄러가 파드를 노드에 배치 시킬 때 사용됩니다.
스케줄러는 파드 안의 해당 필드를 통해서 필요 자원을 파악하고 배치 가능한 노드에 스케쥴 합니다.
limits는 어플리케이션이 추가적으로 자원을 필요로할 때 최대로 증가할 수 있는 상한 값입니다.
쿠버네티스에서 request와 limits는 어떻게 기술하느냐에 따라서 여러 종류의 QoS를 제공합니다.
해당 파드는 requests와 limits를 갖고 있지 않는 파드입니다.
이 경우에는 가장 낮은 우선순위로 고려되고, 파드가 위치한 노드에 압축 불가능 자원이 전부 사용된다면 제일 먼저 해당 파드가 죽습니다.
해당 파드는 requests와 limits를 모두 가지고 있지만 값이 다른 경우입니다.
만약 노드가 압축 불가능 자원이 전부 사용된다면 Best-Effort 파드 다음으로 죽을 확률이 높습니다.
해당 파드는 requests와 limits를 모두 가지고 있고 값이 동일한 경우입니다.
가장 우선순위가 높은 파드이며, Best-Effort 파드와 Burstable 파드 다음으로 가장 나중에 죽습니다.
정리하면 다음과 같습니다.
파드 | requests | limits | 동일 여부 | 제거 우선 순위 |
---|---|---|---|---|
Best-Effort 파드 | X | X | - | 1 순위 |
Burstable 파드 | O | O | X | 2 순위 |
Guaranteed 파드 | O | O | O | 3 순위 |
쿠버네티스에서 requests와 limits를 통해서 어떤 컨테이너를 우선순위로 죽이는지 알아보았습니다.
이와 관련된 기능으로 파드의 우선순위(priority)와 선점(preemption) 이 있습니다.
파드 우선순위를 사용하면 파드들은 상대적인 우선순위를 가지게 되고,
파드를 스케줄링할 수 없는 경우 스케줄러가 우선순위가 낮은 파드를 선점하여 보류 중인 파드를 스케줄링할 수 있도록 합니다.
우선순위와 선점을 사용하기 위해서는
다음 yaml을 보면서 자세하게 얘기해보겠습니다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "이 프라이어리티 클래스는 XYZ 서비스 파드에만 사용해야 한다."
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
먼저 PriorityClass를 생성하였습니다.
해당 PriorityClass는 high-priority라는 이름과 1,000,000 이라는 우선순위 값을 가집니다.
이 우선순위 값이 상대적으로 높을수록 더 중요한 파드임을 나타낼 수 있습니다.
이렇게 파드 우선순위 기능이 활성화 되면 스케줄러는 파드를 노드에 배치하는 순서에 영향을 미칩니다.
priority admission controller는 priorityClassName을 보고 새로운 파드의 우선순위 값을 채웁니다.
여러 파드가 배치를 기다리는 경우 스케줄러는 가장 높은 우선순위의 파드를 맨 처음에 오도록 정렬합니다.
만약 파드를 배치할수 있는 충분한 용량을 가진 노드가 없다면,
큐블릿을 통해 제거 우선 순위가 가장 높은 파드를 제거하여 용량을 확보하며
그래도 용량이 부족하다면 우선순위가 낮은 파드를 제거합니다.
쿠버네티스에서는 모든 사용자를 신뢰할 수 없는 클러스터에서,
파드 우선순위를 사용할 때는 신중하라고 얘기합니다.
만약 악의적인 사용자가 우선순위가 가장 높은 파드를 생성한다면
다른 파드가 제거되거나 스케줄링이 되지 않을 수 있기 때문입니다.
따라서 쿠버네티스에서는 ResourceQuota가 PriorityClass를 지원하도록 확장되었으며,
시스템 파드에는 더 큰 우선순위 번호가 예약되어있습니다.
쿠버네티스에서는 일부 사용자가 플랫폼의 모든 자원을 소비하지 못하게 하는 제어 장치를 마련해놓았습니다.
ResourceQuota는 네임스페이스에서 사용할 수 있는 리소스의 총량을 제한하도록 하는 장치입니다.
네임스페이스에서 만들 수 있는 오브젝트 수와 사용할 수 있는 리소스의 총 양을 제한할 수 있습니다.
Limit Range는 네임스페이스에서 자원에 따라 리소스 할당(파드 또는 컨테이너)을 제한하는 정책입니다.
즉, 각 자원에 따라 최대 자원량을 설정한다는 것이 ResourceQuota와의 차이점 입니다.
또한 오버커밋 레벨로 알려진 requests와 limits 사이의 비율을 제어할 수 있습니다.
아래 표를 보면 requests와 limits에 대한 선택 가능한 값이 정해져있습니다.
종류 | 자원 | 최소 | 최대 | 기본 제한 | 기본 요청 | 제한 / 요청 비율 |
---|---|---|---|---|---|---|
컨테이너 | CPU | 500m | 2 | 500m | 250m | 4 |
컨테이너 | 메모리 | 250Mi | 2Gi | 500Mi | 250Mi | 4 |
requests와 limits의 차이가 크면 노드에 오버커밋을 할 가능성이 커지는데,
Limit Range를 통해서 어떤 컨테이너도 클러스터 노드가 제공하는 것보다 큰 자원을 요청할 수 없기 때문에
requests와 limits의 차이를 줄여 오버커밋을 줄일 수 있습니다.
쿠버네티스를 통해서 다목적 어플리케이션을 올리게 된다면, 용량을 계획하는 것은 간단하지 않습니다.
이런 경우에는 환경에 따라서 설계를 해야할 필요가 있습니다.
만약 운영 클러스터가 아닌 환경에서 하드웨어를 최적으로 활용하려면 Best-Effort와 Burstable 파드를 주로 사용하면 됩니다.
하지만 운영 클러스터라면 주로 Guaranteed 파드를 사용하고 약간의 Burstable 파드를 사용하면 됩니다.
그래도 가장 좋은 것은 각 어플리케이션에 대한 용량을 측정하는 것 입니다.
파드 | CPU 요청 | CPU 제한 | 메모리 요청 | 메모리 제한 | 인스턴스 |
---|---|---|---|---|---|
A | 500m | 500m | 500Mi | 500Mi | 4 |
B | 250m | 500m | 250Mi | 1000Mi | 2 |
C | 500m | 1000m | 1000Mi | 2000Mi | 2 |
D | 500m | 500m | 500Mi | 500Mi | 1 |
총합 | 4000m | 5500m | 5000Mi | 8500Mi | 9 |
위의 표는 몇 가지 서비스의 CPU와 메모리 요구사항을 보여줍니다.
이런 식으로 각 환경별 모든 서비스에 대한 총 필요 자원량을 계산해놓는다면 효율적인 파드 관리를 할 수 있을 것입니다.
이번 글을 통해서 쿠버네티스의 런타임 의존성과 자원 프로파일을 식별하는 것에 대해서 파악하였습니다.
이번 장은 디자인 패턴이라고 하기보단 쿠버네티스에 다목적 파드를 잘 관리하는 법인 것 같습니다.
먼저 런타임 제약사항과 파드가 사용할 리소스를 통해서 효율적인 배치를 할 수 도있습니다.
또한 노드의 압축 불가능 자원을 많이 소비하였을 때 우선 순위가 높은 파드를 남겨둘 수 있습니다.
이를 통해서 우리는 어플리케이션의 성능을 향상시킬 수 있으며, Down time을 줄일 수 있을 것입니다.
내용이 참 좋네요 ㅎㅎㅎㅎ