LabHub
배우기 러닝패스 코스

GPU Operator and Time-Slicing

The Runtime Gate — An Unvalidated String Against What Actually Loaded

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

런타임 관문의 양쪽을 함께 다룹니다. 클러스터 쪽에서는 RuntimeClass 가 무엇을 고르고 무엇을 검증하지 않는지 보고, 노드 쪽에서는 설정이 조용히 무시되는 두 가지 경우를 만들어 봅니다. 마지막에는 그 둘을 대조하는 점검기를 직접 만듭니다.

왜 중요한가

파드가 노드에는 붙었는데 컨테이너 생성에서 죽는다면 그것은 런타임 쪽입니다. RuntimeClasshandler 는 그냥 문자열이고 apiserver 는 그것을 검증하지 않습니다. 노드마다 containerd 설정이 다를 수 있어 검증할 주체가 애초에 없기 때문입니다. 그래서 오타는 적용도 되고 스케줄도 되고, 그 노드가 컨테이너를 만드는 순간에만 드러납니다.

노드 쪽에는 조용한 실패가 둘 더 있습니다. imports 글롭이 파일 이름과 어긋나면 containerd 는 아무 말 없이 지나가고, 설정 버전이 3 으로 올라가면 플러그인 이름 자체가 바뀌어 옛 이름으로 적은 설정이 통째로 무시됩니다. 두 경우 모두 파일에는 원하는 내용이 그대로 적혀 있습니다. 그래서 점검은 파일이 아니라 로드된 설정을 봐야 하고, 그 판정을 클러스터의 RuntimeClass 목록과 맞춰 봐야 비로소 쓸모가 있습니다. 이 실습의 마지막 산출물이 그 점검기입니다.

환경

이 파드에는 진짜 containerd 가 없습니다. 그래서 설정은 TOML 파서로 다루고, 점검기는 dump 를 인자로 받을 수 있게 만들어 채점기가 두 가지 dump 로 실제로 돌려 봅니다. 클러스터 쪽은 kwok 이 띄운 진짜 apiserver 와 스케줄러라 RuntimeClass 등록도 오버헤드 계산도 진짜입니다. 작업 디렉터리는 /root/gpurt 이고 그 아래 k8s/·etc/·bin/·fixtures/·out/ 을 씁니다.

단계

  1. gpu-rt 네임스페이스와 RuntimeClass 두 개를 만듭니다.
  2. 오타 난 handler 로도 파드가 스케줄되는 것을 보고 out/unvalidated.txt 에 적습니다.
  3. cpu 1짜리 노드 두 대를 만들고 RuntimeClass 오버헤드로 결과가 갈리는 것을 봅니다.
  4. 드롭인을 못 잡는 imports 글롭을 만들고 out/imports.txt 에 적습니다.
  5. 같은 설정을 버전 3 형식으로 etc/config-v3.toml 에 다시 씁니다.
  6. bin/rc-crosscheck.sh 로 클러스터와 dump 를 대조하는 점검기를 만듭니다.
  7. 기본 런타임을 바꾸는 드롭인을 만들고 out/blast.txt 에 폭발 반경을 적습니다.
  8. 일부만 로드된 dump 를 본떠 점검기를 물리고 out/crosscheck.txt 에 저장합니다.

참고

핸들러를 고르는 오브젝트를 만든다

gpu-rt 네임스페이스를 만들고, /root/gpurt/k8s/runtimeclasses.yaml 에 RuntimeClass 두 개를 적어 적용하세요. 이름과 handler 가 각각 nvidia, nvidia-cdi 입니다.

RuntimeClass 는 클러스터 스코프 오브젝트이고, handler 값이 노드의 containerd 설정에 있는 runtimes.<이름> 과 맞아야 합니다. kubelet 은 파드를 만들 때 이 문자열을 CRI 요청에 실어 보내고, containerd 는 자기 설정에서 같은 이름을 찾습니다. apiVersionnode.k8s.io/v1 입니다.

