콘텐츠로 이동

cgroups & Namespace

분류: Layer 4 - CS 기초 — 운영체제 & Linux

Linux Namespace는 프로세스가 볼 수 있는 시스템 세계를 나누는 커널 기능이고, cgroups(Control Groups)는 프로세스 그룹이 얼마나 많은 자원을 쓸 수 있는지 제한하고 관측하는 커널 기능이다.

컨테이너는 보통 이 둘을 함께 쓴다. Namespace로 PID, 네트워크, 마운트, 사용자 같은 시야를 분리하고, cgroups로 CPU, 메모리, I/O 같은 자원 사용량에 상한을 둔다. 그래서 컨테이너는 별도 OS를 부팅한 VM이 아니라, 호스트 Linux 커널 위에서 격리된 프로세스 묶음이다.

이 문서의 핵심 질문은 세 가지다.

  1. 컨테이너 안에서 왜 host 프로세스와 파일시스템이 다르게 보이는가?
  2. docker run --memory=512m --cpus=0.5 같은 설정이 커널에서 어떤 제한으로 바뀌는가?
  3. OOMKilled, CPU throttle, host PID 노출 같은 장애가 보이면 격리 문제인지, 자원 제한 문제인지, 이미지/런타임 문제인지 어떻게 구분하는가?

2. 왜 chroot와 일반 프로세스만으로 부족했나

섹션 제목: “2. 왜 chroot와 일반 프로세스만으로 부족했나”

일반 Linux 프로세스도 이미 독립 주소 공간을 가진다. process-thread.mdx에서 본 것처럼 한 프로세스의 Heap을 다른 프로세스가 마음대로 덮어쓸 수 없고, system-call-interrupt.mdx에서 본 것처럼 파일과 네트워크 접근은 syscall을 통해 커널이 검증한다.

하지만 서버 운영에서는 “메모리를 서로 덮어쓰지 못한다”만으로 부족하다. 같은 호스트에서 여러 서비스를 돌리려면 다음 질문이 생긴다.

  • 한 서비스가 host 전체 프로세스 목록을 보면 안 되지 않는가?
  • 두 서비스가 같은 80 포트를 서로 독립적으로 쓰려면 어떻게 해야 하는가?
  • 앱 A의 루트 디렉터리 /와 앱 B의 루트 디렉터리 /가 다르게 보여야 하지 않는가?
  • 한 서비스가 메모리를 계속 먹으면 호스트 전체가 죽기 전에 멈출 수 있어야 하지 않는가?
  • 컨테이너 안의 root가 host의 진짜 root와 같으면 위험하지 않은가?

chroot()는 이 문제 중 아주 작은 부분만 풀었다. chroot()는 호출 프로세스와 자식 프로세스가 보는 루트 디렉터리만 바꾼다. 파일 경로의 시작점을 바꾸는 데는 유용하지만, 보안 샌드박스나 컨테이너 격리로는 부족하다.

chroot로 남는 문제왜 위험한가Namespace/cgroups가 보완하는 축
PID 가시성/proc가 host 기준이면 다른 프로세스 PID를 볼 수 있다PID Namespace
네트워크 공유같은 host 포트, 라우팅 테이블, 소켓 공간을 공유한다Network Namespace
마운트 경계 부족rootfs를 바꿔도 mount view와 열린 fd가 새어나갈 수 있다Mount Namespace
사용자 권한 공유컨테이너 안 root가 host root에 가까우면 탈출 피해가 커진다User Namespace, capability
자원 상한 부재fork bomb, 메모리 누수, I/O 폭주가 host 전체를 압박한다cgroups v2

여기서 중요한 분리는 격리제한이다.

  • Namespace의 철학: “무엇이 보이는가를 바꾼다.”
  • cgroups의 철학: “얼마나 쓸 수 있는가를 바꾼다.”

둘은 서로 대체 관계가 아니다. PID Namespace가 있어도 메모리 사용량은 제한하지 못하고, memory.max가 있어도 host 프로세스 목록을 숨기지는 못한다. 컨테이너 런타임은 이 두 축을 같이 조립한다.

2.5 선행 기술의 한계와 Namespace+cgroups의 등장

섹션 제목: “2.5 선행 기술의 한계와 Namespace+cgroups의 등장”

Namespace와 cgroups는 “여러 프로세스를 한 host에서 같이 돌리되, 서로의 세계와 자원 사용량을 분리해야 한다”는 문제에서 등장했다.

chroot 시대의 핵심 실패는 “프로세스를 별도 공간에 넣었다고 생각했지만, 실제로는 파일시스템 경로 일부만 바뀌었다”는 점이다. 한 프로세스가 다른 프로세스의 PID를 볼 수 있고, 같은 네트워크 스택을 쓰며, 메모리와 CPU를 무제한으로 쓸 수 있다면 멀티테넌트 실행 환경은 성립하지 않는다.

Linux는 이 문제를 하나의 거대한 컨테이너 기능으로 풀지 않았다. 대신 메인라인 커널에 작은 기능들을 단계적으로 넣고, Docker, containerd, runc 같은 런타임이 이를 조합하게 했다.

Linux 격리·제한 메커니즘의 메인라인 통합

  1. 2002: Mount Namespace

    파일시스템 마운트 뷰를 분리해 chroot보다 안전한 rootfs 경계를 만들 수 있게 했다.

  2. 2008: PID/Network Namespace + cgroups v1

    프로세스 번호 공간, 네트워크 스택, 자원 제한을 분리해 컨테이너의 뼈대가 갖춰졌다.

  3. 2013: User Namespace

    컨테이너 안 UID/GID를 host의 다른 UID/GID로 매핑해 root 권한의 폭발 반경을 줄였다.

  4. 2016 이후: cgroup Namespace + cgroups v2

    컨테이너가 자기 cgroup 경로만 보게 하고, 컨트롤러별 다중 계층을 단일 트리로 정리했다.

이 토픽이 사라지면 Docker의 핵심 설명이 빈다. Dockerfile과 이미지는 L5에서 다루지만, 실행된 컨테이너가 “왜 host와 다르게 보이는지”와 “왜 512MiB를 넘으면 죽는지”는 L4의 커널 기능으로 설명해야 한다.

아래 용어는 뒤에서 반복된다. 처음에는 내부 구현보다 “무엇을 구분하기 위한 말인가”를 잡으면 된다.

