콘텐츠로 이동

cgroups & Namespace

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

Linux Namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 격리하고, cgroups(Control Groups)는 그 프로세스가 사용할 수 있는 자원의 양을 제한하는 커널 기능이다. 컨테이너는 이 둘의 조합이다.

프론트엔드 → 플랫폼 브릿지

프론트엔드 개발에서 <iframe sandbox> 속성을 떠올려보자. sandbox 속성은 iframe 내의 스크립트가 부모 페이지의 DOM에 접근하지 못하게 하고, 특정 origin으로만 네트워크 요청을 허용하며, 별도의 스토리지 컨텍스트를 부여한다. 브라우저가 iframe을 격리하는 방식과 Linux 커널이 컨테이너를 격리하는 방식은 놀랍도록 유사하다.

iframe sandboxLinux 컨테이너
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.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 설정 가능
IPCSystem V IPC (세마포어, 공유메모리)컨테이너간 IPC 자원 충돌 방지
USER사용자/그룹 ID 매핑컨테이너 내 root → host에서는 일반 유저
CGROUPcgroup 루트 디렉토리 뷰컨테이너가 자신의 cgroup 제한만 보도록

PID Namespace 동작 원리:

Terminal window
# host에서 확인: nginx 컨테이너의 실제 PID
docker run -d --name mynginx nginx
docker 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 동작 원리:

Terminal window
# 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 ← 컨테이너 전용 IP

Namespace 확인 명령:

Terminal window
# 현재 프로세스의 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 namespace
readlink /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 주요 컨트롤러:

컨트롤러제어 대상핵심 파일
cpuCPU 사용 가중치·쿼터cpu.weight, cpu.max
memory메모리 소프트/하드 제한memory.high, memory.max
blkio (io)블록 I/O 대역폭·IOPSio.max, io.weight

실제 cgroup 파일시스템 확인:

Terminal window
# 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% 제한.

Terminal window
# 컨테이너 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):

Terminal window
# 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.highmemory.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 값 자체를 상향 조정해야 한다.

출처: Linux Kernel cgroup-v2 공식 문서, KEP-2570 Memory QoS


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 v1cgroups v2
계층 구조컨트롤러별 다중 계층단일 통합 트리
프로세스 위치컨트롤러마다 다른 cgroup 가능항상 하나의 cgroup에만 속함
메모리 파일명memory.limit_in_bytesmemory.max
CPU 쿼터 파일명cpu.cfs_quota_us / period_uscpu.max (하나의 파일에 통합)
rootless 지원제한적완전 지원
K8s 지원1.31부터 maintenance mode1.25(GA), 현재 기본값

마이그레이션 시 실무 주의점:

  1. 파일명 변경: v1의 memory.limit_in_bytes → v2의 memory.max, cpu.cfs_quota_us + cpu.cfs_period_uscpu.max(하나의 파일에 quota period 형식으로 통합).
  2. cgroup driver 일치: kubelet과 Docker 모두 --cgroup-driver=systemd로 설정해야 한다. v1에서 cgroupfs 드라이버를 쓰던 환경은 systemd 드라이버로 전환 필요.
  3. 자동 감지: K8s kubelet은 OS의 cgroup 버전을 자동 감지하므로 별도 kubelet 설정 변경 없이 동작한다.
Terminal window
# 현재 시스템의 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 내 프로세스의 자원 사용량을 모니터링하고 제한 강제
Terminal window
# 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 100000

Docker --memory--cpus가 cgroups를 어떻게 설정하는가

섹션 제목: “Docker --memory와 --cpus가 cgroups를 어떻게 설정하는가”
Terminal window
# --memory=1g → memory.max = 1073741824
docker 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: 512Mimemory.max536870912
requests.memory: 256Mimemory.low268435456 (soft guarantee)
limits.cpu: 500mcpu.max50000 100000
requests.cpu: 250mcpu.weight10 (비례 가중치)
Terminal window
# 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.max

unshare로 직접 Namespace 생성해보기 (실습)

