본문 바로가기
DevOps/Kubernetes

Kubernetes Node ContainerGCFailed 트러블슈팅

by 비어원 2025. 9. 30.
728x90

어느날 쿠버네티스 워커노드에서 갑자기 ContainerGCFailed 이벤트가 발생하여 노드의 디스크에 문제가 발생했다.

Events:
    Type    Reason                Age                        From    Message
    ----    ----                ----                    ----    ----
    Warning    ContainerGCFailed    3m2s (x151 over 152m)    kubelet    failed to read podLogRootDirectory "/var/log/pods": open /var/log/pods: input/output error

 
해당 이벤트가 발생한 노드에서 디스크 I/O 기능에 장애가 발생하였고, 디스크 조회가 되지 않았다. 심지어 해당 노드로 SSH 접속도 되지 않아서 일단 노드 재구동으로 문제를 해결하였다.
 
하지만 노드는 Ready 상태로 머물러 있었기 때문에, 해당 노드에 파드가 여전히 스케줄링 되어있었고, 이 여파로 정적파일을 서빙하는 Nginx 애플리케이션에서 확률적으로(로드밸런싱으로 장애난 노드에 스케줄링 된 파드로 트래픽이 꽂힐 확률) 장애가 발생했다. Readiness probe로 정적파일 서빙을 검사하는 방식으로 구성했다면 이슈를 최소화 할 수 있었던 것 같다.

원인 분석

일단 ContainerGCFailed 에러가 발생한 워커노드를 재구동하여 SSH 접근이 가능하게 복구하였고, 이후에 시스템로그를 분석해보았는데 대략적으로 다음의 에러로그가 다량 발견되었다.

$ cat /var/log/messages | grep 'containerd\[' | grep 'Failed to get usage for snapshot'
Sep 1 00:00:00 NODE_NAME containerd[111]: time="2025-09-01T00:00:00.000000000+09:00" level=error msg="Failed to get usage for snapshot \"abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234\"" error="lstat /var/lib/containerd/io.containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1299/fs/app/userdata/Default/Cache/Cache_Data/abcd1234abcd1234_0: no such file or directory"
Sep 1 00:01:00 NODE_NAME containerd[111]: time="2025-09-01T00:01:00.000000000+09:00" level=error msg="Failed to get usage for snapshot \"abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234\"" error="lstat /var/lib/containerd/io.containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1299/fs/app/userdata/Default/Cache/Cache_Data/abcd1234abcd1234_0: no such file or directory"
...

 

