fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • langgraph 페이지로 이동
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • Docling — IBM Research 의 문서 파싱 toolkit 상세 정리
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • ai 페이지로 이동
    • agent 페이지로 이동
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • apartment 페이지로 이동
    • 구리 럭키아파트 24평 인테리어 레퍼런스 모음
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] CJ푸드빌 디지털 채널 면접: 슬롯 도메인 경험을 커머스 도메인 설계 능력으로 번역하기
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 도메인 모델링: 주문·재고·노출의 세 축을 분리해서 설계하기
    • 커머스 주문 상태와 데이터 정합성 기본기 — CJ푸드빌 면접 대비
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리 — CJ푸드빌 디지털 채널 백엔드 면접 대비
    • [초안] F&B 주문/매장/픽업 상태머신 설계 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • [초안] DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
    • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
  • finance 페이지로 이동
    • industry-cycle 페이지로 이동
    • investing 페이지로 이동
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • [초안] 커머스/F&B 면접 답변집 — 슬롯 도메인 경험을 주문·결제·쿠폰·매장 설계로 매핑하기
    • [초안] F&B / e-Commerce 운영 모니터링과 장애 대응 인터뷰 정리
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS 가 안 줄어드는 이유 — gc.collect 의 한계와 malloc_trim
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
    • [초안] Spring Security 6.x OAuth2 + JWT 상용 인증 설계 — Grant 선택, Resource Server, Refresh Rotation, 로그아웃
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/devops/Kubernetes GPU 노드에서 /run…
devops

Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때

NHN Cloud OCR 리얼 배포 중 ArgoCD sync가 Degraded로 떨어졌다. pod sandbox 생성 단계에서 no space left on device 에러가 반복 발생했고, 원인은 GPU 노드의 /run tmpfs 포화였다. 루트 디스크는 16%밖에 안 쓰고 있는데 pod가 안 뜨는 상황이라 처음엔 혼란스러웠다. 이 글에서는 /run t...

2026.05.27·9 min read·0 views

NHN Cloud OCR 리얼 배포 중 ArgoCD sync가 Degraded로 떨어졌다. pod sandbox 생성 단계에서 no space left on device 에러가 반복 발생했고, 원인은 GPU 노드의 /run tmpfs 포화였다.

루트 디스크는 16%밖에 안 쓰고 있는데 pod가 안 뜨는 상황이라 처음엔 혼란스러웠다. 이 글에서는 /run tmpfs가 뭔지, containerd가 왜 거기에 데이터를 저장하는지, GPU 노드에서 왜 특히 문제가 되는지를 정리한다.

증상

ArgoCD에서 ocr-api와 ocr-console이 Degraded 상태로 전환됐다. kubectl로 이벤트를 조회하니 pod sandbox 생성 단계에서 에러가 반복되고 있었다.

pod sandbox란 pod 안의 모든 컨테이너가 공유하는 격리 환경(network namespace, IPC namespace 등)을 만드는 첫 단계다. Kubernetes는 pod를 생성할 때 먼저 pause 컨테이너(인프라 컨테이너)를 실행해서 namespace를 확보하고, 그 위에 실제 앱 컨테이너를 올린다. sandbox 생성이 실패하면 그 pod 안의 어떤 컨테이너도 시작할 수 없어서 ContainerCreating 상태로 멈추게 된다.

이 sandbox 생성 과정에서 containerd가 /run/containerd/.../config.json을 쓰려고 했는데, /run tmpfs가 꽉 차서 파일을 못 쓴 것이 에러의 직접적인 원인이었다.

plaintext
Failed to create pod sandbox: rpc error: code = Unknown desc =
failed to create containerd task: failed to start shim:
write /run/containerd/io.containerd.runtime.v2.task/k8s.io/.../config.json:
no space left on device: unknown

동시에 nvidia-container-runtime 관련 에러도 섞여 있었다.