용어첫 이해왜 필요한가
Namespace프로세스가 보는 특정 시스템 자원의 이름 공간 또는 시야같은 host에서도 PID, 네트워크, 마운트가 다르게 보이게 한다
cgroup자원 사용량을 제한·집계하는 프로세스 그룹컨테이너가 CPU, 메모리, I/O를 무제한 쓰지 못하게 한다
cgroups v2cgroup 컨트롤러를 단일 트리(/sys/fs/cgroup)로 통합한 현재 모델CPU, memory, io 제한 파일을 같은 cgroup 경로에서 읽고 쓴다
PID Namespace프로세스 ID 번호 공간을 분리한다컨테이너 안에서 자기 프로세스가 PID 1처럼 보인다
Network Namespace네트워크 인터페이스, 라우팅 테이블, 소켓 공간을 분리한다컨테이너마다 IP와 포트 공간을 따로 갖게 한다
Mount Namespace마운트된 파일시스템 목록과 rootfs view를 분리한다컨테이너가 이미지 rootfs와 volume만 보게 한다
User Namespace컨테이너 안 UID/GID를 host의 다른 UID/GID로 매핑한다컨테이너 안 root가 host root와 같지 않게 만들 수 있다
cpu.maxcgroups v2에서 CPU quota와 period를 나타내는 파일50000 100000이면 100ms 중 50ms만 CPU를 쓸 수 있다
memory.maxcgroups v2에서 메모리 하드 제한을 나타내는 파일초과하면 cgroup 내부 OOM kill이 발생할 수 있다
memory.high메모리 소프트 제한이다초과하면 reclaim과 throttling으로 느려질 수 있지만 바로 죽지는 않는다
OOMKilledcgroup 또는 노드 메모리 압박으로 프로세스가 강제 종료된 상태앱 예외가 아니라 커널의 메모리 방어일 수 있다
throttling자원 한도 때문에 프로세스 실행을 일부러 늦추는 것CPU나 memory.high 초과에서 p99 latency가 튈 수 있다
container runtime컨테이너 실행을 실제 커널 기능 호출로 바꾸는 계층Docker CLI의 요청이 커널 설정으로 내려가는 길이다
containerd이미지 관리와 컨테이너 생명주기를 다루는 고수준 런타임Docker와 Kubernetes가 컨테이너 실행을 위임하는 중간 계층이다
runcOCI(Open Container Initiative) 스펙을 받아 실제 Namespace, cgroup, mount를 설정하는 저수준 런타임clone(), cgroup 파일 쓰기, rootfs 준비가 이 층에서 일어난다

시스템 콜 관점에서는 clone(), unshare(), setns()를 함께 기억하면 좋다.

  • clone()은 새 실행 흐름을 만들 때 어떤 자원을 공유하고 어떤 Namespace를 새로 만들지 플래그로 고르는 Linux 입구다.
  • unshare()는 이미 실행 중인 프로세스가 일부 자원을 새 Namespace로 분리할 때 쓴다.
  • setns()는 프로세스가 기존 Namespace에 들어갈 때 쓴다. docker exec나 디버깅 도구의 내부 모델을 이해할 때 도움이 된다.

4. Namespace — “각자의 세계”를 만드는 메커니즘

섹션 제목: “4. Namespace — “각자의 세계”를 만드는 메커니즘”

Namespace는 커널 객체 자체를 복사하지 않는다. 같은 host 커널을 쓰되, 프로세스가 특정 자원을 조회할 때 어느 이름 공간을 기준으로 볼지 바꾼다.

예를 들어 host에서 nginx 프로세스의 실제 PID가 12847일 수 있다. 그런데 그 프로세스가 새 PID Namespace 안에서 실행되면 컨테이너 내부에서는 PID 1로 보인다. 숫자가 “거짓”인 것이 아니라, 서로 다른 PID 번호 공간에서 같은 task를 다르게 부르는 것이다.

Namespace별로 분리되는 시스템 뷰

PID

프로세스 ID 번호 공간을 분리한다. 컨테이너 안 메인 프로세스가 PID 1이 된다.

컨테이너 내부 `ps`가 host 전체 프로세스를 보지 않아야 할 때

NET

네트워크 인터페이스, 라우팅 테이블, 포트 바인딩 공간을 분리한다.

컨테이너마다 독립 IP와 포트 공간을 가져야 할 때

MNT

마운트 목록과 rootfs view를 분리한다. chroot보다 넓은 파일시스템 경계다.

이미지 레이어, volume, `/proc` mount를 컨테이너별로 나눌 때

USER

컨테이너 안 UID/GID를 host의 다른 UID/GID로 매핑한다.

컨테이너 안 root 권한이 host root 권한으로 이어지지 않게 할 때

UTS/IPC

hostname/domainname, System V IPC, POSIX message queue 같은 이름 공간을 분리한다.

컨테이너별 hostname이나 공유 메모리 충돌을 막을 때

CGROUP

프로세스가 보는 cgroup 경로를 자기 범위로 좁힌다.

컨테이너 내부에서 host 전체 cgroup 트리를 노출하고 싶지 않을 때

가장 작은 사고 실험은 PID 1이다.

관찰 위치같은 nginx master가 어떻게 보이는가의미
hostPID 12847host PID Namespace의 실제 task 번호
컨테이너 내부PID 1컨테이너 PID Namespace에서 첫 프로세스
/proc/12847/ns/pidpid:[4026532253] 같은 inode같은 inode면 같은 PID Namespace

여기서 PID 1은 특별하다. 컨테이너 안에서 PID 1이 된 프로세스는 일반 앱 프로세스처럼 보이지만, 고아 자식 프로세스를 reap하고 종료 신호를 처리해야 하는 init 역할도 일부 떠안는다. Node.js 앱을 바로 PID 1로 실행하면 SIGTERM이 기대대로 처리되지 않거나 zombie가 쌓이는 문제가 생길 수 있다.

반례: “PID Namespace가 있으니 컨테이너 안 프로세스가 host에 전혀 존재하지 않는다”는 말은 틀렸다. host 커널 입장에서는 여전히 task가 있고 host PID도 있다. Namespace는 존재를 지우는 것이 아니라, 특정 프로세스가 보는 번호 공간을 바꾼다.

Network Namespace는 컨테이너마다 독립된 네트워크 스택을 제공한다. Docker 기본 bridge 네트워크에서는 보통 다음 구조가 생긴다.

  1. 컨테이너 안에는 eth0와 loopback(lo)이 보인다.
  2. host에는 그 eth0와 이어지는 veth pair의 반대쪽이 보인다.
  3. host의 bridge, NAT, iptables/nftables 규칙이 외부 요청을 컨테이너 IP와 포트로 전달한다.

그래서 -p 8080:3000은 host의 8080 포트를 컨테이너 Network Namespace 안의 3000 포트로 연결하는 설정이다. 컨테이너 안에서 localhost:3000은 자기 Namespace 안의 loopback이고, host의 localhost:3000과 같지 않다.

반례: “컨테이너마다 포트 공간이 독립이면 host 포트 충돌이 없어야 한다”는 말은 반만 맞다. 컨테이너 내부 포트는 독립적이지만, 외부에서 접근하려고 host 포트에 publish하면 host 포트는 여전히 전역 자원이다. 두 컨테이너가 모두 -p 8080:3000을 요구하면 host 8080에서 충돌한다.

Mount Namespace는 프로세스가 보는 마운트 목록을 분리한다. 컨테이너가 /를 보면 host의 /가 아니라 이미지 레이어와 volume이 조합된 rootfs를 본다. Docker 문서에서 말하는 OverlayFS, bind mount, volume은 이 Mount Namespace의 view 위에서 보인다.

