cgroups & Namespace
분류: Layer 4 - CS 기초 — 운영체제 & Linux
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”Linux Namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 격리하고, cgroups(Control Groups)는 그 프로세스가 사용할 수 있는 자원의 양을 제한하는 커널 기능이다. 컨테이너는 이 둘의 조합이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”프론트엔드 → 플랫폼 브릿지
프론트엔드 개발에서 <iframe sandbox> 속성을 떠올려보자. sandbox 속성은 iframe 내의 스크립트가 부모 페이지의 DOM에 접근하지 못하게 하고, 특정 origin으로만 네트워크 요청을 허용하며, 별도의 스토리지 컨텍스트를 부여한다. 브라우저가 iframe을 격리하는 방식과 Linux 커널이 컨테이너를 격리하는 방식은 놀랍도록 유사하다.
| iframe sandbox | Linux 컨테이너 |
|---|---|
| DOM 접근 차단 | PID Namespace (다른 프로세스 안 보임) |
| 별도 origin 네트워크 | Network Namespace (독립 IP 스택) |
| 별도 스토리지 | Mount Namespace (독립 파일시스템 뷰) |
| JS 실행 CPU 제한 없음 | cgroups CPU 쿼터 (CPU 사용량 제한) |
Docker를 매일 사용하지만 docker run --memory 512m이 실제로 어떻게 동작하는지, 왜 컨테이너 안에서 ps aux를 하면 host의 프로세스가 안 보이는지 설명하기 어렵다면 — 이 문서가 그 답이다.
실무적 이유: ECS 태스크 정의의 memory, K8s Pod의 resources.limits가 실제로 커널 수준에서 어떻게 강제되는지 이해해야 OOM Kill 트러블슈팅, 적절한 자원 할당, 성능 튜닝이 가능하다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”3.1 Linux Namespace — “각자의 세계”를 만드는 기법
섹션 제목: “3.1 Linux Namespace — “각자의 세계”를 만드는 기법”비유: 오피스텔 건물을 상상해보자. 건물(Host OS)은 하나지만 각 호실(컨테이너)은 독립된 공간이다. 101호 입주자는 102호의 냉장고(파일시스템)를 볼 수 없고, 101호 인터폰(네트워크)은 102호 인터폰과 독립적이다. Namespace는 이 “각 호실의 벽”을 만드는 기술이다.
원리: Namespace는 Linux 커널이 제공하는 기능으로, 같은 Host에서 실행 중인 프로세스들이 서로 다른 “뷰”의 시스템 자원을 보도록 만든다. clone(), unshare(), setns() 세 가지 시스템 콜로 조작한다.
Linux 6.x 기준으로 7종의 Namespace가 존재한다:
| Namespace | 격리 대상 | 컨테이너에서의 역할 |
|---|---|---|
| PID | 프로세스 ID 번호 공간 | 컨테이너 안 PID 1 = 실제로는 host의 PID 수천 |
| NET | 네트워크 스택 전체 | 컨테이너마다 독립 IP, 라우팅 테이블, 소켓 |
| MNT | 파일시스템 마운트 뷰 | 컨테이너는 자기 rootfs만 보임 |
| UTS | 호스트명·도메인명 | 컨테이너마다 다른 hostname 설정 가능 |
| IPC | System V IPC (세마포어, 공유메모리) | 컨테이너간 IPC 자원 충돌 방지 |
| USER | 사용자/그룹 ID 매핑 | 컨테이너 내 root → host에서는 일반 유저 |
| CGROUP | cgroup 루트 디렉토리 뷰 | 컨테이너가 자신의 cgroup 제한만 보도록 |
PID Namespace 동작 원리:
# host에서 확인: nginx 컨테이너의 실제 PIDdocker run -d --name mynginx nginxdocker inspect mynginx | grep '"Pid"'# 예상 출력:# "Pid": 12847, ← host에서 본 실제 PID
# 컨테이너 안에서 확인: PID Namespace로 격리됨docker exec mynginx ps aux# 예상 출력:# USER PID %CPU %MEM COMMAND# root 1 0.0 0.1 nginx: master process nginx ← 컨테이너 안에서는 PID 1# nginx 7 0.0 0.1 nginx: worker process# ※ host의 다른 프로세스(PID 12848 등)는 전혀 보이지 않음NET Namespace 동작 원리:
# host에서: 컨테이너는 veth(가상 이더넷) 인터페이스 쌍으로 host와 연결됨ip link show# 예상 출력:# 1: lo: <LOOPBACK,UP,LOWER_UP># 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP># 5: veth3a1b2c@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> ← 컨테이너와 연결된 veth
# 컨테이너 안에서: 자기 만의 네트워크 스택docker exec mynginx ip addr# 예상 출력:# 1: lo: <LOOPBACK,UP,LOWER_UP> inet 127.0.0.1/8# 4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> inet 172.17.0.2/16 ← 컨테이너 전용 IPNamespace 확인 명령:
# 현재 프로세스의 namespace 확인 (/proc/<PID>/ns 디렉토리)ls -la /proc/1/ns# 예상 출력:# lrwxrwxrwx 1 root root 0 Apr 9 10:00 cgroup -> 'cgroup:[4026531835]'# lrwxrwxrwx 1 root root 0 Apr 9 10:00 ipc -> 'ipc:[4026531839]'# lrwxrwxrwx 1 root root 0 Apr 9 10:00 mnt -> 'mnt:[4026531840]'# lrwxrwxrwx 1 root root 0 Apr 9 10:00 net -> 'net:[4026531992]'# lrwxrwxrwx 1 root root 0 Apr 9 10:00 pid -> 'pid:[4026531836]'# lrwxrwxrwx 1 root root 0 Apr 9 10:00 user -> 'user:[4026531837]'# lrwxrwxrwx 1 root root 0 Apr 9 10:00 uts -> 'uts:[4026531838]'# ※ 숫자 [4026531836]은 namespace의 inode 번호. 두 프로세스가 같은 번호면 같은 namespace
# host 프로세스와 컨테이너 프로세스의 PID namespace 번호 비교readlink /proc/1/ns/pid # host init의 PID namespacereadlink /proc/12847/ns/pid # 컨테이너 프로세스의 PID namespace# 출력이 다르면 → 서로 다른 PID namespace (격리됨)3.2 cgroups v2 — 자원 사용량을 제어하는 기법
섹션 제목: “3.2 cgroups v2 — 자원 사용량을 제어하는 기법”비유: 오피스텔 비유를 이어서, 건물 관리자(cgroups)가 각 호실에 “전기(CPU)는 월 200kWh까지, 수도(메모리)는 월 10톤까지” 제한을 걸 수 있다. 제한을 초과하면 차단기가 내려가거나(OOM Kill) 속도를 줄인다(CPU throttle).
원리: cgroups(Control Groups)는 프로세스 그룹의 자원 사용량을 제한·모니터링·집계하는 커널 기능이다. cgroups v2(Linux 4.15+에서 안정화, 5.x부터 기본값)는 단일 계층 구조(/sys/fs/cgroup)로 모든 컨트롤러를 통합했다.
cgroups v2 주요 컨트롤러:
| 컨트롤러 | 제어 대상 | 핵심 파일 |
|---|---|---|
| cpu | CPU 사용 가중치·쿼터 | cpu.weight, cpu.max |
| memory | 메모리 소프트/하드 제한 | memory.high, memory.max |
| blkio (io) | 블록 I/O 대역폭·IOPS | io.max, io.weight |
실제 cgroup 파일시스템 확인:
# cgroups v2 마운트 확인mount | grep cgroup# 예상 출력:# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# cgroup 최상위 디렉토리 구조ls /sys/fs/cgroup/# 예상 출력:# cgroup.controllers cgroup.procs cgroup.subtree_control# cpu.pressure io.pressure memory.pressure# memory.stat system.slice user.slice# docker/ ← Docker 컨테이너의 cgroup이 여기에 생성됨
# 실행 중인 Docker 컨테이너의 cgroup 경로 확인cat /proc/12847/cgroup# 예상 출력:# 0::/system.slice/docker-abc123def456.scope# ※ cgroups v2에서는 단일 계층 구조 (0:: 접두사)CPU 제한 (cpu.max): 쿼터 주기 형식. 예를 들어 50000 100000은 100ms마다 50ms만 사용 가능 → CPU 50% 제한.
# 컨테이너 CPU 제한 확인 (docker --cpus=0.5 설정 시)cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.max# 예상 출력:# 50000 100000# ← 100ms 주기당 50ms CPU 사용 가능 (50% 제한)메모리 제한 (memory.high / memory.max):
# docker --memory=512m 설정 시cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max# 예상 출력:# 536870912 ← 512 * 1024 * 1024 = 536,870,912 bytes
# memory.high: soft limit (초과 시 reclaim 시도, 속도 저하)cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.high# 예상 출력:# max ← --memory-reservation 설정 없으면 max (제한 없음)
# 현재 메모리 사용량 실시간 확인cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.current# 예상 출력:# 134217728 ← 현재 128MB 사용 중memory.high vs memory.max 선택 기준:
두 파일은 제한 강도가 다르며, 운영 목적에 따라 다르게 설정한다.
| 파일 | 제한 강도 | 스로틀링 | OOM 발생 | 초과 허용 | 용도 |
|---|---|---|---|---|---|
memory.high | 소프트 | 발생 | 없음 | 일시 가능 | 조기 경보, 메모리 압력 감지 |
memory.max | 하드 | 없음 | 발생 | 불가 | 절대 한계, OOM Kill로 강제 종료 |
실무 패턴: memory.high를 memory.max의 **80~90%**로 설정하면, 프로세스가 한계에 도달하기 전에 스로틀링으로 조기 경보를 받을 수 있다. 예를 들어 memory.max = 512MiB일 때 memory.high = 410MiB(~80%)로 설정하면 OOM Kill 없이 먼저 속도 저하 신호를 얻을 수 있다. K8s에서는 KEP-2570(Memory QoS)에서 memory.high = limits.memory × 0.9를 권장 기준으로 제시하고 있다. 단, memory.high 초과 상태가 지속되면 프로세스가 크게 느려지므로, 스로틀링 지속 시 memory.max 값 자체를 상향 조정해야 한다.
3.2.1 cgroups v1 → v2 마이그레이션: 핵심 차이
섹션 제목: “3.2.1 cgroups v1 → v2 마이그레이션: 핵심 차이”cgroups v1은 컨트롤러별로 독립적인 계층(hierarchy)을 가졌다. 하나의 프로세스가 cpu 계층과 memory 계층에 각각 다른 경로로 속할 수 있어 구조가 복잡했다.
# cgroups v1 구조 (여러 계층, 파편화)/sys/fs/cgroup/ memory/ ← memory 컨트롤러 전용 계층 docker/ <container_id>/memory.limit_in_bytes cpu/ ← cpu 컨트롤러 전용 계층 docker/ <container_id>/cpu.cfs_quota_us blkio/ ← I/O 컨트롤러 전용 계층 docker/ <container_id>/blkio.throttle.read_bps_device
# cgroups v2 구조 (단일 통합 트리)/sys/fs/cgroup/ system.slice/ docker-<id>.scope/ memory.max ← 모든 컨트롤러가 같은 경로에 존재 cpu.max io.max| 비교 항목 | cgroups v1 | cgroups v2 |
|---|---|---|
| 계층 구조 | 컨트롤러별 다중 계층 | 단일 통합 트리 |
| 프로세스 위치 | 컨트롤러마다 다른 cgroup 가능 | 항상 하나의 cgroup에만 속함 |
| 메모리 파일명 | memory.limit_in_bytes | memory.max |
| CPU 쿼터 파일명 | cpu.cfs_quota_us / period_us | cpu.max (하나의 파일에 통합) |
| rootless 지원 | 제한적 | 완전 지원 |
| K8s 지원 | 1.31부터 maintenance mode | 1.25(GA), 현재 기본값 |
마이그레이션 시 실무 주의점:
- 파일명 변경: v1의
memory.limit_in_bytes→ v2의memory.max,cpu.cfs_quota_us+cpu.cfs_period_us→cpu.max(하나의 파일에quota period형식으로 통합). - cgroup driver 일치: kubelet과 Docker 모두
--cgroup-driver=systemd로 설정해야 한다. v1에서cgroupfs드라이버를 쓰던 환경은 systemd 드라이버로 전환 필요. - 자동 감지: K8s kubelet은 OS의 cgroup 버전을 자동 감지하므로 별도 kubelet 설정 변경 없이 동작한다.
# 현재 시스템의 cgroup 버전 확인stat -fc %T /sys/fs/cgroup/# 출력이 "cgroup2fs" → v2, "tmpfs" → v1 또는 hybrid출처: Kubernetes cgroup v2 공식 문서, K8s 1.31 cgroup v1 Maintenance Mode
3.3 컨테이너 = Namespace + cgroups 조합
섹션 제목: “3.3 컨테이너 = Namespace + cgroups 조합”컨테이너는 별도의 OS나 커널이 아니다. Host 커널을 공유하되, Namespace로 “격리된 뷰”를 제공하고, cgroups로 “자원 사용량을 제한”한 프로세스 그룹이다.
Host Linux 커널 (하나)├── Namespace: 각 컨테이너마다 격리된 시스템 뷰│ ├── PID Namespace → 컨테이너 안에서 PID 1 = 유일한 프로세스처럼 보임│ ├── NET Namespace → 컨테이너 전용 IP, 포트, 라우팅 테이블│ ├── MNT Namespace → 컨테이너 전용 파일시스템 (overlayfs)│ └── UTS Namespace → 컨테이너 전용 hostname│└── cgroups v2: 각 컨테이너의 자원 사용량 제한 ├── cpu.max → --cpus 또는 K8s limits.cpu ├── memory.max → --memory 또는 K8s limits.memory └── io.max → 디스크 I/O 대역폭 제한VM vs 컨테이너 비교:
| VM (Virtual Machine) | 컨테이너 | |
|---|---|---|
| 커널 | 게스트 OS 전용 커널 | Host 커널 공유 |
| 격리 방식 | 하이퍼바이저 (HW 가상화) | Namespace + cgroups (커널 기능) |
| 시작 시간 | 수십 초 (OS 부팅) | 수백 ms (프로세스 시작) |
| 오버헤드 | 높음 (Guest OS 메모리) | 낮음 (커널 공유) |
| 격리 강도 | 강함 | 상대적으로 약함 (커널 취약점 공유) |
3.4 실제 docker run 시 내부에서 일어나는 일
섹션 제목: “3.4 실제 docker run 시 내부에서 일어나는 일”docker run --memory=512m --cpus=0.5 nginx 를 실행하면:
1. Docker CLI → Docker Daemon에 컨테이너 생성 요청2. Docker Daemon → containerd (컨테이너 런타임)에 위임3. containerd → runc (저수준 OCI 런타임) 실행4. runc가 커널 시스템 콜 실행: a. clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWMNT | CLONE_NEWUTS | CLONE_NEWIPC) → 새로운 Namespace 세트 생성 b. /sys/fs/cgroup/system.slice/docker-<id>.scope/ 디렉토리 생성 → cpu.max에 "50000 100000" 기록 (--cpus=0.5) → memory.max에 "536870912" 기록 (--memory=512m) c. overlayfs 마운트 (이미지 레이어 조합 → 컨테이너 rootfs) d. 컨테이너 프로세스를 새 Namespace 안에서 시작 → nginx가 PID Namespace 안에서 PID 1로 실행됨5. 커널: 이후 해당 cgroup 내 프로세스의 자원 사용량을 모니터링하고 제한 강제# docker run 직후 생성된 cgroup 경로 확인CONTAINER_ID=$(docker run -d --memory=512m --cpus=0.5 nginx)cat /proc/$(docker inspect $CONTAINER_ID --format='{{.State.Pid}}')/cgroup# 예상 출력:# 0::/system.slice/docker-<CONTAINER_ID>.scope
# 실제 cgroup 파일로 제한값 확인CG_PATH="/sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope"cat $CG_PATH/memory.max# 예상 출력: 536870912
cat $CG_PATH/cpu.max# 예상 출력: 50000 1000004. 실무 연결고리
섹션 제목: “4. 실무 연결고리”Docker --memory와 --cpus가 cgroups를 어떻게 설정하는가
섹션 제목: “Docker --memory와 --cpus가 cgroups를 어떻게 설정하는가”# --memory=1g → memory.max = 1073741824docker run -d --memory=1g --name myapp myimage
# 실제 cgroup에 기록된 값 확인docker inspect myapp --format='{{.HostConfig.Memory}}'# 예상 출력: 1073741824 ← 1GB in bytes
# cgroup 파일에서 직접 확인PID=$(docker inspect myapp --format='{{.State.Pid}}')cat /proc/$PID/cgroup# 0::/system.slice/docker-<id>.scope
# OOM Kill 발생 여부 확인cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.events# 예상 출력:# low 0# high 0# max 3 ← memory.max에 3번 도달# oom 1 ← OOM 이벤트 1번 발생# oom_kill 1 ← OOM Kill 1번 발생K8s requests/limits가 실제로 cgroups를 어떻게 설정하는가
섹션 제목: “K8s requests/limits가 실제로 cgroups를 어떻게 설정하는가”# K8s Pod 스펙resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m"K8s → containerd → runc 경로로 아래와 같이 변환된다:
| K8s 스펙 | cgroups v2 파일 | 실제 값 |
|---|---|---|
limits.memory: 512Mi | memory.max | 536870912 |
requests.memory: 256Mi | memory.low | 268435456 (soft guarantee) |
limits.cpu: 500m | cpu.max | 50000 100000 |
requests.cpu: 250m | cpu.weight | 10 (비례 가중치) |
# EKS 노드에서 Pod의 cgroup 경로 확인# (kubelet은 /sys/fs/cgroup/kubepods/ 아래에 Pod별 cgroup 생성)ls /sys/fs/cgroup/kubepods/burstable/pod<POD_UID>/# 예상 출력:# cgroup.procs cpu.max cpu.weight memory.high memory.low memory.maxunshare로 직접 Namespace 생성해보기 (실습)
섹션 제목: “unshare로 직접 Namespace 생성해보기 (실습)”# 새로운 PID, UTS, Mount Namespace로 bash 실행# ※ root 권한 또는 user namespace 지원 필요sudo unshare --pid --fork --mount-proc bash
# 실행 후 새 bash 안에서:ps aux# 예상 출력:# USER PID %CPU %MEM COMMAND# root 1 0.0 0.1 bash ← 이 bash가 PID 1! (PID Namespace 격리됨)# root 2 0.0 0.0 ps aux
hostname mycontainerhostname# 예상 출력: localhost ← UTS Namespace 격리 안 했으면 host와 동일
# host 터미널에서: 위 bash는 host에서 다른 PID로 보임ps aux | grep bash# 예상 출력:# root 23456 0.0 0.0 bash ← host에서의 실제 PID5. 내 업무에 어떻게 연결되는가
섹션 제목: “5. 내 업무에 어떻게 연결되는가”NestJS + ECS 맥락:
- ECS 태스크 정의에서
memory: 512,cpu: 256설정 → ECS는 이를 cgroups v2의memory.max,cpu.max로 변환해 강제함 - NestJS 앱이 메모리 누수로 512MB를 초과하면 → 커널 OOM Killer가 컨테이너 프로세스를 강제 종료 → ECS가 태스크를 재시작
docker exec -it <container> bash로 컨테이너에 들어가는 것 = 해당 컨테이너의 Namespace Set 안으로 진입하는 것- ECS Exec(
aws ecs execute-command)도 동일한 원리로 컨테이너의 Namespace 안에서 셸을 실행함
AWS 관점:
- EC2 인스턴스 위의 ECS: Host EC2의 Linux 커널 하나를 여러 태스크(컨테이너)가 Namespace로 격리하여 공유
- Fargate: AWS가 태스크별로 micro VM을 제공하여 더 강한 격리 (Namespace + VM 이중 격리)
- CloudWatch Container Insights의 CPU/메모리 메트릭: cgroups의
cpu.stat,memory.current를 읽어서 제공
6. 학습 체크리스트
섹션 제목: “6. 학습 체크리스트”- Linux Namespace 6종(PID/NET/MNT/UTS/IPC/USER)의 역할을 각각 한 줄로 설명할 수 있다
- cgroups v2가
/sys/fs/cgroup아래 단일 계층 구조임을 알고, 실제 파일을 확인할 수 있다 -
docker run --memory=512m이 커널 수준에서memory.max파일에 값을 쓰는 과정을 설명할 수 있다 - K8s
limits.cpu: 500m이cpu.max의50000 100000으로 변환됨을 설명할 수 있다 - VM과 컨테이너의 격리 방식 차이(하이퍼바이저 vs Namespace+cgroups)를 설명할 수 있다
-
cat /proc/<PID>/cgroup으로 프로세스가 속한 cgroup 경로를 확인할 수 있다 - OOM Kill이 발생하면
memory.events파일에서 확인할 수 있음을 안다
6.5 트러블슈팅
섹션 제목: “6.5 트러블슈팅”🔧 OOM Kill이 발생하는데 메모리가 충분해 보임
섹션 제목: “🔧 OOM Kill이 발생하는데 메모리가 충분해 보임”증상: ECS 태스크 또는 K8s Pod가 갑자기 재시작됨. CloudWatch 로그에 exit code 137 또는 OOMKilled: true 확인됨.
원인: exit code 137은 SIGKILL(신호 9)로 종료된 것. cgroups의 memory.max 제한을 초과한 프로세스를 커널 OOM Killer가 강제 종료한 것. free -m으로 Host 메모리를 보면 여유 있어 보이지만, 컨테이너 자체의 cgroup 제한이 별도로 존재함.
해결:
# 1. 실제 OOM 발생 여부 확인cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.events# oom_kill 값이 0보다 크면 OOM Kill 발생 확인
# 2. 컨테이너의 현재 메모리 제한 확인docker stats <container># 예상 출력:# CONTAINER CPU% MEM USAGE / LIMIT MEM%# myapp 45% 510MiB / 512MiB 99.6% ← 거의 한계
# 3. NestJS 앱 내 메모리 누수 확인 (heap 스냅샷)# Node.js: --inspect 플래그 후 Chrome DevTools로 heap snapshot
# 4. 단기 해결: 컨테이너 메모리 제한 상향# ECS 태스크 정의 수정: memory 512 → 1024# K8s: resources.limits.memory: 512Mi → 1Gi
# 5. K8s에서 OOM 이력 확인kubectl describe pod <pod-name># Events 섹션에서 OOMKilling 이벤트 확인🔧 memory.high 초과해도 로그가 없음 (Silent Failure)
섹션 제목: “🔧 memory.high 초과해도 로그가 없음 (Silent Failure)”증상: 컨테이너가 OOM Kill 없이 점점 느려지지만 커널 로그(dmesg)에 아무 메시지도 없음. docker stats의 MEM% 수치가 높게 유지되어도 재시작이 일어나지 않아 문제를 인식하기 어려움.
원인: memory.high를 초과하면 커널이 해당 cgroup의 프로세스를 스로틀링하고 메모리 회수(reclaim)를 강제하지만, 이 과정에서 별도 커널 로그가 생성되지 않는다(OOM Kill과 달리). 초과 횟수는 memory.events 파일의 high 카운터에만 기록된다.
감지 방법:
# 1. memory.events에서 high 카운터 직접 확인CG_PATH="/sys/fs/cgroup/system.slice/docker-<id>.scope"cat $CG_PATH/memory.events# 예상 출력:# low 0# high 14 ← memory.high를 14번 초과해 스로틀링 발생# max 0# oom 0# oom_kill 0
# 2. PSI(Pressure Stall Information)로 메모리 압박 수치 확인cat $CG_PATH/memory.pressure# 예상 출력:# some avg10=23.45 avg60=18.32 avg300=9.11 total=34521000# full avg10=5.12 avg60=3.40 avg300=1.21 total=8123000# ↑ some avg10 > 20%이면 메모리 압박이 실시간으로 심각한 상태
# 3. inotifywait로 memory.events 변경 감지 (실시간 알림)inotifywait -m $CG_PATH/memory.events -e modifyPrometheus로 모니터링:
# cAdvisor가 수집하는 메트릭 (K8s 환경)container_memory_failcnt # memory.max 도달 횟수 (OOM 직전 지표)container_memory_usage_bytes # 현재 사용량
# memory.high 초과 횟수는 cAdvisor 1.47+ 부터 지원container_memory_events_high_total # high 카운터
# PromQL 예시: high 카운터가 분당 1 이상 증가하면 알림rate(container_memory_events_high_total[1m]) > 0출처: Linux Kernel cgroup-v2 공식 문서, Netdata — Diagnosing cgroups v2 Memory Throttling
🔧 컨테이너 내부에서 host의 프로세스가 보임
섹션 제목: “🔧 컨테이너 내부에서 host의 프로세스가 보임”증상: docker exec -it <container> ps aux 실행 시 host의 프로세스들이 전부 보임. 컨테이너가 PID 1을 가지지 않고 높은 PID 번호를 가짐.
원인: PID Namespace가 제대로 설정되지 않았거나, --pid=host 옵션으로 컨테이너를 실행하여 host의 PID Namespace를 공유하고 있는 상태. 보안상 컨테이너가 host 프로세스를 볼 수 있어 위험.
해결:
# 현재 컨테이너가 host PID namespace를 공유하는지 확인docker inspect <container> | grep '"PidMode"'# 출력이 "host"이면 → host PID namespace 공유 중
# host PID namespace 공유 여부 비교# host init의 PID namespace inodereadlink /proc/1/ns/pid# 예상 출력: pid:[4026531836]
# 컨테이너 프로세스의 PID namespace inodereadlink /proc/<container_pid>/ns/pid# 출력이 host와 동일하면 → PID namespace 격리 안 됨
# 해결: --pid=host 옵션 제거하고 재시작docker run -d nginx # --pid=host 없이 실행 (기본값: PID namespace 격리)
# K8s에서는 spec.hostPID: true 제거# spec:# hostPID: false (기본값, 명시 불필요)🔧 컨테이너 CPU throttle이 과도하게 발생 — 응답 지연
섹션 제목: “🔧 컨테이너 CPU throttle이 과도하게 발생 — 응답 지연”증상: NestJS 앱의 p99 레이턴시가 급등. CPU 사용률은 50% 이하로 보이는데 응답이 느림. CloudWatch에서 CPUUtilization은 낮지만 응답 시간이 느린 상황.
원인: cgroups의 cpu.max 쿼터 제한으로 CPU throttle 발생. 예를 들어 cpu.max = 50000 100000 (50% 제한)이면 특정 100ms 구간에 CPU를 50ms 이상 사용하면 나머지 구간 동안 프로세스가 강제로 정지됨. 평균 CPU는 낮아 보여도 특정 순간에 burst가 필요한 작업(요청 처리, GC 등)에서 throttle이 걸림.
해결:
# 1. throttle 통계 확인cat /sys/fs/cgroup/system.slice/docker-<id>.scope/cpu.stat# 예상 출력:# usage_usec 45230000# user_usec 38120000# system_usec 7110000# nr_periods 1200 ← 총 CPU 스케줄링 주기 수# nr_throttled 348 ← throttle된 주기 수 (29% throttle!)# throttled_usec 34800000 ← throttle로 정지된 총 시간
# throttle 비율 계산: nr_throttled / nr_periods = 348/1200 = 29%
# 2. K8s에서 확인kubectl top pod <pod-name># CPU Throttle은 metrics-server로는 보이지 않음 → Datadog/Prometheus 필요
# 3. 해결: CPU limit 상향 또는 requests/limits 비율 조정# K8s에서 CPU throttle을 없애려면 limits.cpu를 충분히 크게 설정# 또는 limits.cpu 자체를 제거 (BestEffort → Burstable QoS 변경)resources: requests: cpu: "250m" limits: cpu: "1000m" # 이전 500m → 1000m으로 상향
# 4. Fargate 사용 시: vCPU 설정 자체를 늘려야 함🔧 컨테이너 안에서 hostname이 host와 동일하게 보임
섹션 제목: “🔧 컨테이너 안에서 hostname이 host와 동일하게 보임”증상: docker exec -it <container> hostname 결과가 host 서버명과 동일. 서비스 내부에서 hostname으로 자기 식별을 할 때 예상치 못한 동작 발생.
원인: UTS Namespace가 제대로 격리되지 않았거나 --uts=host 옵션으로 host의 UTS Namespace를 공유 중.
해결:
# UTS namespace 공유 여부 확인docker inspect <container> | grep -i uts# "UTSMode": "host" → host UTS namespace 공유 중
# 정상적인 컨테이너라면docker run -d --name test nginxdocker exec test hostname# 예상 출력: a3f2b1c4d5e6 ← 컨테이너 ID 기반 자동 생성 hostname
# hostname 직접 지정하려면docker run -d --hostname myservice nginxdocker exec myservice hostname# 예상 출력: myservice
# K8s에서는 Pod의 hostname이 Pod 이름으로 자동 설정됨# spec.hostname 으로 명시적 지정 가능7. 다음 학습 단계
섹션 제목: “7. 다음 학습 단계”| 주제 | 왜 필요한가 | 관련 레이어 |
|---|---|---|
| Docker 내부 동작 | Dockerfile, 이미지 레이어, overlayfs, 컨테이너 네트워크 이해 | L5 |
| K8s 아키텍처 | Pod/Node/kubelet이 cgroups를 어떻게 활용하는지 | L6 |
| Linux 네트워킹 | veth pair, bridge, iptables, CNI 플러그인 이해 | L4 보충 |
| seccomp & AppArmor | Namespace/cgroups 외 추가 컨테이너 보안 계층 | L7+ |
8. 추천 리소스
섹션 제목: “8. 추천 리소스”📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 About cgroup v2 — Kubernetes 공식 문서 — K8s가 cgroups v2를 어떻게 활용하는지, 마이그레이션 방법까지 설명하는 공식 가이드 (입문)
- 📖 Understanding cgroups: From v1 to v2 and Why It Matters for Kubernetes — DiveInto.com — cgroups v1과 v2의 차이, 단일 계층 구조로의 변화, K8s에서의 실제 영향을 설명 (중급)
- 📖 Container security fundamentals part 2: Isolation & namespaces — Datadog Security Labs — 6종 Namespace를 코드 레벨로 설명하고 보안 관점에서 분석 (중급)
- 📖 How Docker Containers Work Under the Hood: Namespaces and Cgroups — Atlantbh — docker run 내부 동작 흐름을 Namespace + cgroups 관점으로 단계별 설명 (입문)
- 📖 namespaces(7) — Linux man page — 모든 Namespace 타입의 공식 커널 문서, 시스템 콜 인터페이스까지 (고급)
9. 예상 출력 (실습 명령 모음)
섹션 제목: “9. 예상 출력 (실습 명령 모음)”# ─── 1. cgroups 마운트 확인 ───────────────────────────────────────mount | grep cgroup# 예상 출력:# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)# ※ cgroup (v1 없음) → 순수 v2 시스템 확인
# ─── 2. /sys/fs/cgroup 디렉토리 구조 ─────────────────────────────ls /sys/fs/cgroup/# 예상 출력:# cgroup.controllers cgroup.max.depth cgroup.procs# cgroup.subtree_control cpu.pressure io.pressure# memory.numa_stat memory.pressure memory.stat# system.slice/ user.slice/ init.scope/# docker/ ← Docker 사용 시 생성됨
# ─── 3. 현재 프로세스의 cgroup 확인 ──────────────────────────────cat /proc/1/cgroup# 예상 출력 (host):# 0::/init.scope
cat /proc/$$/cgroup# 예상 출력 (현재 셸):# 0::/user.slice/user-1000.slice/session-1.scope
# ─── 4. Docker 컨테이너의 cgroup 확인 ────────────────────────────CTNR=$(docker run -d --memory=256m --cpus=0.25 alpine sleep 3600)PID=$(docker inspect $CTNR --format='{{.State.Pid}}')
cat /proc/$PID/cgroup# 예상 출력:# 0::/system.slice/docker-<CONTAINER_ID>.scope
# ─── 5. cgroup 파일로 실제 제한값 확인 ───────────────────────────CG="/sys/fs/cgroup/system.slice/docker-${CTNR}.scope"cat $CG/memory.max# 예상 출력: 268435456 ← 256 * 1024 * 1024 bytes
cat $CG/cpu.max# 예상 출력: 25000 100000 ← 100ms당 25ms (25% CPU)
# ─── 6. unshare로 PID Namespace 직접 생성 ────────────────────────# (새 터미널에서 실행, root 권한 필요)sudo unshare --pid --fork --mount-proc bash
# unshare 안에서:ps aux# 예상 출력:# USER PID %CPU %MEM COMMAND# root 1 0.0 0.1 bash ← 이 bash가 이 Namespace의 PID 1# root 8 0.0 0.0 ps aux
echo "Host에서 이 PID는 다르게 보인다"# → 다른 터미널에서 ps aux | grep "sleep\|unshare"로 확인 시 실제 host PID 출력
# ─── 7. Namespace 목록 확인 ──────────────────────────────────────lsns# 예상 출력:# NS TYPE NPROCS PID USER COMMAND# 4026531835 cgroup 120 1 root /sbin/init# 4026531836 pid 120 1 root /sbin/init# 4026531837 user 120 1 root /sbin/init# 4026531838 uts 120 1 root /sbin/init# 4026531839 ipc 120 1 root /sbin/init# 4026531840 mnt 118 1 root /sbin/init# 4026531992 net 120 1 root /sbin/init# 4026532250 mnt 1 12847 root nginx: master process ← 컨테이너 전용 mnt ns# 4026532251 uts 1 12847 root nginx: master process# 4026532252 ipc 1 12847 root nginx: master process# 4026532253 pid 1 12847 root nginx: master process ← 컨테이너 전용 pid ns# 4026532255 net 1 12847 root nginx: master process ← 컨테이너 전용 net ns
# ─── 8. CPU throttle 통계 확인 ───────────────────────────────────cat $CG/cpu.stat# 예상 출력:# usage_usec 1230000# user_usec 980000# system_usec 250000# nr_periods 45# nr_throttled 12 ← 45 주기 중 12번 throttle (26%)# throttled_usec 120000010. 요약
섹션 제목: “10. 요약”- Namespace는 격리, cgroups는 제한 — Namespace로 컨테이너마다 독립된 PID/NET/MNT 뷰를 만들고, cgroups로 CPU·메모리·I/O 사용량의 상한을 강제한다
- 컨테이너는 커널을 공유하는 격리된 프로세스 — VM처럼 별도 OS를 부팅하는 것이 아니라 Host 커널 위에서 Namespace+cgroups로 격리된 프로세스 그룹이다
docker run --memory=512m은 커널 파일에 숫자를 쓰는 것 —/sys/fs/cgroup/.../memory.max에536870912를 기록하면 커널이 나머지를 강제한다- K8s
limits는 cgroups로 변환된다 —limits.cpu: 500m→cpu.max: 50000 100000,limits.memory: 512Mi→memory.max: 536870912 - OOM Kill과 CPU throttle은 cgroups의 직접 결과 — exit code 137, 낮은 CPU 사용률에도 높은 레이턴시는 cgroup 제한 설정 문제이며,
/sys/fs/cgroup/.../memory.events와cpu.stat에서 진단 가능하다