섹션 제목: “unshare로 직접 Namespace 생성해보기 (실습)”
Terminal window
# 새로운 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 mycontainer
hostname
# 예상 출력: localhost ← UTS Namespace 격리 안 했으면 host와 동일
# host 터미널에서: 위 bash는 host에서 다른 PID로 보임
ps aux | grep bash
# 예상 출력:
# root 23456 0.0 0.0 bash ← host에서의 실제 PID

5. 내 업무에 어떻게 연결되는가

섹션 제목: “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를 읽어서 제공
  • Linux Namespace 6종(PID/NET/MNT/UTS/IPC/USER)의 역할을 각각 한 줄로 설명할 수 있다
  • cgroups v2가 /sys/fs/cgroup 아래 단일 계층 구조임을 알고, 실제 파일을 확인할 수 있다
  • docker run --memory=512m이 커널 수준에서 memory.max 파일에 값을 쓰는 과정을 설명할 수 있다
  • K8s limits.cpu: 500mcpu.max50000 100000으로 변환됨을 설명할 수 있다
  • VM과 컨테이너의 격리 방식 차이(하이퍼바이저 vs Namespace+cgroups)를 설명할 수 있다
  • cat /proc/<PID>/cgroup으로 프로세스가 속한 cgroup 경로를 확인할 수 있다
  • OOM Kill이 발생하면 memory.events 파일에서 확인할 수 있음을 안다

🔧 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 제한이 별도로 존재함.

해결:

Terminal window
# 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 카운터에만 기록된다.

감지 방법:

Terminal window
# 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 modify

Prometheus로 모니터링:

# 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 프로세스를 볼 수 있어 위험.

해결:

Terminal window
# 현재 컨테이너가 host PID namespace를 공유하는지 확인
docker inspect <container> | grep '"PidMode"'
# 출력이 "host"이면 → host PID namespace 공유 중
# host PID namespace 공유 여부 비교
# host init의 PID namespace inode
readlink /proc/1/ns/pid
# 예상 출력: pid:[4026531836]
# 컨테이너 프로세스의 PID namespace inode
readlink /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이 걸림.

해결:

Terminal window
# 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를 공유 중.

해결:

Terminal window
# UTS namespace 공유 여부 확인
docker inspect <container> | grep -i uts
# "UTSMode": "host" → host UTS namespace 공유 중
# 정상적인 컨테이너라면
docker run -d --name test nginx
docker exec test hostname
# 예상 출력: a3f2b1c4d5e6 ← 컨테이너 ID 기반 자동 생성 hostname
# hostname 직접 지정하려면
docker run -d --hostname myservice nginx
docker exec myservice hostname
# 예상 출력: myservice
# K8s에서는 Pod의 hostname이 Pod 이름으로 자동 설정됨
# spec.hostname 으로 명시적 지정 가능
주제왜 필요한가관련 레이어
Docker 내부 동작Dockerfile, 이미지 레이어, overlayfs, 컨테이너 네트워크 이해L5
K8s 아키텍처Pod/Node/kubelet이 cgroups를 어떻게 활용하는지L6
Linux 네트워킹veth pair, bridge, iptables, CNI 플러그인 이해L4 보충
seccomp & AppArmorNamespace/cgroups 외 추가 컨테이너 보안 계층L7+
Terminal window
# ─── 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 1200000
  1. Namespace는 격리, cgroups는 제한 — Namespace로 컨테이너마다 독립된 PID/NET/MNT 뷰를 만들고, cgroups로 CPU·메모리·I/O 사용량의 상한을 강제한다
  2. 컨테이너는 커널을 공유하는 격리된 프로세스 — VM처럼 별도 OS를 부팅하는 것이 아니라 Host 커널 위에서 Namespace+cgroups로 격리된 프로세스 그룹이다
  3. docker run --memory=512m은 커널 파일에 숫자를 쓰는 것/sys/fs/cgroup/.../memory.max536870912를 기록하면 커널이 나머지를 강제한다
  4. K8s limits는 cgroups로 변환된다limits.cpu: 500mcpu.max: 50000 100000, limits.memory: 512Mimemory.max: 536870912
  5. OOM Kill과 CPU throttle은 cgroups의 직접 결과 — exit code 137, 낮은 CPU 사용률에도 높은 레이턴시는 cgroup 제한 설정 문제이며, /sys/fs/cgroup/.../memory.eventscpu.stat에서 진단 가능하다