User Namespace는 권한 경계와 연결된다. 컨테이너 안에서 UID 0(root)인 프로세스를 host의 일반 UID로 매핑할 수 있다. 이 기능이 없으면 컨테이너 내부 root가 host 자원에 접근했을 때 피해 범위가 커진다. 다만 User Namespace만으로 모든 보안 문제가 해결되는 것은 아니다. capability, seccomp, AppArmor/SELinux, read-only filesystem, volume 정책이 함께 필요하다.

작은 비유로는 브라우저의 iframe sandbox를 떠올릴 수 있다. 같은 브라우저 프로세스 안에서도 DOM, origin, storage 접근 범위를 제한한다는 점은 비슷하다. 다만 Linux Namespace는 브라우저 API가 아니라 커널이 프로세스의 PID, 네트워크, 마운트 같은 OS 자원 view를 바꾸는 기능이다.

Namespace는 “보이는 세계”를 바꾼다. 그렇기 때문에 다음 일을 직접 하지는 않는다.

Namespace가 하지 않는 일대신 봐야 할 축
CPU를 0.5개까지만 쓰게 하기cgroups cpu.max, cpu.weight
메모리 512MiB 초과 시 죽이기cgroups memory.max, OOM killer
이미지 파일을 만들고 배포하기Docker image, registry, OverlayFS
syscall을 허용/차단하기seccomp, capability, LSM(Linux Security Modules)
커널 취약점으로부터 완전히 분리하기VM, microVM, gVisor/Kata 같은 sandboxed runtime

이 경계를 알아야 장애를 잘못 분류하지 않는다. ps에 host 프로세스가 보이면 Namespace 문제일 수 있지만, exit code 137은 보통 cgroup 메모리 제한 문제다.

5. cgroups v2 — 자원 사용량을 제한하고 관측하는 메커니즘

섹션 제목: “5. cgroups v2 — 자원 사용량을 제한하고 관측하는 메커니즘”

cgroups(Control Groups)는 프로세스를 그룹으로 묶고 그 그룹의 자원 사용량을 제한, 집계, 관측하는 커널 기능이다. 현재 학습 기준은 cgroups v2다. cgroups v2는 여러 컨트롤러를 단일 계층 구조(/sys/fs/cgroup)로 통합한다.

컨트롤러제어 대상대표 파일첫 이해
cpuCPU quota와 가중치cpu.max, cpu.weight, cpu.stat얼마나 오래 CPU를 쓸 수 있는가
memory메모리 한도와 압박memory.max, memory.high, memory.current, memory.events얼마까지 메모리를 쓰고, 넘으면 어떻게 되는가
io블록 I/O 대역폭과 가중치io.max, io.weight, io.pressure디스크 읽기/쓰기 사용량을 어떻게 나눌 것인가
pids생성 가능한 task 수pids.max, pids.currentfork bomb이나 과도한 child process 생성을 막을 수 있는가

cgroups v1은 컨트롤러별로 별도 계층을 만들 수 있었다. 같은 프로세스가 memory 계층에서는 A 경로에, cpu 계층에서는 B 경로에 속할 수 있어 운영자가 실제 제한을 추적하기 어려웠다.

cgroups v2는 단일 트리를 쓴다.

/sys/fs/cgroup/
system.slice/
docker-<id>.scope/
cgroup.procs
cpu.max
cpu.stat
memory.current
memory.events
memory.high
memory.max
io.max

이 단일 트리 덕분에 “이 프로세스 묶음의 CPU 제한, 메모리 제한, I/O 제한이 무엇인가”를 같은 경로에서 읽을 수 있다. Docker, containerd, kubelet, systemd는 이 트리의 하위 디렉터리를 만들어 프로세스를 배치한다.

비교 항목cgroups v1cgroups v2
계층 구조컨트롤러별 다중 계층단일 통합 트리
메모리 하드 제한memory.limit_in_bytesmemory.max
CPU 쿼터cpu.cfs_quota_us + cpu.cfs_period_uscpu.max
I/O 컨트롤러 이름blkioio
rootless/위임 모델제한적이고 복잡v2에서 더 일관적

첫 회독에서는 파일 이름이 바뀐다는 점보다 v2에서는 한 프로세스 묶음이 하나의 cgroup 경로에 속하고, 그 경로 아래 파일들이 제한과 관측 지표가 된다는 점을 잡으면 된다.

5.2 cpu.max — CPU를 몇 ms 쓸 수 있는가

섹션 제목: “5.2 cpu.max — CPU를 몇 ms 쓸 수 있는가”

cpu.maxquota period 형식이다. 기본 단위는 마이크로초다.

cpu.max학습용 해석Docker/K8s 감각
max 100000제한 없음CPU limit 없음
50000 100000100ms 주기마다 50ms 사용 가능0.5 CPU
25000 100000100ms 주기마다 25ms 사용 가능0.25 CPU
200000 100000100ms 주기마다 200ms 사용 가능2 CPU

이 제한은 평균 CPU 사용률만 낮추는 것이 아니다. 특정 100ms 구간에서 quota를 다 쓰면 다음 period가 올 때까지 task가 멈출 수 있다. 그래서 평균 CPU 사용률이 50% 이하로 보여도 요청 burst, JSON 직렬화, GC, 암호화 작업이 한 구간에 몰리면 p99 latency가 튄다.

수치 예시: cpu.max = 50000 100000인 컨테이너가 100ms 동안 요청을 처리한다. 처음 40ms는 JS 실행, 다음 10ms는 GC, 그 뒤 20ms가 더 필요하다고 하자. 앞의 50ms에서 quota를 모두 썼기 때문에 나머지 20ms 작업은 다음 period까지 기다린다. 애플리케이션 로그에는 “CPU 사용률 50%“처럼 보일 수 있지만, 사용자 요청은 period 경계에서 지연된다.

cpu.weight는 quota와 다르다. quota는 상한이고, weight는 CPU가 부족할 때 나눠 갖는 비율이다. limit 없이 weight만 있으면 idle CPU를 더 쓸 수 있다. 반대로 quota가 낮으면 host에 CPU가 남아 있어도 해당 cgroup은 period 안에서 더 못 쓸 수 있다.

5.3 memory.maxmemory.high — 죽일 것인가, 느리게 할 것인가

섹션 제목: “5.3 memory.max와 memory.high — 죽일 것인가, 느리게 할 것인가”

memory.max는 하드 제한이다. cgroup의 메모리 사용량이 이 값을 넘고 reclaim으로도 해결되지 않으면 cgroup 내부에서 OOM kill이 발생할 수 있다. Docker의 --memory=512m, Kubernetes의 limits.memory: 512Mi는 이 축으로 이해하면 된다.

memory.high는 소프트 제한이다. 이 값을 넘으면 커널이 해당 cgroup의 메모리 회수를 강하게 시도하고, 프로세스가 느려질 수 있다. 하지만 memory.high 초과만으로 바로 kill되지는 않는다.

파일제한 성격초과 시 현상운영에서 보이는 신호
memory.high소프트 제한reclaim, throttling, latency 증가memory.eventshigh 증가, memory.pressure 상승
memory.max하드 제한cgroup OOM, 프로세스 killOOMKilled, exit code 137, memory.eventsoom_kill 증가

