kubernetes에서 자동적으로 pod를 스케줄링할 수 있는 혁신적인 방법은 없을까? (feat. descheduler) (1)
TL;DR
- kubernetes 운영중 pod이 node에 적절히 분산되지 않는 문제를 경험했고 해당 문제를 해결할 수 있는 방법을 찾았다.
- 그 중 descheduler는 Pod이 배포된 이후에도 지속적으로 Node에 대해서 관리해주는 장점이 있다.
- descheduler는 Node 사용량을 기반으로 Pod 분산 외에도 다양한 정책을 지원해준다.
배경
현재 팀에서는 개발한 제품을 직접 kubernetes에 올리며 운영하고 있습니다. 그 과정에서 배포를 할때마다 특정 node에서 cpu usage가 80%가 넘는다는 alert를 지속적으로 받게되었습니다. 그래서 조사를 해보니 특정 node에서는 cpu 사용량이 30%정도 밖에 사용하지 않는 경우들을 발견했습니다.
원인
현재 저희는 node를 여유롭게 사용하고 있는 상태는 아닙니다. 만약 그랬다면 pod이 여기저기 막뜨고 죽더라도 충분히 여유롭게 스케쥴링이 되고 alert도 발생하지 않았을 것입니다.
여유롭게 사용하고 있는 상태가 아니다보니 deployment에서 진행하는 rollout 과정에서 특정 노드에 많은 일시적으로 많이 스케쥴링이 됩니다. 밑에 pod이 배포되는 상황을 보면서 간단하게 설명하겠습니다.
가정) 현재 우리가 동작시킬려고 하는 Pod이 뜰 수 있는 Node는 A, B, C밖에 없다.
한 pod당 node에서 cpu usage를 30% 사용한다고 가정 (실제로 pod request/limit이 아닙니다.)
// 1. before rollout
Node A - Pod(v1), Pod(v1) -> 60%
Node B - Pod(v1), Pod(v1) -> 60%
Node C - Pod(v1), Pod(v1) -> 60%
// 2. rollout in progress
Node A - Pod(v1), Pod(v1) -> 60%
Node B - Pod(v1), Pod(v1), Pod(v2) -> 90%
Node C - Pod(v1), Pod(v1), Pod(v2) -> 90%
// 3. rollout in progress
Node A - Pod(v1), Pod(v1) -> 60%
Node B - Pod(v1), Pod(v1), Pod(v2) -> 90%
Node C - Pod(v2) -> 30%
// 4. rollout in progress
Node A - Pod(v1), Pod(v1) -> 60%
Node B - Pod(v1), Pod(v1), Pod(v2) -> 90%
Node C - Pod(v2), Pod(v2), Pod(v2) -> 90%
// 5. rollout in progress
Node A - Pod(v1) -> 30%
Node B - Pod(v1), Pod(v2) -> 60%
Node C - Pod(v2), Pod(v2), Pod(v2) -> 90%
// 6. rollout in progress
Node A - Pod(v1), Pod(v2), Pod(v2) -> 90%
Node B - Pod(v1), Pod(v2) -> 60%
Node C - Pod(v2), Pod(v2), Pod(v2) -> 90%
// 7. rollout done.
Node A - Pod(v2), Pod(v2) -> 60%
Node B - Pod(v2) -> 30%
Node C - Pod(v2), Pod(v2), Pod(v2) -> 90%
deployment rollout을 위에 설명한 것과 다르게 실행할 수 있는지에 대한 확신없지만 제가 아는 영역에서는 새로운 v2를 동작시키고 실제로 잘 동작한다면 그 이후에 v1을 죽입니다. (현재는 pod이 2개씩 배포된다고 가정했지만 1개씩 배포한다고해도 똑같은 상황이 발생할 수 있습니다.)
여기서 중요한 것은 기존 v1이 동작하는 것이 보장되고 v2가 새롭게 배포될때 비슷한 조건이라면 특별히 어떤 pod이 배포되어있는 것을 고려하지 않습니다. (scheduler 알고리즘을 선택할 수 있지만 매우 구체적으로 하기에는 어렵다고 알고 있습니다.)
위와 같은 상황에서 제가 경험한 특정 node는 많은 리소스를 쓰고 있는데 특정 node는 팽팽 놀고 있게 되었습니다.
descheduler란?
Kubernetes SIG라는 그룹에서 만들고 있습니다. 실제 kubernetes에 영향력이 있고 실제로 믿을만한 프로젝트를 만들고 있습니다. 다들 아시는 kustomize라는 프로젝트도 만들었습니다.
descheduler는 위에서 설명한 스케줄이 된 Pod에 대해서 Operator가 정한 정책 기반으로 처리해주는 프로젝트이면서 프로덕트입니다.
추가적으로 쿠버네티스 스케줄러는 Pod를 생성할 때 최적의 노드에 배치하지만, 시간이 지남에 따라 다음과 같은 문제가 발생할 수 있습니다:
- 노드 간 리소스 불균형: 일부 노드는 과도하게 사용되고, 다른 노드는 여유 리소스가 많을 수 있습니다.
- Pod 배치 비효율성: Pod가 특정 노드에 집중적으로 배치되어 리소스가 비효율적으로 사용될 수 있습니다.
- 노드 상태 변화: 노드가 추가되거나 제거되면 기존 Pod 배치가 최적이 아닐 수 있습니다.
- Pod의 라이프사이클 변화: Pod의 우선순위, 리소스 요청량, 노드 라벨 등이 변경되면 재배치가 필요할 수 있습니다.
Descheduler는 이러한 문제를 해결하기 위해 Pod를 재배치합니다.
다른 방법은 없나요?
Pod Topology Spread Constraints
Pod Topology Spread Constraints
는 쿠버네티스의 스케줄링 기능 중 하나로, Pod가 클러스터 내 여러 토폴로지 도메인에 균등하게 분산되도록 보장합니다. 이는 특정 노드, 영역, 또는 지역에 Pod가 집중되지 않도록 하여 리소스 사용을 최적화하고, 장애 발생 시 서비스의 가용성을 높이는 데 기여합니다.
장점
- 동적 균형 유지: Pod 배치 시 자동으로 토폴로지(노드/존/리전) 간 균형 조정
- 세부 제어:
maxSkew
로 최대 허용 편차 설정 가능 (예: 노드당 Pod 차이 ≤1) - 고가용성 강화: 장애 도메인 분산으로 특정 영역 장애 시 서비스 지속성 보장
단점
- 런타임 재분산 미지원: 초기 배포 후 노드 추가/삭제 시 자동 재조정 불가
- 레이블 의존성: 모든 노드에 일관된
topologyKey
레이블 필수 (예:kubernetes.io/hostname
)
선택하지 못한 이유
가장 간단하고 쉽게 도입할 수 있는 방법이라고 생각했습니다. 만약 descheduler를 사용하지 않는다면 가장 사용했을때 근본적인 문제를 해결해준다고 생각합니다.
사실 선택보다는 오히려 두가지를 모두 활용해서 적용하는게 더 맞다고 생각하고 있습니다.
Pod Anti-Affinity
Pod Anti-Affinity
는 쿠버네티스 클러스터에서 특정 라벨을 가진 Pod가 동일한 노드에 배치되지 않도록 하는 제약 조건입니다. 이는 특정 노드에 리소스가 집중되는 것을 방지하고, 장애 발생 시 서비스의 가용성을 높이는 데 기여합니다.
장점
- 엄격한 분리 보장: 동일 레이블 Pod의 동일 노드 배치 차단 (예:
app=web
) - 유연한 레이블 기반 정책:
labelSelector
로 특정 워크로드 타겟팅 가능
단점
- 성능 저하 위험: 대규모 클러스터(100+ 노드)에서 스케줄링 지연 발생 가능
- 노드 수 제약: 레플리카 수 > 노드 수일 경우 스케줄링 실패 가능성
선택하지 못한 이유
저희는 node가 많은 편은 아니었지만 똑같은 node에 여러 replica가 올라가고 있는 상황이었기 때문에 또 다른 스케줄링 전략을 도입하기에는 무리가 있다고 생각했습니다.
어찌보면 node 수보다 replica 수가 많았기에 거의 무조건적으로 랜덤으로 스케쥴링될것이라고 생각했습니다.
maxSurge 값 조정
maxSurge
는 쿠버네티스의 Deployment
에서 롤링 업데이트를 수행할 때, 기존 Pod 수를 초과하여 동시에 생성할 수 있는 Pod의 수를 지정합니다. 이를 통해 업데이트 과정에서 서비스의 가용성을 유지하면서도 빠르게 새로운 버전을 배포할 수 있습니다.
장점
- 빠른 배포: maxSurge를 사용하면 기존 Pod 수를 초과하여 새로운 Pod를 생성할 수 있어 배포 속도가 빨라집니다.
- 서비스 가용성: 업데이트 중에도 기존 Pod가 계속 실행되므로 서비스의 가용성을 유지할 수 있습니다.
- 리소스 최적화: 여러 노드에 Pod를 분산 배치하여 리소스 사용을 최적화하고, 특정 노드에 리소스가 집중되는 것을 방지합니다.
단점
- 리소스 소비 증가: maxSurge로 인해 일시적으로 더 많은 리소스를 소비할 수 있으며, 이는 클러스터의 리소스 한계에 도달할 수 있습니다.
- 복잡한 스케줄링: 많은 수의 Pod가 동시에 생성되면 스케줄링이 복잡해질 수 있으며, 이는 스케줄링 지연을 초래할 수 있습니다.
- 노드 수 제약: Pod가 분산 배치되기 위해 충분한 노드가 필요하며, 노드 수가 부족할 경우 스케줄링 실패 가능성이 있습니다.
선택하지 못한 이유
기본적으로 node가 매우 여유로운 상태가 아니었기 때문에 maxSurge를 높이기에는 조금 무리가 있다고 판단했습니다.
종합 비교
항목 | Pod Topology Spread | Pod Anti-Affinity | Descheduler |
---|---|---|---|
적용 시점 | 배포 시 | 배포 시 | 사후 주기적 실행 |
성능 영향 | 낮음 | 대규모 시 높음 | 중간 |
실시간 재분산 | ❌ | ❌ | ❌ |
고가용성 강화 | ⭕ | ⭕ | △ |
설정 복잡도 | 중간 | 낮음 | 높음 |
자원 사용 효율화 | ⭕ | △ | ⭕ |
kubernetes scheduler와 뭐가 다른데?
구분 | Kubernetes Scheduler | Descheduler |
---|---|---|
목적 | 새로 생성된 Pod을 최적 노드에 배치 | 기존 Pod 재배치를 통한 클러스터 최적화 |
실행 주기 | Pod 생성 시 즉시 실행 | 주기적/수동 실행 (기본 1시간 간격) |
의사결정 기준 | 노드 리소스 가용성, Affinity/테인트 규칙 | 누적된 리소스 사용 패턴, 정책 위반 여부 |
위에서 설명한 Pod Topology Spread Constraints
, Pod Anti-Affinity
, maxSurge
에 대한 것은 모두 기존 Kubernetes Scheduler 에서 관리하고 처리합니다.
Kubernetes Scheduler와 descheduler의 차이점은 Pod을 생성할때만 고려하는가 혹은 Pod이 실행중인 상태에서도 고려할 수 있는가 입니다. descheduler는 어떻게 실행하느냐에 따라 (Job, CronJob, Deployment 3가지 방법이 있습니다.) 주기적이라고 할수도 있지만 무엇보다 현재 동작하고 있는 Pod들에 대해서 처리합니다.
궁극적으로 descheduler의 장점과 단점은?
장점
- 주기적인 실행으로 특별히 운영자가 신경쓰지 않아도 누적된 불균형을 해소
- 다양한 전략 및 정책 지원. Pod Anti-Affinity와 비슷한
RemoveDuplicates
부터 해서 Node 사용량에 따른 Pod 재배치와 같은 정책들을 지원합니다. - 적절한 정책 사용으로 인프라 비용을 감소
단점
- 복잡한 정책으로 인한 충돌 위험 - 여러 정책을 사용할 경우 예측 불가능한 동작을 할 수 있습니다.
- PDB(PodDisruptionBudget) 미설정시 장애 발생 가능 - 모든 Pod이 한번에 evict이 될 가능성이 존재합니다. PDB가 설정안된 Pod에 대해서는 처리하지 않는 설정이 존재합니다.
- Node가 많은 Cluster에 경우 실제 동작하는데 오래걸리면 리소스를 많이 사용할 수 있습니다.
- 제한된 실시간성 - 기존 1시간에 한번씩 동작하면 해당 시간을 줄일수는 있지만 운영 부하가 증가할 수 있습니다.
policy
Kubernetes Descheduler는 다음과 같은 정책들을 활용할 수 있습니다:
RemoveDuplicates
- 목적: 복제본을 클러스터 전체에 고르게 분산
- 동작 방식: 노드당 동일 워크로드 Pod 1개만 유지
- 주요 설정:
excludeOwnerKinds
: 제외할 OwnerRef 종류 지정 (예: “ReplicaSet”을 지정하면 Deployment에 의해 생성된 Pod 제외)
# DeschedulerPolicy 예시
apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
profiles:
- name: duplicate-policy
pluginConfig:
- name: "RemoveDuplicates"
args:
excludeOwnerKinds: ["ReplicaSet"] # Deployment Pod 제외
plugins:
balance:
enabled: ["RemoveDuplicates"]
LowNodeUtilization
- 목적: 리소스 사용률이 낮은 노드로 Pod 재분배
- 동작 방식: 설정된 임계값 미만의 노드를 찾아 다른 노드의 Pod를 해당 노드로 이동
- 주요 설정:
thresholds
: 저사용 노드 판단 기준 (CPU, 메모리, Pod 수)targetThresholds
: Pod 이동 대상 노드의 최대 사용률numberOfNodes
: 정책 활성화를 위한 최소 저사용 노드 수
pluginConfig:
- name: "LowNodeUtilization"
args:
thresholds: {"cpu": 20, "memory": 20} # 20% 미만 노드 대상
targetThresholds: {"cpu": 50, "memory": 50} # 50% 초과 노드에서 Pod 제거
numberOfNodes: 3 # 3노드 이상 저부하시 활성화
plugins:
balance:
enabled: ["LowNodeUtilization"]
HighNodeUtilization
- 목적: Pod를 더 적은 수의 노드로 집중시켜 클러스터 효율성 향상
- 동작 방식: 사용률이 낮은 노드에서 Pod를 제거하여 다른 노드로 재배치
- 주요 설정:
thresholds
: 저사용 노드 판단 기준 (CPU, 메모리, Pod 수)numberOfNodes
: 정책 활성화를 위한 최소 저사용 노드 수
RemovePodsViolatingInterPodAntiAffinity
- 목적: Pod 간 안티어피니티 규칙을 위반하는 Pod 제거
- 동작 방식: 안티어피니티 규칙을 위반하는 Pod를 찾아 제거
RemovePodsViolatingNodeAffinity
- 목적: 노드 어피니티 규칙을 위반하는 Pod 제거
- 동작 방식: 노드 어피니티 조건을 더 이상 만족하지 않는 Pod를 찾아 제거
- 주요 설정:
nodeAffinityType
: 고려할 노드 어피니티 유형 지정
RemovePodsViolatingNodeTaints
- 목적: 노드 테인트를 위반하는 Pod 제거
- 동작 방식: 노드의 테인트 변경으로 인해 테인트 규칙을 위반하게 된 Pod를 제거
- 주요 설정:
excludedTaints
: 무시할 테인트 목록includedTaints
: 고려할 테인트 목록
RemovePodsViolatingTopologySpreadConstraint
- 목적: 토폴로지 분배 제약 조건을 위반하는 Pod 제거
- 동작 방식: 토폴로지 도메인 간 Pod 분배를 최적화하기 위해 위반 Pod 제거
- 주요 설정:
constraints
: 고려할 제약 조건 유형 (DoNotSchedule, ScheduleAnyway)
pluginConfig:
- name: "RemovePodsViolatingTopologySpreadConstraint"
args:
constraints: ["DoNotSchedule"]
topologyBalanceNodeFit: true
plugins:
balance:
enabled: ["RemovePodsViolatingTopologySpreadConstraint"]
RemovePodsHavingTooManyRestarts
- 목적: 과도하게 재시작된 Pod 제거
- 동작 방식: 설정된 재시작 횟수를 초과한 Pod를 찾아 제거
- 주요 설정:
podRestartThreshold
: Pod 제거 기준 재시작 횟수includingInitContainers
: 초기화 컨테이너 재시작 포함 여부
PodLifeTime
- 목적: 지정된 수명을 초과한 Pod 제거
- 동작 방식: 설정된 시간을 초과하여 실행 중인 Pod를 찾아 제거
- 주요 설정:
maxPodLifeTimeSeconds
: Pod의 최대 허용 수명 (초)states
: 고려할 Pod 상태 목록
pluginConfig:
- name: "PodLifeTime"
args:
maxPodLifeTimeSeconds: 86400 # 24시간
states: ["Pending", "Running"]
plugins:
deschedule:
enabled: ["PodLifeTime"]
- 주의사항: Stateful 워크로드는 제외 설정 필요. -> db 혹은 데이터를 저장하는 pod에 대해서는 안전성을 위해서 제외하는 것을 권장.
RemoveFailedPods
- 목적: 실패 상태의 Pod 제거
- 동작 방식: 특정 실패 이유나 종료 코드를 가진 Pod를 찾아 제거
- 주요 설정:
reasons
: 고려할 실패 이유 목록exitCodes
: 고려할 종료 코드 목록minPodLifetimeSeconds
: Pod 제거 전 최소 실행 시간
pluginConfig:
- name: "RemoveFailedPods"
args:
exitCodes: [137, 143] # OOM/Kill 신호 포착
minPodLifetimeSeconds: 300 # 5분 이상 실행된 Pod만 대상
plugins:
deschedule:
enabled: ["RemoveFailedPods"]
마무리
지금까지 descheduler
가 왜 만들어졌고 kubernetes scheduler가 지원해주는 기능과 descheduler가 지원해주는 기능의 차이와 그럼에도 descheduler의 장점에 대해서 이야기했습니다. 그리고 추가적으로 descheduler은 어떤 정책들을 지원하는지 알아봤습니다.
다음에는 실제로 descheduler를 활용한 Usecase에 대해서 알아보고 어떻게 도입할 수 있는지 알아보겠습니다.
Leave a comment