본문으로 건너뛰기

8.3 nodeSelector와 affinity

8.1의 filtering 단계는 Pod가 선언한 배치 조건에 맞지 않는 노드를 탈락시킨다. 이 장은 그 조건을 선언하는 문법을 다룬다. 도구는 세 층으로 나뉜다. nodeSelector는 노드 label과의 단순 일치이고, nodeAffinity는 연산자와 필수/선호 구분을 갖춘 확장형이며, podAffinity와 podAntiAffinity는 노드의 속성이 아니라 이미 배치된 다른 Pod와의 관계로 조건을 건다. 마지막에 다루는 topologySpreadConstraints는 replica를 장애 도메인에 고르게 펴는 전용 문법이다.

nodeSelector, label 일치로 거르기

가장 단순한 형태는 노드에 label을 달고 Pod가 그 label을 지목하는 것이다.

kubectl label nodes node-1 disktype=ssd
apiVersion: v1
kind: Pod
metadata:
  name: fast-storage-app
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: app
    image: nginx:1.27

nodeSelector에 나열한 key/value 전부와 일치하는 label을 가진 노드만 filtering을 통과한다. 문법이 단순한 만큼 표현력도 딱 여기까지다. “값이 a 또는 b인 노드”, “이 key가 없는 노드”, “가능하면 이런 노드” 같은 요구는 표현할 방법이 없고, 조건에 맞는 노드가 하나도 없으면 Pod는 Pending으로 남는다.

nodeAffinity, required와 preferred

nodeAffinity는 nodeSelector의 확장이다. 필드 이름 둘이 유난히 긴데, 두 토막으로 끊어 읽으면 뜻이 그대로 드러난다. requiredDuringSchedulingIgnoredDuringExecution은 스케줄링 시점의 필수 조건으로, filtering에서 미달 노드를 탈락시킨다. preferredDuringSchedulingIgnoredDuringExecution은 선호 조건으로, scoring에서 weight(1~100)만큼 가점을 주되 만족하는 노드가 없어도 배치 자체는 진행된다.

공통 접미사 IgnoredDuringExecution은 실행 중에는 무시한다는 뜻이다. 배치가 끝난 뒤 노드 label이 바뀌어 조건이 깨져도 이미 실행 중인 Pod를 쫓아내지 않는다. 평가는 스케줄링 시점 한 번뿐이다.

apiVersion: v1
kind: Pod
metadata:
  name: zone-pinned-app
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values: ["zone-a", "zone-b"]
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: disktype
            operator: In
            values: ["ssd"]
  containers:
  - name: app
    image: nginx:1.27

matchExpressions가 지원하는 연산자는 In, NotIn, Exists, DoesNotExist, Gt, Lt 여섯 가지다. NotInDoesNotExist를 쓰면 특정 노드를 피하는 조건이 되므로, 노드 회피 요구도 이 문법 안에서 해결된다. 조합의 논리도 정해져 있다. nodeSelectorTerms에 항목을 여러 개 두면 그중 하나만 맞아도 통과하고(OR), 한 항목 안의 matchExpressions는 전부 맞아야 통과한다(AND).

podAffinity와 podAntiAffinity, 기준이 Pod로 바뀐다

nodeSelector와 nodeAffinity의 기준은 노드에 붙은 label이다. podAffinity와 podAntiAffinity는 기준을 이미 실행 중인 Pod의 분포로 바꾼다. labelSelector로 기준이 되는 Pod 집합을 정하고, topologyKey로 “같은 곳"의 단위를 정한다. topologyKey가 kubernetes.io/hostname이면 같은 노드가 한 단위이고, topology.kubernetes.io/zone이면 같은 zone 전체가 한 단위다.

podAffinity는 기준 Pod가 실행 중인 단위 안으로 배치를 유도한다. 자주 통신하는 캐시 Pod와 웹 Pod를 같은 노드나 같은 zone에 두어 왕복 지연을 줄이는 용도가 전형적이다. podAntiAffinity는 반대로 기준 Pod가 있는 단위를 피한다. labelSelector를 자기 자신의 label로 지정하면 같은 replica끼리 서로를 피하는 분산 선언이 된다.

podAffinity와 podAntiAffinity는 노드마다 클러스터 전체의 Pod 분포를 대조해야 해서 연산 비용이 크다. 공식 문서는 이 기능이 스케줄링을 상당히 느리게 만들 여지가 있어 수백 노드를 넘는 클러스터에서는 사용을 권장하지 않는다고 안내한다. 노드 label로 표현되는 요구는 nodeAffinity로, 분산 요구는 topologySpreadConstraints로 대신하는 편이 비용이 낮다.

replica 분산과 topologySpreadConstraints

replica 3개가 한 노드에 몰려 있으면 그 노드 하나의 장애가 서비스 전체의 장애가 된다. 분산의 고전적 선언이 자기 자신을 향한 podAntiAffinity다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: web
            topologyKey: kubernetes.io/hostname
      containers:
      - name: app
        image: nginx:1.27

required로 건 hostname anti-affinity는 노드당 replica 1개라는 강한 제약이다. 노드 3대 클러스터에서 replicas를 4로 올리면 네 번째 Pod는 갈 곳이 없어 Pending으로 남는다. preferred로 완화하면 노드가 부족할 때 같은 노드 배치를 허용하지만, 이번에는 분산이 얼마나 지켜지는지 통제할 방법이 없다.

topologySpreadConstraints는 이 양자택일을 정도의 문제로 바꾼다. 위 Deployment의 Pod template spec에 anti-affinity 대신 다음을 선언한다.

      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: web
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: web

maxSkew는 단위 간 Pod 수 차이의 허용치다. zone이 셋이고 maxSkew가 1이면 2:1:1 같은 분포는 허용되고, 3:1:0을 만드는 배치는 거부된다. whenUnsatisfiable이 DoNotSchedule이면 filtering에서 탈락시키는 필수 조건으로, ScheduleAnyway면 scoring에서 skew를 줄이는 노드를 우대하는 선호 조건으로 동작한다. zone 분산은 필수로 강제하고 노드 분산은 선호로 두는 위 조합이 실무에서 자주 쓰는 형태다.

정리

  • nodeSelector는 label 완전 일치만 표현하고, nodeAffinity는 연산자 여섯 종과 required/preferred 구분을 더한 확장이다.
  • IgnoredDuringExecution 접미사 그대로, 조건 평가는 스케줄링 시점뿐이며 이후의 label 변경이 배치를 되돌리지는 않는다.
  • podAffinity와 podAntiAffinity는 topologyKey 단위로 다른 Pod와의 동거와 분리를 선언하지만, 대규모 클러스터에서는 스케줄링 비용을 먼저 따진다.
  • replica 분산은 required anti-affinity의 전부 아니면 Pending 양자택일보다, maxSkew로 정도를 조절하는 topologySpreadConstraints가 유연하다.