고급 스케줄링
- 16.1 Taint와 Toleration — 노드가 파드를 밀어내기
- 16.2 Node Affinity — 파드가 노드를 끌어당기기
- 16.3 Pod Affinity / Anti-Affinity — 파드끼리 모이고 흩어지기
3장에서 nodeSelector로 파드를 특정 노드에 보내는 법을 배웠지. 16장은 그걸 훨씬 더 강력하게 표현하는 방법들이야 — taint/toleration, node affinity, pod (anti-)affinity. 멀티 테넌트, GPU 노드, 가용영역 분산, 운영·개발 분리 같은 실무 시나리오의 핵심 도구야. 방향성을 먼저 정리하면, nodeSelector / node affinity는 파드 쪽에서 "어디로 가고 싶다"고 표현하는 거고, taint / toleration은 노드 쪽에서 "이 노드는 아무나 못 온다"고 막는 거고, pod affinity / anti-affinity는 다른 파드 위치 기준으로 따라가거나 피하는 거야.
먼저 Taint와 Toleration. 노드에 taint를 붙이면 그 taint를 tolerate하는 파드만 그 노드에 들어올 수 있어. 파드를 일일이 수정 안 하고 노드 한 곳에서 정책을 푸는 게 장점이지. kubeadm으로 깐 클러스터의 마스터 노드를 보면 이미 node-role.kubernetes.io/master:NoSchedule taint가 붙어 있어. 형식은 <key>=<value>:<effect>고 value는 비어도 돼. effect는 세 가지야 — NoSchedule(tolerate 안 하면 스케줄 안 됨), PreferNoSchedule(최대한 피하되 다른 데 자리 없으면 OK, 소프트), NoExecute(신규 차단 + 이미 떠 있는 파드도 evict). NoExecute가 가장 강력해. 노드를 비우고 싶을 때 써.
taint 추가는 kubectl taint node node1.k8s node-type=production:NoSchedule 같은 식으로 하고, toleration은 파드 spec의 tolerations에 key, operator, value, effect를 적어. operator는 Equal(기본)과 Exists(value 무관)가 있고, Exists는 "이 key가 있기만 하면 OK"라는 뜻이야. 실무 활용을 보면, 운영·개발 노드 분리할 땐 운영 노드에 node-type=production:NoSchedule taint를 걸고 운영 파드에만 toleration을 줘. 그런데 주의 — 이것만으로는 운영 파드가 개발 노드로도 갈 수 있어. 양쪽 노드 다 taint를 줘야 완전 격리돼. GPU 노드 보호도 GPU 노드에 gpu=true:NoSchedule을 걸고 GPU 쓰는 파드만 toleration을 다는 식. 그리고 장애 노드 자동 evict — K8s가 자동으로 추가하는 두 toleration이 있어. node.kubernetes.io/not-ready와 node.kubernetes.io/unreachable에 effect는 NoExecute, tolerationSeconds: 300이야. 노드가 NotReady·Unreachable이 되면 5분 후 파드를 evict해서 다른 노드에 재배치해. 이 시간을 줄이고 싶으면 파드 spec에 더 짧은 tolerationSeconds로 직접 적으면 돼.
Node Affinity는 nodeSelector의 강력한 후계자야. 단순 라벨 매칭을 넘어서 AND/OR/NOT, In/NotIn/Exists, 가중치 기반 선호까지 표현 가능해. nodeSelector는 결국 deprecate될 예정이고. GKE 노드는 기본적으로 topology.kubernetes.io/region(지역), topology.kubernetes.io/zone(가용영역), kubernetes.io/hostname(노드 이름) 같은 라벨이 붙어 있어. 이 세 개가 affinity의 핵심 토폴로지 키야.
Hard requirement는 affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution에 nodeSelectorTerms로 적어. 이 긴 이름을 풀어보면, **requiredDuringScheduling**은 스케줄 시 반드시 만족해야 한다는 뜻이고, **IgnoredDuringExecution**은 이미 떠 있는 파드는 라벨이 바뀌어도 안 쫓겨난다는 뜻이야. RequiredDuringExecution은 책 시점에 미구현이고 지금도 그대로야. 그래서 노드 라벨을 떼도 기존 파드는 그대로 돌아.
Soft preference는 preferredDuringSchedulingIgnoredDuringExecution에 weight와 preference를 적어. 스케줄러는 노드 점수 계산할 때 이 가중치를 더해. 예를 들어 zone1에 weight 80, dedicated에 weight 20을 두면 zone1의 dedicated 노드가 최우선, 그 다음 zone1의 shared, 그 다음 다른 zone의 dedicated 식이야. 그리고 한 가지 알아둘 게, SelectorSpreadPriority라는 기본 priority가 같이 작동해. 파드를 한 노드에 다 몰지 않고 분산시키는 거야. 그래서 zone1을 강하게 선호해도 5개 replica 중 1개는 zone2로 갈 수 있어. 레플리카가 같은 ReplicaSet/Service라면 노드 분산이 자동으로 일어난다는 점을 기억해.
Pod Affinity / Anti-Affinity는 노드 라벨이 아니라 다른 파드의 위치를 기준으로 스케줄링하는 거야. 프론트엔드와 백엔드를 같은 노드에 두고 싶다면 어느 노드인지 직접 지정하지 말고 "백엔드가 가는 곳에 따라가"라고 적는 거지. Pod Affinity(같은 노드에 모으기)는 affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution에 topologyKey와 labelSelector를 적어. app=backend 라벨 가진 파드와 같은 hostname(같은 노드)에 배치되는 식이야. 재미있는 부수 효과가 있는데, 백엔드 파드를 지운 뒤 다시 만들어도 스케줄러는 같은 노드를 선호해. 왜냐하면 프론트엔드 파드들이 그 노드에 모여 있고 affinity 규칙이 깨지면 안 되니까. 즉 affinity는 양방향으로 작동해.
topologyKey가 핵심 개념이야. "어느 단위에서 모을지"를 정하는 거거든. kubernetes.io/hostname이면 같은 노드, topology.kubernetes.io/zone이면 같은 가용영역, topology.kubernetes.io/region이면 같은 지역, 커스텀 라벨(예: rack)이면 같은 랙이야. 스케줄러 동작은 labelSelector로 매칭되는 기존 파드를 찾고 → 그 파드들이 도는 노드의 topologyKey 라벨 값을 모으고 → 같은 값을 가진 노드들 중에서 배치하는 식이야. 기본적으로 같은 namespace의 파드만 매칭하고, cross-ns가 필요하면 namespaces 필드를 명시해야 해. Soft preference는 preferredDuringSchedulingIgnoredDuringExecution + weight + podAffinityTerm이고 노드 affinity와 구조 동일해.
Pod Anti-Affinity는 podAffinity를 podAntiAffinity로 바꾸기만 하면 돼. 자기 자신의 라벨을 selector로 쓰면 같은 Deployment의 파드가 한 노드에 안 모이게 만들 수 있어. 이러면 frontend 파드는 노드당 1개야. 노드보다 replica가 많으면 나머지는 Pending이 돼. 이런 경우엔 보통 soft anti-affinity가 적절해. 약하게 분산하되 부족하면 모이는 걸 허용하는 식으로. 실무 활용을 보면, HA는 replica를 여러 zone에 분산해(topology.kubernetes.io/zone topologyKey + anti-affinity). 한 zone 통째로 죽어도 서비스 유지돼. 간섭 방지는 CPU 많이 쓰는 두 워크로드가 한 노드에 모이지 않게 하는 거고, 백엔드 따라가는 사이드카는 캐시 파드를 백엔드와 같은 노드에 배치해 latency 절감하는 거야. 한 가지 주의 — pod affinity는 스케줄링이 비싸. 노드·파드가 많은 클러스터에서 광범위한 affinity 룰은 성능을 떨어뜨려. 토폴로지 키 범위를 좁게 잡고, hard보다 soft를 선호하는 게 좋아.
정리
16장 읽고 기억할 거 세 가지:
- Taint는 노드 쪽 정책, Affinity는 파드 쪽 표현. 동일한 격리도 어느 쪽에서 거느냐가 다르다. 운영 노드 보호처럼 모든 파드를 막아야 할 땐 taint(파드를 일일이 수정 못 하니까), 특정 파드가 특정 노드를 원할 땐 node affinity. NoExecute taint만 기존 파드도 쫓아낼 수 있다는 점, 그리고 NotReady/Unreachable의 5분 evict 타임아웃을
tolerationSeconds로 줄일 수 있다는 점이 운영 핵심. requiredDuringSchedulingIgnoredDuringExecution은 스케줄 시점에만 검사한다. 노드 라벨이나 다른 파드가 나중에 바뀌어도 이미 떠 있는 파드는 안 옮긴다. 노드 affinity 룰을 바꿨다고 파드가 재배치되지 않는다는 거 — 운영 변경 시 자주 헷갈리는 지점.- Pod affinity의 본질은 topologyKey. 같은 노드 / zone / region 중 어디서 "함께"인지를 결정하는 게 topologyKey. 자기 자신을 selector로 anti-affinity 걸면 replica를 zone 단위로 분산시키는 HA 패턴이 나온다. hard requirement는 비싸고 Pending 위험이 있으니 가능하면 soft preference로 시작.