오타 난 핸들러도 apiserver 를 통과한다

/root/gpurt/k8s/runtimeclass-typo.yaml 에 handler 를 일부러 틀리게 적은 RuntimeClass nvidia-typo 와, 그것을 쓰는 파드 probe-typo 를 함께 적어 gpu-rt 에 적용하세요. 확인한 사실을 /root/gpurt/out/unvalidated.txt 에 세 줄로 적습니다. API_VALIDATES_HANDLER, FAILS_AT, VISIBLE_TO 입니다.

apiserver 는 handler 문자열을 검증하지 않습니다. 노드마다 containerd 설정이 다를 수 있어서 검증할 주체가 애초에 없기 때문입니다. 그래서 오타는 적용도 되고 스케줄도 됩니다. 어긋남이 드러나는 자리는 그 노드가 컨테이너를 만드는 순간이고, 그것을 아는 것은 노드뿐입니다. 세 줄의 값은 no, 그리고 어느 단계에서 실패하는지, 누가 그것을 아는지를 한 단어로 적으세요.

RuntimeClass 의 오버헤드가 스케줄링을 바꾼다

/root/gpurt/k8s/node-tight.yaml 로 cpu 1짜리 kwok 노드 두 대(gpu-tight-a, gpu-tight-b)를 만드세요. 그다음 /root/gpurt/k8s/overhead.yamloverhead.podFixed 가 cpu 250m · memory 128Mi 인 RuntimeClass nvidia-overhead 와, cpu 를 900m 씩 요청하는 파드 두 개를 적어 적용하세요. fits 는 a 노드에 RuntimeClass 없이, over 는 b 노드에 그 RuntimeClass 를 붙여서 놓습니다.

RuntimeClass 의 overhead 는 그 런타임이 파드 하나마다 추가로 먹는 자원입니다. apiserver 가 파드의 spec.overhead 에 그 값을 채워 넣고, 스케줄러는 컨테이너 요청에 그것을 더해 자리를 계산합니다. 그래서 같은 900m 요청이라도 오버헤드가 붙으면 1150m 이 되어 cpu 1짜리 노드에 들어가지 못합니다. 두 노드를 따로 두는 이유는 한 노드에 두면 먼저 들어간 파드가 자리를 먹어 비교가 흐려지기 때문입니다. kwok 노드는 kwok.x-k8s.io/node: fake 어노테이션이 있어야 관리됩니다.

imports 글롭이 어긋나면 조용히 지나간다

/root/gpurt/etc/config.toml 에 사고 노드의 주 설정을 만드세요. version = 2, CRI 플러그인 아래 default_runtime_name = "runc"runtimes.runc, 그리고 드롭인을 잡지 못하는 imports 글롭을 둡니다. 드롭인은 /root/gpurt/etc/conf.d/99-nvidia.tomlruntimes.nvidia 로 만드세요. 확인 결과를 /root/gpurt/out/imports.txt 에 네 줄로 적습니다.

글롭이 아무것도 매칭하지 않는 것은 오류가 아니라 정상적인 결과라 containerd 는 로그도 남기지 않습니다. 그래서 확장자를 .conf 로 적어 두고 파일은 .toml 로 떨어뜨리면 그 설정은 영영 읽히지 않습니다. imports.txt 네 줄은 GLOB, MATCHED, DROPIN_EXISTS, LOADED_NVIDIA 이고, GLOB 은 설정에 적은 글롭을 그대로, MATCHED 는 그 글롭이 실제로 잡는 파일 수를 적습니다. 확인은 ls <글롭> 로 합니다.

containerd 2.x 의 설정 버전 3 으로 다시 쓴다

/root/gpurt/etc/config-v3.toml 에 같은 내용을 버전 3 형식으로 쓰세요. version = 3 이고 플러그인 이름은 io.containerd.cri.v1.runtime 입니다. default_runtime_namerunc, runtimes 에는 runcnvidia 가 모두 있어야 하며 nvidia 에는 options.BinaryName 을 둡니다. imports 글롭은 이번에는 드롭인을 실제로 잡아야 합니다.