에러로그를 분석해보면 containerd에서 /var/lib/containerd/io.containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1299/fs/app/userdata/Default/Cache/** 경로의 스냅샷 사용량을 가져오는 것을 실패하고 원인은 해당 경로에 아무것도 없다는 로그이다.

 
문제는 이 로그가 분단위로 다량 발생했었는데, ContainerGCFailed로 노드의 디스크가 고장난 근본적인 원인이지 않을까 하고 Containerd가 저 스냅샷을 조회하는 방식을 확인해봐야겠다고 생각했다.
 

Containerd와 Snapshot

쿠버네티스 클러스터에서 CRI (Container Runtime Interface)로는 Containerd를 사용하고 있었고, Containerd는 파드가 생성될 때 스케줄링된 노드에 컨테이너를 생성, 관리, 삭제할 때 사용하는 런타임 엔진이다.
 

그리고 컨테이너를 생성하면 Containerd는 해당 컨테이너의 파일시스템을 격리하기 위해 snapshotter 컴포넌트를 사용한다. 컨테이너가 생성되면 containerd는 스냅샷 단위로 /var/lib/containerd/io.containerd/io.containerd.snapshotter.v1.overlayfs 하위에 디렉터리를 생성하고 컨테이너에 여러 스냅샷을 파일시스템을 붙여서 격리시킨다.

 
쿠버네티스에서 컨테이너 임시볼륨이 해당 디렉터리 하위에서 관리되고 있고, 이 디렉터리 관리 주체는 containerd이다.

Snapshot GC

하지만 컨테이너는 쿠버네티스에서 언제든지 제거될 수 있다. 파드가 제거되거나 롤아웃되거나 파드의 liveness가 깨져서 재시작되는 경우 등 여러가지 이유로 컨테이너가 삭제되고 생성된다. 컨테이너가 삭제될 때 즉시 overlayfs가 제거되는 것이 아니라 Containerd가 스냅샷을 주기적으로 청소한다. 이를 GC라고 부른다.
 
containerd 1.6.21 버전을 기준으로 말하자면 스냅샷 GC를 하는 과정은 다음과 같다.

  1. 모든 스냅샷은 SnapshotStore에 저장한다.
  2. SnapshotStore에 저장된 모든 스냅샷을 순회하여 도달 가능한 스냅샷을 마킹한다. (sn.Timestamp 갱신)
  3. 도달 가능한 스냅샷의 용량을 계산하여 스냅샷 정보를 갱신한다.
  4. 도달 불가능한 스냅샷을 청소한다. (sn.Timestamp 갱신 안된 것들)

https://github.com/containerd/containerd/blob/v1.6.21/pkg/cri/server/snapshots.go#L71

containerd/pkg/cri/server/snapshots.go at v1.6.21 · containerd/containerd

An open and reliable container runtime. Contribute to containerd/containerd development by creating an account on GitHub.

github.com

 

자주 삭제되는 디렉토리 (Feat. Puppeteer)

Failed to get usage for snapshot 으로 잡히는 경로들을 확인해봤는데, 해당 경로들은 모두 디스크 캐시 부분이었다. 해당 디스크 캐시를 사용하는 파드를 확인해본 결과 Puppeteer였고, Puppeteer를 사용하여 브라우저를 렌더링하는 애플리케이션이 있는데 디스크 캐시를 사용하고 있었고, /app/userdata/Default/Cache 하위에 렌더링 결과 디스크 캐시가 다량 있었으며, 요청이 많을수록 캐시가 자주 교체되고 있었다.

 

파일이 자주 삭제되고 생성되기 때문에 그 순간에 스냅샷 용량을 확인할 때마다 Failed to get usage for snapshot 에러로그가 쌓이고 있었다. 뿐만 아니라, 캐시 디렉토리 하위에 파일 갯수도 많기 때문에 containerd GC를 할 떄마다 파일 조회가 빈번하게 발생할 것이고, 이 영향으로 인해 ContainerGCFailed로 노드의 디스크가 고장난 것으로 추측된다.

 

해결 방법

Puppeteer의 디스크 캐시와 같이 파일 갯수가 많은 디렉터리라면 Containerd GC에 상당히 부담을 줄 수 있을 것으로 보인다.
 
Containerd GC 대상에서 제외를 시키면 문제가 해결 될 것이며, 이에 대한 해결 방안은 emptyDir를 사용하는 방식이 될 수 있다.

디스크 캐시와 같이 파일 갯수가 많은 디렉터리를 emptyDir로 볼륨마운팅을 하면 해당 볼륨은 컨테이너 임시 볼륨이 아닌 파드 수준의 임시 볼륨이 되며, 관리주체 또한 CRI(컨테이너 관리)가 아닌 kubelet(파드 관리)이 된다. 그리고 emptyDir로 볼륨마운팅을 하면 실제 노드에서 해당 볼륨이 있는 실제 위치도 /var/lib/kubelet/pods/<POD_UUID>/volumes/kubernetes.io~empty-dir/ 하위로 된다.

 
물론 해당 볼륨도 kubelet이 GC를 하긴 하지만, 파드가 삭제될 때 emptyDir이 GC 되기 때문에 컨테이너 임시볼륨 GC와는 검사 주기도 상당히 차이가 나기 떄문에 디스크 부하도 줄어드는 효과를 낼 수 있다.
 
실제로 캐시 디렉토리를 emptyDir로 변경한 이후로는 ContainerGCFailed가 더 이상 발생하지 않는다. (물론 장기적으로 더 지켜봐야 한다.)

728x90

댓글