수치 예시: memory.max = 536870912라면 512MiB다. 같은 컨테이너에서 memory.high = 483183820이라면 대략 461MiB, 즉 max의 약 90%다. 이 설계는 512MiB에 닿기 전에 reclaim 압박을 보게 하려는 의도다. 다만 너무 낮게 잡으면 정상 워크로드도 계속 reclaim에 걸려 p99 latency가 커진다.

선택 심화: 일부 Kubernetes/커널 조합은 memory.high를 Memory QoS 같은 기능과 함께 다루거나, 오래된 환경에서는 기대한 방식으로 노출하지 않을 수 있다. 그래서 memory.high는 “항상 kill 전에 자동 완충해주는 장치”가 아니라, 실제 노드와 런타임에서 memory.eventshigh 증가와 latency 변화를 같이 확인해야 하는 압박 신호로 읽는다.

반례: free -m에서 host 메모리가 충분하다고 해서 컨테이너 OOM이 불가능한 것은 아니다. cgroup은 host 전체 RAM과 별도의 상한을 둔다. host에 32GiB가 남아 있어도 어떤 컨테이너의 memory.max가 512MiB이고 그 컨테이너가 530MiB를 쓰면 OOMKilled가 될 수 있다.

5.4 I/O와 pids 제한은 왜 같이 봐야 하나

섹션 제목: “5.4 I/O와 pids 제한은 왜 같이 봐야 하나”

CPU와 메모리가 가장 자주 보이지만, 컨테이너 운영에서는 I/O와 프로세스 수 제한도 중요하다.

  • io.max는 특정 block device에 대해 읽기/쓰기 대역폭이나 IOPS 상한을 걸 수 있다.
  • io.pressure는 I/O 때문에 task가 얼마나 기다렸는지 보여준다.
  • pids.max는 cgroup 안에서 만들 수 있는 task 수를 제한한다. fork bomb이나 과도한 child_process.spawn()으로 host가 마비되는 일을 줄인다.

작은 반례: CPU와 메모리가 충분한데도 API가 느릴 수 있다. 로그를 동기 파일로 많이 쓰거나, 컨테이너 내부 임시 디스크에 대량 파일을 쓰면 I/O wait가 병목이 된다. 이때 CPU limit을 올려도 해결되지 않는다. cgroup의 io.pressure, host의 disk latency, 애플리케이션의 logging 방식을 같이 봐야 한다.

5.5 cgroup 파일을 읽는 법 — 값, 이벤트, 압박을 나눠 본다

섹션 제목: “5.5 cgroup 파일을 읽는 법 — 값, 이벤트, 압박을 나눠 본다”

cgroup 파일은 모두 같은 성격이 아니다. 어떤 파일은 현재 설정값이고, 어떤 파일은 누적 이벤트이며, 어떤 파일은 최근 압박 정도를 보여준다. 이 구분을 못 하면 “제한이 있다”와 “실제로 제한에 걸렸다”를 섞게 된다.

파일 유형예시질문해석
설정값cpu.max, memory.max, memory.high, pids.max어떤 상한이 걸려 있는가?배포 스펙과 런타임 설정의 결과
현재값memory.current, pids.current지금 얼마나 쓰고 있는가?순간 사용량. peak를 놓칠 수 있다
누적 이벤트memory.events, cpu.stat제한에 실제로 걸린 적이 있는가?oom_kill, high, nr_throttled 같은 실패 흔적
압박 지표memory.pressure, io.pressure, cpu.pressuretask가 자원 부족 때문에 얼마나 기다렸는가?죽지 않는 성능 저하를 찾는 단서

worked example: 컨테이너가 느려졌는데 memory.current가 410MiB이고 memory.max가 512MiB라면 겉으로는 여유가 있어 보인다. 하지만 memory.eventshigh가 계속 증가하고 memory.pressure some avg10이 20%를 넘으면, 최근 10초 동안 많은 task가 reclaim 때문에 지연됐다는 뜻이다. 이 문제는 “아직 max에 안 닿았으니 메모리 문제가 아니다”가 아니라 “하드 OOM 전의 소프트 압박”으로 읽어야 한다.

또 다른 반례: cpu.statusage_usec가 낮다고 해서 CPU 문제가 없다는 뜻은 아니다. nr_throttled가 높으면 quota 때문에 실행 기회를 잃고 있을 수 있다. 반대로 nr_throttled가 거의 0인데 CPU 사용률이 높다면 quota보다 앱의 CPU-bound 작업이나 worker 수를 먼저 본다.

5.6 작은 사이징 예시 — 512MiB 컨테이너를 어떻게 나눠 볼까

섹션 제목: “5.6 작은 사이징 예시 — 512MiB 컨테이너를 어떻게 나눠 볼까”

컨테이너 메모리 limit을 정할 때 memory.max 숫자만 외우면 부족하다. Node.js 서버 하나가 512MiB 컨테이너에서 돈다고 해보자.

영역학습용 예시왜 남겨야 하는가
V8 old space350~400MiB 시작점JS 객체가 오래 살아남는 공간. limit과 같게 잡으면 여유가 없다
native/Buffer/external40~80MiB이미지 처리, HTTP body, crypto, addon이 JS heap 밖 메모리를 쓴다
스레드 stack/런타임/라이브러리30~60MiBNode 런타임, libuv thread pool, shared library가 RSS에 포함된다
파일 캐시와 순간 peak상황별upload, JSON stringify, 압축에서 순간적으로 RSS가 뛴다

이 숫자는 정답이 아니라 사고 모델이다. memory.max가 512MiB인데 V8 heap을 512MiB로 잡으면, JS heap 밖 메모리가 들어갈 공간이 없다. 그래서 컨테이너 OOM은 heapUsed가 512MiB에 닿기 전에 발생할 수 있다.

결정 기준:

  • 정상 peak가 limit의 80~90%를 자주 넘으면 limit 상향, batch 크기 축소, worker 분리를 검토한다.
  • heapUsed만 증가하면 JS 객체 누수를 의심한다.
  • heapUsed는 안정적인데 rss가 증가하면 Buffer, native addon, mmap, 이미지/압축 작업을 의심한다.
  • peak가 짧고 자주 반복되면 memory.high와 pressure 지표를 함께 본다. OOM은 없지만 지연이 누적될 수 있다.

6. 컨테이너는 Namespace + cgroups + rootfs 조합이다

섹션 제목: “6. 컨테이너는 Namespace + cgroups + rootfs 조합이다”

컨테이너는 독립 OS가 아니다. 컨테이너는 다음 세 가지를 조합한 실행 단위다.

  1. Namespace: 프로세스가 보는 PID, 네트워크, 마운트, 사용자, IPC view를 분리한다.
  2. cgroups: 그 프로세스 묶음이 쓸 수 있는 CPU, 메모리, I/O, task 수를 제한하고 관측한다.
  3. rootfs와 mount: 이미지 레이어, overlayfs, volume을 조합해 컨테이너 안 파일시스템 view를 만든다.
