파드 Pending — volume node affinity 충돌

Kubernetes4분조회

데이터를 디스크에 들고 있어야 하는 앱이라 StatefulSet 으로 띄우고, 파드마다 EBS 볼륨을 PVC 로 하나씩 붙여 씁니다. 클러스터는 노드 12대를 세 가용영역에 나눠 두고 있었습니다.

그 StatefulSet 파드 하나가 6분째 Pending 에서 안 넘어갔습니다. 노드는 12대 전부 Ready 상태.

$ kubectl get pod app-0
NAME    READY   STATUS    RESTARTS   AGE
app-0   0/1     Pending   0          6m

스케줄러의 거절 사유 — 개수까지 기록

$ kubectl describe pod app-0
Events:
  Warning  FailedScheduling  0/12 nodes are available:
    9 node(s) had volume node affinity conflict,
    3 Insufficient memory.

12대를 전부 후보로 놓고 하나씩 떨어뜨리면서 이유별 개수를 남깁니다. 9대는 볼륨, 3대는 메모리. 원인이 두 개입니다.

taint 상태 확인 — 원인 아님

$ kubectl get nodes -o json | jq -r '.items[] | "\(.metadata.name) \(.spec.taints // [])"'

taint가 붙은 노드는 3대. 개수는 맞아떨어지지만 그 3대는 Insufficient memory로 잡힌 쪽입니다. taint가 원인이면 메시지는 node(s) had untolerated taint로 나옵니다.

EBS 볼륨은 가용영역을 못 넘음

EBS 볼륨은 만들어진 가용영역에 묶여서 다른 존의 노드에는 붙지 않습니다. 그래서 EBS CSI 드라이버는 PV를 만들 때 그 존을 nodeAffinity에 박아두고, 파드가 이 PV를 쓰면 스케줄러는 해당 존의 노드만 후보로 봅니다.

$ kubectl get pv pvc-8f3a... -o jsonpath='{.spec.nodeAffinity}' | jq
{
  "required": {
    "nodeSelectorTerms": [
      {
        "matchExpressions": [
          {
            "key": "topology.ebs.csi.aws.com/zone",
            "operator": "In",
            "values": ["ap-northeast-2a"]
          }
        ]
      }
    ]
  }
}

ap-northeast-2a 하나로 고정.

처음 스케줄된 존이 파드 수명 내내 제약

$ kubectl get nodes -L topology.kubernetes.io/zone --no-headers | awk '{print $NF}' | sort | uniq -c
   3 ap-northeast-2a
   5 ap-northeast-2b
   4 ap-northeast-2c

2a에 3대, 그 3대는 전부 메모리가 꽉 찬 상태. 나머지 9대는 볼륨이 붙을 수 없는 존이라 후보에서 빠집니다. 0/12의 내역이 이렇게 나옵니다.

한 번 바인딩된 PVC는 파드가 재생성돼도 그대로 재사용됩니다. 처음 스케줄된 존이 파드가 삭제될 때까지 따라붙습니다. 오토스케일링이나 노드 교체로 그 존의 여유가 사라지면 클러스터 전체에 자리가 남아도 이 파드만 못 뜹니다.

응급 처치 — 해당 존에 노드 추가

$ aws autoscaling set-desired-capacity \
    --auto-scaling-group-name <asg-name> \
    --desired-capacity 4

2a에 자리가 생기면 Pending 파드가 바로 스케줄됩니다.

근본 대응 — volumeBindingMode

Immediate면 PVC가 생기는 시점에 볼륨이 만들어지고 존이 확정됩니다. 파드가 어느 노드로 갈지 정해지기 전에 존이 먼저 정해지는 구조입니다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer   # Immediate 가 아니라 이것
parameters:
  type: gp3

WaitForFirstConsumer는 순서를 뒤집습니다. 스케줄러가 파드를 놓을 노드를 먼저 정하고, 그 노드가 속한 존에 볼륨을 만듭니다. 충돌 조건 자체가 없어집니다.

존별 노드 수도 같이 맞춰야 하는데, 한 존에 3대뿐이면 그 3대가 차는 순간 같은 상황이 그대로 재현되기 때문입니다. 존마다 최소 3대가 남도록 ASG min을 조정했습니다.

흔한 오답 — PVC 재생성

“PVC를 지우고 다시 만들라”는 답이 자주 보입니다. 존이 다시 잡히니 파드는 뜹니다. 다만 StorageClass의 reclaimPolicy가 기본값 Delete면 PVC를 지울 때 PV와 EBS 볼륨까지 같이 삭제되고, 안에 있던 데이터도 사라집니다.

StatefulSet의 PVC는 대부분 지우면 안 되는 대상입니다. 노드를 한 대 늘리는 쪽이 안전합니다.

남은 것

이미 Immediate로 만들어져 바인딩된 PV에는 StorageClass 변경이 소급 적용되지 않습니다. nodeAffinity는 PV 생성 시점에 확정되고 이후 수정할 수 없습니다. 옮기려면 스냅샷으로 다른 존에 새 볼륨을 만들고 PVC 를 교체해야 하는데, 그 사이 파드를 내려야 하니 다운타임이 따라옵니다.

참고

  1. 불러오는 중