GPU Operator 와 타임슬라이싱 · 런타임 등록이 조용히 실패하는 자리 · 이론
containerd 설정은 어떻게 합쳐지는가
한 줄 요약
설정 파일에 썼다는 것과 그 설정이 로드됐다는 것은 다른 사건이다. 이 둘 사이에는 병합 규칙, 재시작 여부, 파드 종료 시 정리 로직, 그리고 이미 돌고 있는 컨테이너라는 네 개의 틈이 있고, GPU 사고는 거의 전부 그 틈에서 난다.
왜 이게 필요했나
실제로 있었던 일이다. 타임슬라이싱을 켜려고 ClusterPolicy 를 고쳤고, 그 여파로 toolkit 데몬셋이 다시 돌았다. 그리고 그날부터 클러스터의 GPU 네 장을 아무도 못 쓰게 됐다.
원인을 찾는 데 몇 시간이 걸렸는데, 그 시간의 대부분은 잘못된 곳을 정확히 확인하느라 썼다. cat /etc/containerd/conf.d/99-nvidia.toml 을 하면 nvidia 런타임이 멀쩡히 적혀 있다. 파일은 있다. 그러니 "설정은 됐다" 고 판단하고 다른 데를 뒤진다. 정작 containerd config dump 를 처음 쳤을 때 나온 답은 한 줄이었다 — 로드된 런타임 핸들러는 runc 하나뿐이었다.
어떻게 동작하나
틈 1 — 드롭인이 통째로 무시될 수 있다. containerd 의 주 설정은 imports 로 다른 파일을 끌어올 수 있다.
version = 2imports = ["/etc/containerd/conf.d/*.toml"]여기서 사람들이 거의 모두 틀리는 지점이 있다. 이 병합은 필드 단위가 아니다. 드롭인이 어떤 플러그인의 설정을 건드리면, 그 플러그인의 설정 전체가 그 파일의 내용으로 바뀐다. 드롭인이 적지 않은 항목은 앞 파일의 값으로 돌아가는 것이 아니라 기본값이 된다.
드롭인은 이름순으로 읽히므로, 결국 그 플러그인을 언급한 마지막 파일이 전부 가져간다.
사고 노드에서 실제로 측정한 값이다(containerd 1.7.27).
99-nvidia.toml 만 import → runtimes.nvidia 6개zz-labhub-registry.toml 만 import → runtimes.nvidia 0개둘 다 import → runtimes.nvidia 0개 ← 여기zz-labhub-registry.toml 은 세 줄짜리였다. Harbor 가 HTTP 라 인증서 경로를 가리키려고 넣은 것이다.
[plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d"런타임 이야기는 한 글자도 없다. 그런데 CRI 플러그인을 언급했다는 이유만으로, 그리고 이름이 99- 보다 뒤에 온다는 이유만으로, CRI 설정 전체를 이 세 줄로 갈아치웠다. nvidia 런타임 셋이 그때 사라졌다.
왜 일주일 동안 안 보였나
이 고장의 진짜 무서운 점은 여기다. config dump 를 열어 보면 이렇게 나온다.
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2"sandbox_image = "registry.k8s.io/pause:3.8"runc 가 있고 sandbox_image 도 그럴듯하다. 설정이 살아 있는 것처럼 보인다. 사실은 전부 기본값이다. 우리가 쓴 값은 하나도 남아 있지 않은데, 기본값이 우리가 쓸 법한 값과 비슷해서 티가 안 난다.
그래서 이 dump 를 근거로 "드롭인 병합이 안 되는구나" 라고 두 번 잘못 진단했다. 두 번 다 그럴듯했고 두 번 다 틀렸다. 답을 준 것은 추론이 아니라 드롭인을 하나씩 빼면서 dump 를 다시 뜨는 것이었다.
읽어서 결론 내리지 않는다는 원칙은 파일에만 적용되는 게 아니다. dump 를 읽을 때도, 지금 보는 값이 내가 쓴 값인지 기본값인지를 구분해야 한다. 그 둘을 구분하는 유일한 방법은 하나씩 빼 보고 결과가 달라지는지 보는 것이다.
틈 2 — SIGHUP 으로는 런타임 핸들러가 등록되지 않는다. toolkit 컨테이너는 설정을 쓴 다음 containerd 에 SIGHUP 을 보낸다. 그런데 containerd 1.7 의 리로드는 플러그인을 다시 초기화하지 않는다. CRI 플러그인은 런타임 핸들러 목록을 초기화 시점에 읽으므로, 새로 추가된 핸들러는 프로세스를 재시작하기 전에는 목록에 들어오지 않는다. 그래서 "toolkit 파드가 Ready 이고 설정 파일도 갱신됐는데 런타임은 없는" 상태가 아주 정상적으로 만들어진다.
틈 3 — toolkit 파드를 지우면 설정이 되돌아간다. toolkit 컨테이너에는 종료 시 정리 로직이 있다. 자기가 넣은 nvidia 설정을 빼고 원래대로 돌려놓는다. 설계로는 옳다 — 오퍼레이터를 제거했는데 노드에 쓰레기 설정이 남으면 안 되니까. 문제는 사고 대응 중의 손버릇이다. "파드를 지웠다 다시 띄우자" 를 먼저 하고 그다음에 containerd 를 재시작하면, 재시작 시점에는 설정이 이미 빠진 뒤다. 순서가 뒤집히면 아무리 반복해도 낫지 않는다.
틈 4 — 이미 돌고 있는 컨테이너는 아무것도 알려 주지 않는다. 런타임 핸들러는 컨테이너를 만들 때만 조회된다. 이미 실행 중인 GPU 파드는 설정이 사라져도 멀쩡히 계속 돈다. 그래서 고장은 즉시 보이지 않고, 노드를 재부팅하거나 파드가 재생성되는 순간 — 대개 며칠 뒤, 대개 새벽에 — 한꺼번에 터진다. 이 잠복이 GPU 설정 사고를 유난히 비싸게 만든다.
현장에서 만나는 모습
첫째, 복구 순서가 정해져 있다. 되돌리는 순서는 늘 이렇다. ① containerd config dump 로 현재 로드된 핸들러 확인 → ② toolkit 데몬셋을 정상 상태로 되돌리고 파드가 설정을 다시 쓰게 한 뒤 → ③ 그다음에 containerd 를 재시작 → ④ 다시 config dump 로 확인. ②와 ③을 바꿔 넣으면 처음부터 다시다.
둘째, 노드 부팅 후 점검을 자동화해 둔다. 잠복하는 고장은 사람이 기억으로 잡을 수 없다. 노드가 뜰 때마다 containerd config dump 에서 필요한 핸들러를 확인하고 없으면 실패로 끝나는 스크립트 하나면 충분하다. 파일을 읽는 점검은 이 사고를 절대 못 잡는다는 점이 핵심이다.
셋째, 설정 소유권을 문서에 적어 둔다. 노드의 containerd 설정을 고치는 주체가 여럿이면(OS 이미지 빌드, 구성 관리 도구, GPU Operator) 언젠가 서로 덮어쓴다. 누가 어느 파일을 소유하는지 한 줄로 정해 두는 것이 이 사고를 근본적으로 줄이는 유일한 방법이다.
다음 퀴즈에서 확인할 것
네 개의 틈을 각각 어떤 증상으로 알아보는지, 그리고 복구 순서를 왜 그렇게 잡아야 하는지를 확인한다. 순서 문제는 특히 실전에서 반복해서 사람을 잡는다.