컨테이너를 만드는 커널 축
flowchart TD
Kernel["Host Linux 커널"] --> NS["Namespace: 보이는 세계 분리"]
Kernel --> CG["cgroups v2: 자원 제한과 관측"]
Kernel --> MNT["Mount/rootfs: 이미지와 volume view"]
NS --> PID["PID: 컨테이너 안 PID 1"]
NS --> NET["NET: 전용 IP·포트·라우팅"]
NS --> USER["USER: UID/GID 매핑"]
CG --> CPU["cpu.max: CPU quota"]
CG --> MEM["memory.max/high: OOM 또는 reclaim"]
CG --> IO["io.max/pressure: I/O 제한과 압박"]
MNT --> ROOTFS["overlayfs rootfs + volume"]

VM과 비교하면 경계가 더 선명해진다.

비교 축VM컨테이너
커널VM마다 게스트 커널host 커널 공유
시작 비용OS 부팅이 필요해 보통 더 느림프로세스 시작에 가까워 빠름
격리 강도하이퍼바이저와 게스트 커널 경계Namespace/cgroups와 커널 보안 기능 경계
적합한 경우커널 버전, 강한 테넌트 격리, 디바이스 요구가 핵심같은 커널 위에서 앱 단위 배포와 자원 효율이 핵심

컨테이너 not-a-VM 오해: 컨테이너 안에서 Ubuntu 파일들이 보인다고 해서 Ubuntu 커널이 따로 부팅된 것은 아니다. uname -r은 컨테이너 이미지의 배포판 버전이 아니라 host Linux 커널 버전을 보여준다. Docker Desktop처럼 macOS/Windows에서 Docker를 쓰는 경우에는 중간에 Linux VM이 있으므로, 컨테이너는 그 VM의 Linux 커널을 공유한다.

7. docker run 요청은 어떻게 커널 설정으로 내려가는가

섹션 제목: “7. docker run 요청은 어떻게 커널 설정으로 내려가는가”

docker run --memory=512m --cpus=0.5 nginx를 실행하면 사용자는 Docker CLI만 보지만, 내부에서는 여러 계층이 움직인다.

  1. Docker CLI가 Docker daemon에 컨테이너 생성을 요청한다.
  2. Docker daemon은 이미지, 네트워크, volume 설정을 확인하고 containerd에 실행을 위임한다.
  3. containerd는 컨테이너 생명주기와 shim 프로세스를 관리하고, OCI 스펙을 runc에 넘긴다.
  4. runc는 Namespace, cgroup, mount 설정을 준비한 뒤 컨테이너 프로세스를 시작한다.
  5. 커널은 이후 해당 cgroup의 CPU, 메모리, I/O 사용량을 강제하고 관측한다.

학습용으로 단순화하면 다음처럼 볼 수 있다.

docker run --memory=512m --cpus=0.5 nginx
-> runc
-> clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | ...)
-> cgroup 디렉터리 생성
-> memory.max = 536870912
-> cpu.max = 50000 100000
-> overlayfs rootfs mount
-> nginx를 새 Namespace 안에서 PID 1로 실행

Kubernetes도 본질은 같다. Pod spec의 resources.requestsresources.limits는 kubelet, containerd, runc를 거쳐 cgroup 파일과 스케줄링 정책으로 변환된다.

K8s 스펙cgroups v2 감각의미
limits.memory: 512Mimemory.max = 536870912넘으면 OOMKilled 가능
requests.memory: 256Mi환경에 따라 memory.low 같은 보호/스케줄링 힌트와 연결될 수 있음이 정도 메모리는 보호받아야 한다는 의도
limits.cpu: 500mcpu.max = 50000 1000000.5 CPU quota
requests.cpu: 250mcpu.weight나 스케줄링 힌트와 연결CPU 경합 시 비율과 배치 판단에 영향

정확한 파일 매핑은 Kubernetes 버전, cgroup driver, QoS class, 노드 OS에 따라 달라질 수 있다. 하지만 첫 학습에서는 “요청/제한 값이 결국 커널의 cgroup 제한과 관측 지표로 내려간다”는 방향을 잡으면 충분하다.

8. 결정 기준 — isolation, resource limit, image/runtime issue 구분하기

섹션 제목: “8. 결정 기준 — isolation, resource limit, image/runtime issue 구분하기”

컨테이너 장애를 볼 때 가장 먼저 할 일은 문제의 축을 나누는 것이다.

컨테이너 문제를 나누는 세 축

Isolation issue

보이면 안 되는 프로세스, 네트워크, hostname, 파일 경로가 보인다.

`--pid=host`, `hostNetwork: true`, 잘못된 volume mount, user namespace 미사용을 의심한다.

Resource limit issue

메모리, CPU, I/O, task 수 제한 때문에 느려지거나 죽는다.

`OOMKilled`, `memory.events`, `cpu.stat`, `io.pressure`, `pids.current`를 본다.

Image/runtime issue

프로세스가 시작하지 못하거나, 파일이 없거나, entrypoint/CMD가 잘못됐다.

`exec format error`, `command not found`, missing env, wrong image tag, bad healthcheck를 본다.

실무에서 자주 쓰는 1차 분류는 다음과 같다.

관찰 신호먼저 의심할 축이유
OOMKilled: true, exit code 137Resource limitmemory.max 초과 또는 노드 메모리 압박으로 kill
낮은 평균 CPU인데 p99만 급등Resource limitcpu.max period 안에서 throttle 가능
컨테이너 안 ps에 host 프로세스가 보임IsolationPID Namespace 공유 또는 hostPID
컨테이너 안 hostname이 host와 같음IsolationUTS Namespace 공유
host 포트가 이미 사용 중Isolation + runtime config내부 포트가 아니라 host publish 포트 충돌
/app/server.js가 없어서 시작 실패Image/runtime이미지 빌드, workdir, COPY, CMD 문제
컨테이너 안 root가 host 파일을 수정 가능Isolation/securityvolume mount, user namespace, capability 문제
fork: Resource temporarily unavailableResource limit 또는 PID 1 문제pids.max 도달, zombie 누적 가능

이 구분이 중요한 이유는 해결 방향이 다르기 때문이다. OOMKilled를 이미지 재빌드로 고칠 수 없고, 잘못된 CMD를 memory limit 상향으로 고칠 수 없다.

8.1 같은 증상처럼 보여도 축이 다른 경우

섹션 제목: “8.1 같은 증상처럼 보여도 축이 다른 경우”

컨테이너 장애는 모두 “컨테이너가 안 된다”로 묶여 보이지만, 원인은 서로 다르다.

겉으로 보이는 현상잘못된 빠른 결론더 나은 분류
컨테이너가 계속 재시작됨Kubernetes가 불안정하다OOMKilled, healthcheck 실패, process crash, bad CMD를 분리
앱이 느림CPU가 부족하다CPU throttle, memory pressure, I/O wait, DB latency를 분리
컨테이너 안 파일이 없음volume 문제다이미지 빌드 누락, working directory, mount 덮어쓰기, entrypoint를 분리
host에서 포트 접속 안 됨앱 서버가 죽었다app bind address, container port, host publish port, security group을 분리
root 권한 문제가 남User Namespace가 꺼졌다user namespace, container user, capability, volume permission을 함께 확인