plaintext
OCI runtime exec failed: /usr/bin/nvidia-container-runtime
did not terminate successfully: exit status 255: unknown

어떤 노드에서 발생했나

클러스터에는 6대의 노드가 있었다.

노드 그룹노드 수용도
application2대OCR API, Console, Admin, Metering
default-worker2대시스템 워크로드
gpu-llm2대LLM 모델 서버 (GPU)

kubectl describe pod로 확인하니 문제가 발생한 pod는 전부 gpu-llm 노드에 스케줄된 것이었다. application 노드에 스케줄된 pod는 정상이었다.

디스크는 충분한데 왜?

GPU 노드에 ssh로 접속해서 df -h를 확인했다.

bash
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           8.9G  8.9G     0 100% /run        # ← 여기가 100%
/dev/vda1       485G   78G  408G  16% /            # ← 루트는 여유
tmpfs            45G  1.2M   45G   1% /dev/shm

루트 디스크는 485G 중 78G만 사용(16%)하고 있었다. 문제는 /run이라는 tmpfs가 8.9G 중 8.9G를 전부 쓰고 있었다는 것이다.

tmpfs와 /run이란

여기서 tmpfs와 /run에 대해 짚고 넘어가자.

tmpfs — RAM 위의 파일시스템

tmpfs(temporary filesystem)는 디스크가 아니라 RAM 위에 존재하는 파일시스템이다. 일반 파일시스템처럼 파일을 읽고 쓸 수 있지만, 모든 데이터가 메모리에 저장되기 때문에 디스크 I/O가 없어 매우 빠르다. 대신 재부팅하면 전부 사라진다.

Linux 커널 문서에 따르면 tmpfs의 기본 크기는 **물리 RAM의 50%**다. 다만 /run은 systemd가 부팅 시 별도로 마운트하며, 배포판마다 기본 크기가 다르다. 이 GPU 노드에서는 약 90G RAM의 10%인 8.9G로 설정되어 있었다.

/run — 런타임 임시 데이터의 표준 경로

/run은 Linux FHS(Filesystem Hierarchy Standard)에서 정의한 런타임 데이터 전용 디렉터리다. 부팅 후 프로세스들이 실행 중에 필요한 임시 파일을 저장하는 곳이다.