containerd 2.x 는 설정 버전 3 을 쓰고, 그때 플러그인 이름 자체가 바뀝니다. 버전만 올리고 옛 이름(io.containerd.grpc.v1.cri)을 그대로 두면 그 설정은 통째로 무시되는데 오류는 나지 않습니다. 앞 단계와 같은 종류의 조용한 실패입니다. 다 쓴 뒤에는 눈으로 보지 말고 파서로 한 번 읽어 확인하세요. python3 -c "import tomllib,sys;print(tomllib.load(open(sys.argv[1],'rb')))" <파일> 이면 됩니다.

클러스터가 요구하는 핸들러와 로드된 것을 대조한다

/root/gpurt/bin/rc-crosscheck.sh 를 만드세요. 인자로 dump 파일을 받으면 그것을, 없으면 containerd config dump 결과를 읽습니다. 클러스터의 RuntimeClass 가 요구하는 handler 중 dump 에 없는 것마다 MISSING=<핸들러> 를 한 줄씩 출력하고 종료 코드 1로 끝냅니다. 하나도 없으면 OK 를 찍고 0으로 끝냅니다.

핵심은 판정 근거가 파일이 아니라 로드된 설정이라는 것입니다. /etc/containerd/config.toml 을 읽는 점검은 이 사고를 원리적으로 못 잡습니다. 파일에는 원하는 내용이 그대로 적혀 있고 로드된 결과만 다르기 때문입니다. dump 의 런타임 이름은 버전에 따라 io.containerd.grpc.v1.cri 아래에도 io.containerd.cri.v1.runtime 아래에도 있을 수 있으니 둘 다 봐야 합니다. 클러스터 쪽은 kubectl get runtimeclass -o jsonpath='{range .items[*]}{.handler}{"\n"}{end}' 로 뽑고, TOML 파싱은 python3tomllib 로 합니다. 채점기가 이 스크립트를 두 가지 dump 로 실제로 돌려 봅니다.

기본 런타임을 바꾸면 폭발 반경이 달라진다

/root/gpurt/etc/conf.d/50-default.toml 에 CRI 플러그인의 default_runtime_namenvidia 로 두는 드롭인을 만드세요. 그 결과를 /root/gpurt/out/blast.txt 에 네 줄로 적습니다. DEFAULT_RUNTIME, AFFECTED, NEEDS_RUNTIMECLASS, BLAST_RADIUS 입니다.

toolkit 이 기본 런타임을 통째로 nvidia 로 바꿔 두는 배치가 흔합니다. 파드마다 runtimeClassName 을 안 써도 되니 편하지만, GPU 를 안 쓰는 파드까지 그 런타임을 타게 됩니다. 그 설정이 깨지면 GPU 파드가 아니라 그 노드의 모든 파드가 못 뜹니다. 네 줄의 값은 각각 런타임 이름, 영향을 받는 파드의 범위, 파드마다 RuntimeClass 가 필요한지, 그리고 사고가 미치는 범위를 한 단어로 적으세요.

만든 점검기를 실제 dump 에 물려 본다

/root/gpurt/fixtures/dump-node1.toml일부 핸들러만 로드된 노드의 dump 를 본떠 적으세요. 그다음 6단계의 스크립트를 그 파일에 물려 출력을 /root/gpurt/out/crosscheck.txt 에 저장하세요.

이 fixture 는 "파일에는 다 적혀 있는데 로드된 것은 일부뿐" 인 노드를 흉내 내는 것입니다. runc 는 반드시 들어 있어야 하고, 클러스터의 RuntimeClass 가 요구하는 핸들러 중 일부는 빠져 있어야 합니다. 전부 들어 있으면 어긋남이 없어 이 단계의 뜻이 살지 않습니다. 채점기는 이 fixture 와 클러스터를 스스로 대조해 어느 핸들러가 빠졌는지 계산한 뒤 여러분의 출력과 맞춰 봅니다.