학습할 때는 “한 번에 고치는 명령”보다 이 분류가 더 중요하다. 분류가 맞으면 필요한 명령은 짧아지고, 분류가 틀리면 많은 명령을 실행해도 원인에서 멀어진다.

8.2 이미지와 런타임은 어디서 갈라지는가

섹션 제목: “8.2 이미지와 런타임은 어디서 갈라지는가”

Docker Basics로 넘어가기 전에 이미지 문제와 런타임 문제를 구분해 둔다.

질문이미지 쪽이면런타임 쪽이면
파일이 없다Dockerfile COPY, build context, .dockerignore 문제volume mount가 이미지 파일을 덮어쓴 문제
명령이 없다base image에 binary가 없음, multi-stage copy 누락entrypoint 인자, shell form/exec form 문제
권한이 없다이미지 안 파일 owner/mode가 잘못됨container user, user namespace, bind mount 권한 문제
실행 중 죽는다앱 코드나 dependency 문제cgroup limit, signal, healthcheck, runtime policy 문제
네트워크가 안 된다앱이 포트를 열지 않음Network Namespace, port publish, Service/Ingress 문제

이 표는 Docker 문서의 상세 범위를 빼앗기 위한 것이 아니다. 이 문서에서는 “커널 격리와 제한 문제인지, 이미지 패키징 문제인지”를 먼저 나누는 기준만 가져간다.

9. 실패 신호로 배우는 Namespace와 cgroups

섹션 제목: “9. 실패 신호로 배우는 Namespace와 cgroups”

이 절은 긴 복구 런북이 아니라 실패 원리를 배우기 위한 지도다. 명령어는 관찰의 최소 단서로만 둔다.

9.1 OOMKilled — host 메모리가 아니라 cgroup 한도를 봐야 한다

섹션 제목: “9.1 OOMKilled — host 메모리가 아니라 cgroup 한도를 봐야 한다”

증상: ECS task나 Kubernetes Pod가 갑자기 재시작된다. 상태에 OOMKilled: true가 보이거나 exit code 137로 종료된다.

원리: 프로세스가 속한 cgroup의 memory.max를 넘었고 reclaim으로 해결되지 않으면 커널이 cgroup 내부 프로세스를 kill한다. 컨테이너의 메인 프로세스가 죽으면 컨테이너도 종료된다.

작은 관찰 포인트:

Terminal window
cat /sys/fs/cgroup/<scope>/memory.events
# high 0
# max 3
# oom 1
# oom_kill 1

oom_kill이 증가했다면 앱 예외보다 cgroup 메모리 제한을 먼저 본다. Node.js라면 heapUsed만 보지 말고 rss, native buffer, image 처리 버퍼, thread stack, V8 old space 한도를 함께 봐야 한다.

판단 기준:

  • 트래픽 없이도 heapUsedrss가 계속 증가하면 누수 가능성이 크다.
  • 특정 batch, upload, report job에서만 치솟으면 작업 단위 크기와 buffer 사용량을 본다.
  • limit이 너무 낮고 정상 peak가 반복적으로 닿는다면 limit 상향 또는 workload 분리가 필요하다.
  • heapUsed는 낮은데 rss만 높으면 native memory, Buffer, shared library, mmap을 의심한다.

9.2 memory.high throttling — 죽지 않았는데 느려지는 경우

섹션 제목: “9.2 memory.high throttling — 죽지 않았는데 느려지는 경우”

증상: 컨테이너가 재시작되지는 않지만 응답이 점점 느려진다. 커널 로그에는 특별한 OOM 메시지가 없다.

원리: memory.high는 kill이 아니라 reclaim과 throttling을 유도한다. 이 과정은 별도 에러 로그 없이 memory.eventshigh 카운터와 memory.pressure에만 보일 수 있다.

Terminal window
cat /sys/fs/cgroup/<scope>/memory.events
# high 14
# max 0
# oom 0
# oom_kill 0
cat /sys/fs/cgroup/<scope>/memory.pressure
# some avg10=23.45 avg60=18.32 avg300=9.11 total=...
# full avg10=5.12 avg60=3.40 avg300=1.21 total=...

반례: “OOMKilled가 아니니 메모리 문제는 아니다”는 틀렸다. OOM은 하드 실패이고, memory.high는 성능 저하형 실패다. 재시작이 없어서 더 늦게 발견될 수 있다.

9.3 CPU throttle — 평균 CPU만 보면 놓친다

섹션 제목: “9.3 CPU throttle — 평균 CPU만 보면 놓친다”

증상: CPU 사용률은 50% 아래로 보이는데 p99 latency가 급등한다. 특히 요청 burst, JSON 직렬화, GC, 압축, 암호화 시점에 튄다.

원리: cpu.max = 50000 100000이면 100ms period마다 50ms quota를 다 쓰는 순간 다음 period까지 실행이 밀린다. 평균 CPU는 낮아 보여도 period 단위로는 자주 막힐 수 있다.

Terminal window
cat /sys/fs/cgroup/<scope>/cpu.stat
# nr_periods 1200
# nr_throttled 348
# throttled_usec 34800000

nr_throttled / nr_periods가 348/1200이면 약 29%의 period에서 throttle이 발생했다. 이 경우 CPU limit을 올리거나, limit 제거가 가능한 서비스인지 보거나, CPU가 긴 작업을 worker/queue로 분리한다.

9.4 host PID가 보임 — PID Namespace 경계가 열린 상태

섹션 제목: “9.4 host PID가 보임 — PID Namespace 경계가 열린 상태”

증상: 컨테이너 안에서 ps aux를 실행했는데 host의 프로세스가 보인다. 컨테이너 메인 프로세스가 PID 1이 아니라 host의 높은 PID처럼 보인다.

원리: PID Namespace가 분리되지 않았거나 --pid=host, Kubernetes hostPID: true 같은 설정으로 host PID Namespace를 공유한다.

판단 기준:

  • 디버깅, observability agent, 일부 low-level system tool은 host PID 접근이 필요할 수 있다.
  • 일반 웹 애플리케이션 컨테이너가 host PID를 공유하는 것은 보통 위험하다.
  • host PID 공유는 “편해서 켜는 옵션”이 아니라 보안 리뷰가 필요한 예외로 취급한다.

9.5 PID 1 문제 — zombie와 graceful shutdown 실패

섹션 제목: “9.5 PID 1 문제 — zombie와 graceful shutdown 실패”

증상: docker stop이 timeout까지 기다린 뒤 SIGKILL로 끝난다. <defunct> 프로세스가 누적된다. 앱 로그에는 명확한 에러가 없다.

원리: PID Namespace 안의 PID 1은 일반 프로세스와 다르게 신호 처리와 자식 reap 책임을 갖는다. Node.js, Python, Java 앱은 보통 자신이 init 프로세스가 될 것을 가정하지 않는다.