저장되는 것예시설명
PID 파일/run/sshd.pid프로세스 ID 기록
Unix 소켓/run/containerd/containerd.sock프로세스 간 통신(IPC)
Lock 파일/run/lock/*동시 접근 방지
FIFO/파이프containerd shim의 log데이터 스트리밍
마운트 포인트컨테이너 rootfsoverlay 마운트 포인트

이전에는 /var/run이 이 역할을 했지만, 현대 Linux에서는 /var/run이 /run의 심볼릭 링크다.

핵심은 /run이 tmpfs라는 점이다. 디스크가 아니라 RAM에서 할당된 제한된 공간이므로, 여기에 저장되는 데이터가 많아지면 루트 디스크와 무관하게 공간 부족이 발생한다.

containerd 아키텍처 — 컨테이너가 실행되기까지

containerd가 /run을 왜 쓰는지 이해하려면 컨테이너가 실행되는 전체 흐름을 알아야 한다.

Kubelet에서 컨테이너까지의 호출 체인

Kubernetes에서 컨테이너가 실행되는 과정은 여러 컴포넌트를 거친다.

plaintext
Kubelet
  ↓  CRI (Container Runtime Interface)
containerd
  ↓  fork/exec
containerd-shim (shim v2)
  ↓  fork/exec
runc (OCI Runtime)
  ↓  clone/exec
컨테이너 프로세스

각 컴포넌트의 역할:

  • Kubelet: Kubernetes 노드 에이전트. pod spec을 받아 컨테이너 생성을 요청한다
  • containerd: 고수준 컨테이너 런타임. 이미지 pull, 스냅샷 관리, 컨테이너 라이프사이클 조율을 담당한다. 직접 컨테이너를 실행하지는 않는다
  • containerd-shim: containerd와 실제 런타임 사이의 중간 프로세스. containerd가 재시작되어도 컨테이너가 죽지 않도록 분리하는 역할이다
  • runc: OCI(Open Container Initiative) 표준을 구현한 저수준 런타임. 실제로 Linux namespace, cgroup을 설정하고 컨테이너 프로세스를 실행한다

GPU 노드에서는 runc 대신 nvidia-container-runtime이 사용된다. 이 런타임이 GPU 디바이스를 컨테이너에 매핑하는 역할을 한다.

containerd가 /run에 저장하는 것

containerd는 컨테이너를 실행할 때 /run/containerd/io.containerd.runtime.v2.task/k8s.io/<컨테이너ID>/ 디렉터리를 만든다. 여기에 저장되는 파일:

파일용도
config.jsonOCI 컨테이너 스펙 (namespace, mount, env 등)
logshim 프로세스의 로그 FIFO 파이프
addressshim의 ttrpc 소켓 주소
rootfs/컨테이너 파일시스템의 overlay 마운트 포인트

이 데이터가 /run(tmpfs)에 저장되는 이유:

  • 속도: 컨테이너 생성/삭제가 빈번하므로 RAM 기반이 유리하다
  • 임시성: 컨테이너가 죽으면 의미 없는 데이터라 디스크 영속이 불필요하다
  • Linux 표준: /run은 런타임 상태 데이터의 표준 경로다

한편 영구 데이터(이미지 레이어, 스냅샷)는 /var/lib/containerd/에 저장된다. 이쪽은 루트 디스크를 사용하므로 tmpfs 제한과 무관하다.

shim v2의 1:1 프로세스 모델

containerd shim v2는 컨테이너당 하나의 shim 프로세스를 fork한다. 각 shim은 자신만의 task 디렉터리를 /run/containerd/io.containerd.runtime.v2.task/k8s.io/ 아래에 갖는다.

이 설계 덕분에 containerd 데몬이 재시작되어도 shim이 살아있어 컨테이너가 죽지 않는다. 하지만 동시에, 컨테이너 수가 많아지면 /run에 저장되는 task 디렉터리와 관련 커널 메타데이터도 비례해서 늘어난다.

Overlay 파일시스템과 마운트 포인트

컨테이너의 rootfs는 overlay 파일시스템으로 구성된다. 이 부분이 /run tmpfs 사용량에 영향을 주는 핵심이다.

Overlay 마운트 구조

overlay 파일시스템은 여러 디렉터리를 겹쳐서 하나의 통합된 뷰를 만든다.

plaintext
[컨테이너가 보는 파일시스템]
         ↑
    overlay mount
    ┌─────────────────────────────┐
    │ upperdir (쓰기 가능 레이어)   │ → /var/lib/containerd/.../snapshots/N/fs
    │ lowerdir (읽기 전용 레이어들) │ → /var/lib/containerd/.../snapshots/{1,2,3,...}/fs
    │ workdir  (overlay 내부 작업) │ → /var/lib/containerd/.../snapshots/N/work
    └─────────────────────────────┘
         ↓
    mount point: /run/containerd/io.containerd.runtime.v2.task/k8s.io/<ID>/rootfs

실제 이미지 데이터(lowerdir, upperdir)는 /var/lib/containerd/(루트 디스크)에 있다. 하지만 마운트 포인트 자체는 /run tmpfs에 위치한다.

커널 메타데이터가 tmpfs를 먹는다

overlay 마운트가 설정되면 커널은 마운트 포인트 아래의 파일/디렉터리에 대해 dentry(디렉터리 엔트리)와 inode 구조체를 생성한다. Linux 커널 문서에 따르면 overlay 파일시스템은 하위 파일시스템의 dentry 트리를 복제하여 빠른 캐시 조회를 가능하게 한다.

이 메타데이터는 마운트 포인트가 속한 파일시스템, 즉 /run tmpfs의 공간을 소비한다. find -xdev -type f로 찾으면 일반 파일이 거의 안 나오는데, df에서는 100%로 표시되는 이유가 이것이다.

GPU 노드에서 특히 문제가 되는 이유

일반 application 노드의 /run은 794M tmpfs에 1%만 사용하고 있었다. GPU 노드만 8.9G tmpfs가 꽉 찬 이유는 두 가지다.

LLM 이미지의 레이어 수

mount 명령으로 overlay 마운트 정보를 확인했다.

일반 컨테이너(calico, neuvector 등):

plaintext
overlay on .../rootfs type overlay (
  lowerdir=snapshots/35/fs,
  upperdir=snapshots/84/fs, ...
)

lowerdir가 1개다.

ocr-llm 컨테이너:

plaintext
overlay on .../rootfs type overlay (
  lowerdir=1645/fs:1644/fs:1643/fs:...:403/fs,
  upperdir=snapshots/1646/fs, ...
)

lowerdir가 약 50개였다.

LLM 모델 서버 이미지는 Ubuntu 기반에 CUDA 툴킷, cuDNN, Python, 모델 관련 의존성이 겹겹이 쌓여 레이어 수가 많다. 레이어가 많을수록 overlay 마운트 시 커널이 생성하는 dentry/inode 메타데이터도 비례해서 늘어난다.

컨테이너 수

GPU 노드에서 실행 중인 active task는 29개였다. ocr-llm 외에도 calico, csi-cinder, neuvector, argo-redis, nvidia-device-plugin, metrics-server 등 시스템 pod가 여럿 올라가 있었다. 각각이 shim task 디렉터리와 overlay 마운트 포인트를 /run에 만든다.

application 노드는 이미지 레이어가 적고(Java JAR 기반으로 3~7개), tmpfs 크기 대비 여유가 충분했다. GPU 노드는 50레이어짜리 LLM 이미지 + 29개 task 조합이 8.9G를 전부 소진한 것이다.

진단 과정

어떤 노드인지 특정

bash
kubectl describe pod ocr-api-deployment-xxx -n ocr
# Node: ocr-real-gpu-llm-f2038b41-node-0

노드 디스크 vs tmpfs 분리 확인

bash
$ df -h /run
tmpfs  8.9G  8.9G  0  100%  /run
 
$ df -h /
/dev/vda1  485G  78G  408G  16%  /

/run 안에서 뭐가 큰지 확인

bash
# 같은 파일시스템만 집계 (-x 옵션)
$ sudo du -sx /run/containerd/
9266636  /run/containerd/
 
# overlay 마운트를 따라가면 50G로 뻥튀기됨 (실제 tmpfs 사용량 아님)
$ sudo du -sh /run/containerd/
50G  /run/containerd/

du -sx의 -x 옵션이 중요하다. 이 옵션 없이 du -sh를 하면 overlay 마운트 포인트를 따라가면서 /var/lib/containerd/의 이미지 데이터까지 합산해 50G로 나온다. 실제 tmpfs 사용량은 9.27G다.

orphan task 식별

bash
# 실행 중인 task 목록
$ sudo ctr -n k8s.io tasks ls | wc -l
29
 
# task 디렉터리 수
$ sudo ls /run/containerd/io.containerd.runtime.v2.task/k8s.io/ | wc -l
39

29개는 active, 10개는 종료된 컨테이너의 shim이 정리되지 않고 남은 orphan이었다. 다만 orphan은 각 4KB로 공간 영향은 미미했다.

해결

즉시 조치 — tmpfs 크기 확장

bash
sudo mount -o remount,size=16G /run

이 한 줄로 /run tmpfs가 8.9G에서 16G로 확장되었다. 기존 데이터는 그대로 유지되면서 7.2G 여유 공간이 생겼다.

bash
$ df -h /run
tmpfs  16G  8.9G  7.2G  56%  /run

확장 후 ContainerCreating으로 멈춰있던 pod들(metrics-server, argo-redis-ha-haproxy)이 자동으로 정상 기동됐고, ocr-api pod도 GPU 노드에서 정상 생성되었다.

GPU 노드 2대 모두 동일한 증상이었고, 양쪽 다 같은 조치로 해결했다.

영구 적용 — /etc/fstab

mount -o remount는 런타임 변경이라 재부팅하면 원래 크기로 돌아간다. 영구 적용하려면 /etc/fstab에 한 줄을 추가한다.

bash
echo 'tmpfs /run tmpfs defaults,size=16G 0 0' | sudo tee -a /etc/fstab

tmpfs는 "최대 허용량"이지 "즉시 할당"이 아니다. 16G로 설정해도 실제 RAM 사용은 현재 사용량(9G)에 비례하므로, 90G RAM 노드에서 16G tmpfs는 메모리 압박 없이 안전하다.

nodeAffinity 이슈

진단 과정에서 하나 더 발견한 것이 있다. ocr-api deployment의 nodeAffinity에 application과 gpu-llm 모두 포함되어 있었다.

yaml
nodeAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
    - matchExpressions:
      - key: magnum.openstack.org/nodegroup
        operator: In
        values:
        - application
        - gpu-llm    # GPU 노드에도 스케줄 가능

여기에 podAntiAffinity(같은 hostname에 같은 app 중복 불가)가 걸려있어서, application 노드 2대가 점유되면 나머지 pod는 GPU 노드로 밀렸다. 이건 의도된 설정이었지만, GPU 노드의 tmpfs가 부족한 상태에서는 문제가 됐다.

추가 발견: nvidia-container-runtime 과도한 로깅

조사 과정에서 NVIDIA Container Toolkit의 알려진 버그(Issue #511)를 발견했다. exec liveness/readiness probe를 실행할 때 nvidia-container-runtime이 과도한 로그를 /run/containerd/.../log.json에 기록하여 tmpfs를 채우는 문제가 보고되어 있다.

이 클러스터에서 관찰된 nvidia-container-runtime did not terminate successfully: exit status 255 에러와 정확히 일치하는 증상이다. PR #560에서 로깅 verbosity를 줄이는 수정이 반영되었으므로, 후속으로 toolkit 버전 업그레이드 또는 로그 출력 경로를 디스크(/var/log/)로 변경하는 방안을 검토할 예정이다.

정리

항목값
근본 원인GPU 노드의 /run tmpfs(8.9G)가 containerd runtime 메타데이터로 포화
왜 GPU만LLM 이미지의 overlay 레이어 약 50개 + 29개 active task → 커널 메타데이터 누적
해결mount -o remount,size=16G /run
영구 적용/etc/fstab 또는 NKS 노드 설정 변경 필요

이번 이슈에서 배운 건 df -h로 루트 디스크만 보면 안 된다는 것이다. /run 같은 tmpfs는 별도 파일시스템이라 루트와 독립적으로 꽉 찰 수 있고, du도 -x 옵션 없이 쓰면 overlay 마운트를 따라가면서 엉뚱한 숫자를 보여준다.

containerd의 shim v2 아키텍처가 /run에 task 디렉터리와 overlay 마운트 포인트를 저장하는 구조를 알고 나니, GPU 노드처럼 이미지 레이어가 많은 환경에서 왜 tmpfs 기본 크기가 부족해지는지 이해할 수 있었다.

참고 자료

  • Tmpfs — Linux Kernel documentation
  • containerd Runtime V2 문서
  • Runtime v2 and Shim Architecture — DeepWiki
  • Overlay Filesystem — Linux Kernel documentation
  • Understanding OCI Runtimes: containerd, Shims, and the Container Lifecycle
  • CRI & containerd Explained — Medium
  • NVIDIA Container Toolkit Issue #511 — Excessive runtime logging
  • NVIDIA Container Toolkit PR #560 — Reduce logging verbosity
on this page
  • 01증상
  • 02어떤 노드에서 발생했나
  • 03디스크는 충분한데 왜?
  • 04tmpfs와 /run이란
  • tmpfs — RAM 위의 파일시스템
  • /run — 런타임 임시 데이터의 표준 경로
  • 05containerd 아키텍처 — 컨테이너가 실행되기까지
  • Kubelet에서 컨테이너까지의 호출 체인
  • containerd가 /run에 저장하는 것
  • shim v2의 1:1 프로세스 모델
  • 06Overlay 파일시스템과 마운트 포인트
  • Overlay 마운트 구조
  • 커널 메타데이터가 tmpfs를 먹는다
  • 07GPU 노드에서 특히 문제가 되는 이유
  • LLM 이미지의 레이어 수
  • 컨테이너 수
  • 08진단 과정
  • 어떤 노드인지 특정
  • 노드 디스크 vs tmpfs 분리 확인
  • /run 안에서 뭐가 큰지 확인
  • orphan task 식별
  • 09해결
  • 즉시 조치 — tmpfs 크기 확장
  • 영구 적용 — /etc/fstab
  • nodeAffinity 이슈
  • 10추가 발견: nvidia-container-runtime 과도한 로깅
  • 11정리
  • 12참고 자료

이런 글도

  • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
    시니어 백엔드 면접에서 운영 역량을 묻는 질문은 대개 이렇게 시작한다. - “장애를 어떻게 탐지하고 대응하나요?” - “알람이 너무 많이 울리면 어떻게 줄이나요?” - “p99 latency가 튀는데 CPU는 정상입니다. 어디를 보겠습니까?” (p50/p95/p99 가 익숙하지 않으면 Observability 입문 의 "Latency 백분위수" 섹션 참고)...
    🚀 devops
    devops
    2026.05.15
  • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    커머스나 F&B 디지털 채널은 사용자 경험과 매출이 분 단위로 직결된다. 점심·저녁 피크에 주문 실패가 1%만 튀어도 가맹점·콜센터·SNS로 거의 동시에 신호가 들어오고, 30분이 지나면 일일 매출 지표에 흠집이 남는다. 이때 엔지니어가 가장 자주 실패하는 지점은 "장애가 뭔지 몰라서"가 아니라 첫 5분 동안 무엇을 보고 무엇을 결정해야 하는지 합의되어 있...
    🚀 devops
    devops
    2026.05.09
  • 응답을 모두 200으로 래핑하는 환경에서 Prometheus 비즈니스 errorCode 메트릭 만들기
    진행 기간: 2026.04 2026.05 운영 중인 API 서버에서 "어떤 비즈니스 에러가 얼마나 발생하고 있는지"를 Grafana에서 보고 싶었다. Spring Boot Actuator + Micrometer 조합이면 보통 httpserverrequestssecondscount{status="4xx"} 같은 표준 메트릭으로 충분한데, 이 서버는 그게 안...
    🚀 devops
    devops
    2026.05.09
  • K8s 위 Spring Boot 앱 메트릭 수집하기 (Prometheus Agent + remote_write)
    K8s 클러스터에서 Spring Boot 애플리케이션을 운영하다 보면 JVM 힙 사용량, HTTP 요청 수, 응답 시간 같은 지표를 모니터링하고 싶어진다. 팀에 중앙 Grafana가 이미 있다면 클러스터 내에서 Prometheus로 긁어다가 remotewrite로 쏴주기만 하면 된다. 이번에 실제로 적용하면서 삽질한 내용들을 정리해봤다. Spring Boo...
    🚀 devops
    devops
    2026.05.09

댓글 (0)