학습용 판단 기준:

  • child process를 만들지 않는 단순 앱이라도 SIGTERM handler는 필요하다.
  • child process를 자주 만들면 init wrapper(tini, Docker --init)나 명시적 reap 전략이 필요하다.
  • SIGTERM handler만 등록하면 graceful shutdown은 좋아질 수 있지만, zombie reap 문제까지 자동 해결되지는 않는다.

9.6 네트워크가 이상함 — Namespace와 port publish를 나눠 본다

섹션 제목: “9.6 네트워크가 이상함 — Namespace와 port publish를 나눠 본다”

증상: 컨테이너 안에서는 서버가 3000 포트에서 뜬 것 같은데 host에서 접근이 안 된다. 또는 host port가 이미 사용 중이라는 에러가 난다.

원리: 컨테이너 내부 포트와 host publish 포트는 다르다. Network Namespace 안의 localhost:3000은 host의 localhost:3000이 아니다. 외부 접근에는 bridge, veth, NAT, port publish 설정이 필요하다.

판단 기준:

  • 앱이 컨테이너 안에서 127.0.0.1에만 bind하면 같은 컨테이너 안에서는 접근되지만 외부 publish가 기대대로 동작하지 않을 수 있다.
  • -p 8080:3000에서 충돌하는 것은 컨테이너 내부 3000이 아니라 host 8080이다.
  • Kubernetes에서는 Pod IP, Service, Ingress, NetworkPolicy를 구분해야 한다. 이 문서에서는 Network Namespace까지가 핵심이고, K8s 네트워킹은 후속 레이어에서 본다.

9.7 컨테이너 안 root가 너무 강함 — User Namespace만 믿지 않는다

섹션 제목: “9.7 컨테이너 안 root가 너무 강함 — User Namespace만 믿지 않는다”

증상: 컨테이너가 root로 실행되고, 민감한 host path가 volume으로 붙어 있다. 컨테이너 탈취 시 host 파일 변경 위험이 있다.

원리: Namespace는 중요한 경계지만 완전한 보안 샌드박스는 아니다. User Namespace, capability drop, seccomp, read-only rootfs, volume 최소화, non-root user가 함께 필요하다.

반례: “컨테이너니까 root로 실행해도 host에는 영향이 없다”는 위험한 직관이다. host 커널을 공유하고, 잘못된 volume mount나 capability가 있으면 피해 범위가 커진다.

9.8 hostname이 host와 같음 — UTS Namespace 예외인지 확인한다

섹션 제목: “9.8 hostname이 host와 같음 — UTS Namespace 예외인지 확인한다”

증상: 컨테이너 안에서 hostname을 봤을 때 host 서버명과 같거나, 서비스가 hostname 기반으로 자기 정체성을 잘못 판단한다.

원리: UTS Namespace는 hostname과 domainname view를 분리한다. 표준 컨테이너는 보통 컨테이너 ID나 Pod 이름에 가까운 hostname을 갖는다. --uts=host 같은 설정을 쓰면 host의 UTS Namespace를 공유한다.

판단 기준:

  • 일반 애플리케이션은 host UTS 공유가 필요 없는 경우가 많다.
  • hostname으로 shard나 tenant를 나누는 코드는 컨테이너 재생성, Pod 이름 변경, host UTS 공유에서 깨질 수 있다.
  • 서비스 식별은 hostname보다 명시적 환경변수, Pod metadata, service discovery를 쓰는 편이 안정적이다.

9.9 fork: Resource temporarily unavailable — pids 제한과 zombie를 함께 본다

섹션 제목: “9.9 fork: Resource temporarily unavailable — pids 제한과 zombie를 함께 본다”

증상: 새 child process를 만들지 못하고 Resource temporarily unavailable 류의 에러가 보인다. 동시에 <defunct>가 누적되어 있을 수 있다.

원리: cgroup의 pids.max에 닿았거나, 부모가 자식 종료 상태를 reap하지 않아 zombie가 process table entry를 계속 잡고 있을 수 있다. PID Namespace 안에서는 컨테이너 PID 1이 이 문제를 더 크게 만든다.

작은 수치 감각: pids.max = 256인 컨테이너에서 앱이 요청마다 외부 명령을 spawn하고, 종료 상태를 회수하지 못해 zombie가 쌓이면 실제 실행 중인 작업이 적어도 새 process 생성이 막힌다. CPU와 메모리가 충분해도 프로세스 수 제한에서 멈춘다.

결정 기준:

  • pids.currentpids.max에 가까우면 자식 프로세스 생성 패턴과 zombie를 본다.
  • child process를 자주 쓰는 앱은 PID 1 init wrapper와 exit/error handler가 필요하다.
  • 단순히 pids.max를 크게 올리면 증상은 늦춰지지만, zombie 누적 원인은 남는다.

10. 격리 강도 선택 기준 — 표준 컨테이너, gVisor, VM

섹션 제목: “10. 격리 강도 선택 기준 — 표준 컨테이너, gVisor, VM”

표준 컨테이너는 빠르고 효율적이지만 host 커널을 공유한다. 신뢰할 수 없는 코드를 실행하거나 강한 테넌트 격리가 필요한 환경에서는 더 두꺼운 경계가 필요할 수 있다.

격리 방식커널 공유시작 속도/오버헤드 감각적합한 경우
Namespace+cgroups 표준 컨테이너host 커널 공유가장 빠르고 가벼움내부 서비스, 신뢰 가능한 워크로드, 높은 밀도
gVisor 같은 sandboxed runtimesyscall 경계를 사용자 공간 커널이 중개표준 컨테이너보다 느릴 수 있음외부 코드 실행, syscall 노출을 줄이고 싶을 때
Kata Containers/MicroVM전용 게스트 커널VM에 가까운 오버헤드강한 멀티테넌시, 규정 준수, 커널 경계가 필요한 워크로드
전통 VM게스트 OS와 커널 분리가장 무거운 편커널 버전, 드라이버, 보안 경계가 핵심 요구사항일 때

오버헤드 숫자는 런타임 버전, syscall 패턴, storage/network 경로에 따라 크게 달라진다. 처음에는 “경계가 두꺼워질수록 보안 격리는 강해지지만 시작 시간과 syscall 비용을 측정해야 한다”는 trade-off로 읽고, 실제 선택은 자신의 workload에서 측정한다.

결정 질문은 단순하다.

  1. 컨테이너 탈출 시 다른 테넌트 데이터에 접근할 수 있는가?
  2. 실행할 코드가 우리 팀이 신뢰하는 코드인가, 사용자가 제출한 코드인가?
  3. host 커널 버전과 syscall 호환성을 워크로드가 강하게 요구하는가?
  4. 성능 밀도와 격리 강도 중 무엇이 더 중요한가?

내부 마이크로서비스를 ECS나 Kubernetes에 배포하는 일반 상황에서는 표준 컨테이너가 보통 충분하다. 사용자가 업로드한 코드를 실행하는 함수 빌더, 채점 서버, AI agent sandbox처럼 신뢰 경계가 강한 워크로드는 표준 컨테이너만으로 충분한지 의심해야 한다.

NestJS, ECS, Kubernetes 관점에서는 이 문서를 다음처럼 연결하면 된다.

실무 설정/현상커널 개념해석
ECS task definition memory: 512cgroup memory.maxtask/container가 쓸 수 있는 메모리 상한
ECS task definition cpu: 256CPU share/quota 계열CPU 경합과 제한 정책에 영향
Kubernetes resources.limits.memorymemory.max초과 시 OOMKilled 가능
Kubernetes resources.limits.cpucpu.maxperiod 단위 throttle 가능
docker execsetns() 모델실행 중인 컨테이너 Namespace 안으로 새 프로세스를 넣는 행위
CloudWatch Container Insightscgroup 지표 수집memory.current, CPU 사용량, throttle 같은 커널 지표를 서비스 지표로 보여줌
Fargate컨테이너 + microVM 계층AWS가 task 단위로 더 두꺼운 격리 경계를 제공

이 문서를 읽은 뒤 Docker Basics로 넘어가면 역할이 나뉜다. 이 문서는 “컨테이너가 커널에서 어떻게 격리되고 제한되는가”를 맡고, Docker 문서는 “그 실행 환경을 이미지, Dockerfile, registry, Compose로 어떻게 패키징하고 배포하는가”를 맡는다.

12. 선택 부록 — 작은 관찰 명령

섹션 제목: “12. 선택 부록 — 작은 관찰 명령”

아래 명령은 개념 확인용이다. 운영 절차로 외우기보다 출력이 어느 경계를 보여주는지 본다.

cgroups v2와 제한값 확인
Terminal window
# cgroups v2인지 확인
stat -fc %T /sys/fs/cgroup/
# cgroup2fs 이면 v2
# 현재 셸이 속한 cgroup
cat /proc/$$/cgroup
# cgroup 경로 안에서 주요 파일 확인
ls /sys/fs/cgroup/<scope> | grep -E "cpu.max|cpu.stat|memory.max|memory.events|memory.pressure"

관찰 포인트: cgroups v2에서는 한 cgroup 경로 아래에서 CPU, memory, io 관련 파일을 함께 본다.

Namespace inode 비교
Terminal window
# host init의 PID namespace
readlink /proc/1/ns/pid
# 특정 프로세스의 namespace 목록
ls -l /proc/<PID>/ns
# 두 프로세스의 pid namespace inode가 같으면 같은 PID Namespace를 공유한다.
readlink /proc/<PID_A>/ns/pid
readlink /proc/<PID_B>/ns/pid

관찰 포인트: Namespace는 이름 공간이고, /proc/<PID>/ns/*의 inode가 같은지로 공유 여부를 볼 수 있다.

unshare로 PID Namespace 감각 잡기
Terminal window
# root 권한 또는 user namespace 설정이 필요할 수 있다.
sudo unshare --pid --fork --mount-proc bash
# 새 shell 안에서
ps aux
# bash가 PID 1처럼 보인다.

관찰 포인트: host에서 보이는 PID와 새 PID Namespace 안에서 보이는 PID가 다르다. 같은 프로세스라도 어느 Namespace에서 보느냐에 따라 번호가 달라진다.

OOMKilled와 CPU throttle 단서
Terminal window
# 메모리 OOM 단서
cat /sys/fs/cgroup/<scope>/memory.events
# oom_kill 값이 증가했는지 본다.
# CPU throttle 단서
cat /sys/fs/cgroup/<scope>/cpu.stat
# nr_throttled / nr_periods 비율을 본다.

관찰 포인트: 컨테이너가 죽거나 느려졌을 때 애플리케이션 로그만 보지 말고 cgroup 이벤트와 throttle 지표를 함께 본다.

13. 다음 학습 단계와 돌아갈 문서

섹션 제목: “13. 다음 학습 단계와 돌아갈 문서”
주제왜 필요한가관련 레이어
Docker 내부 동작이미지, Dockerfile, OverlayFS, registry를 컨테이너 실행과 연결한다L5
Kubernetes Pod와 Nodekubelet, containerd, Pod sandbox, requests/limits가 cgroups로 내려가는 흐름을 본다L6
Linux 네트워킹veth pair, bridge, NAT, CNI를 더 자세히 이해한다L4/L6 보충
seccomp & AppArmorNamespace/cgroups 외에 syscall과 권한을 어떻게 줄이는지 본다L7+
메모리 관리로 돌아가기OOMKilled, RSS, V8 heap, native memory 차이가 헷갈리면 먼저 복습한다L4 선행/복습

cgroups & Namespace 복습 체크

  • Namespace는 보이는 세계를 분리하고, cgroups는 자원 사용량을 제한한다는 차이를 설명할 수 있다.
  • chroot가 파일시스템 루트만 바꾸며 PID, 네트워크, 자원 제한 문제를 해결하지 못한다는 한계를 설명할 수 있다.
  • PID, Network, Mount, User Namespace가 각각 무엇을 분리하는지 한 줄로 말할 수 있다.
  • cgroups v2에서 `cpu.max`, `memory.max`, `memory.high`가 무엇을 뜻하는지 설명할 수 있다.
  • `cpu.max = 50000 100000`이 0.5 CPU 제한이라는 수치 감각을 가진다.
  • `memory.max = 536870912`가 512MiB 제한이고, 초과 시 OOMKilled가 날 수 있음을 안다.
  • 컨테이너가 VM이 아니라 host 커널을 공유하는 프로세스 묶음이라는 점을 설명할 수 있다.
  • Docker CLI, containerd, runc, kernel 기능의 역할을 대략 구분할 수 있다.
  • 컨테이너 장애를 isolation issue, resource limit issue, image/runtime issue로 먼저 나눌 수 있다.
  • OOMKilled, memory.high throttling, CPU throttle, PID 1 zombie 문제의 관찰 신호를 말할 수 있다.
  1. Namespace는 격리, cgroups는 제한이다. Namespace는 PID/NET/MNT/USER 같은 시스템 view를 나누고, cgroups는 CPU/메모리/I/O/task 수를 제한하고 관측한다.
  2. chroot만으로는 컨테이너가 되지 않는다. 파일 경로 일부만 바꿀 뿐 PID 가시성, 네트워크, 권한, 자원 폭주를 충분히 막지 못한다.
  3. 컨테이너는 VM이 아니라 host 커널 위의 격리된 프로세스 묶음이다. 별도 커널을 부팅하지 않고 Namespace, cgroups, rootfs/mount를 조합한다.
  4. docker run --memory=512m --cpus=0.5는 커널 제한으로 내려간다. 학습용으로는 memory.max = 536870912, cpu.max = 50000 100000으로 이해할 수 있다.
  5. 장애는 축을 나눠 본다. host PID가 보이면 isolation issue, OOMKilled와 throttle은 resource limit issue, command not found나 잘못된 CMD는 image/runtime issue에 가깝다.
  6. 표준 컨테이너는 완전한 보안 샌드박스가 아니다. 신뢰할 수 없는 코드를 실행하거나 테넌트 경계가 강하면 gVisor, Kata, microVM, VM 같은 더 두꺼운 격리를 검토한다.