Theme:

"컨테이너는 가상 머신이 아니다"라는 말을 들어보셨을 겁니다. 그렇다면 컨테이너는 정확히 어떻게 격리되는 걸까요? 커널을 공유하는데 어떻게 서로 영향을 주지 않을 수 있을까요?

컨테이너의 두 가지 핵심 기술

컨테이너는 두 가지 Linux 커널 기능의 조합입니다.

  • Namespace: 프로세스가 볼 수 있는 자원의 범위를 격리
  • cgroups: 프로세스가 사용할 수 있는 자원의 을 제한
PLAINTEXT
┌── 가상 머신 ────────────┐    ┌── 컨테이너 ──────────────┐
│  ┌── Guest OS ────────┐ │    │  ┌── 애플리케이션 ──────┐ │
│  │  ┌── App ────────┐ │ │    │  │                      │ │
│  │  └───────────────┘ │ │    │  └──────────────────────┘ │
│  └────────────────────┘ │    │  namespace + cgroups       │
│  하이퍼바이저             │    │  호스트 커널 공유          │
│  호스트 OS               │    │  호스트 OS               │
└─────────────────────────┘    └──────────────────────────┘

Linux Namespace

6가지(+1) 네임스페이스

네임스페이스격리하는 것플래그
PID프로세스 IDCLONE_NEWPID
Network네트워크 인터페이스, IP, 라우팅CLONE_NEWNET
Mount파일 시스템 마운트 포인트CLONE_NEWNS
UTS호스트명, 도메인명CLONE_NEWUTS
IPCSystem V IPC, POSIX 메시지 큐CLONE_NEWIPC
UserUID/GID 매핑CLONE_NEWUSER
Cgroupcgroup 루트 디렉토리 뷰CLONE_NEWCGROUP

PID Namespace

컨테이너 내부에 독립적인 프로세스 공간을 제공합니다.

BASH
# 호스트에서 보이는 프로세스
ps aux | grep nginx
# root  12345  nginx: master process
# nginx 12346  nginx: worker process

# 컨테이너 내부에서 보이는 프로세스
docker exec mynginx ps aux
# PID  USER   COMMAND
# 1    root   nginx: master process    ← PID 1
# 7    nginx  nginx: worker process

컨테이너의 메인 프로세스는 PID 1이 됩니다. 이것이 ENTRYPOINT로 지정한 프로세스이며, 이 프로세스가 종료되면 컨테이너도 종료됩니다.

BASH
# 컨테이너의 네임스페이스 확인
ls -la /proc/$(docker inspect --format '{{.State.Pid}}' mynginx)/ns/
# pid -> pid:[4026532xxx]
# net -> net:[4026532yyy]
# mnt -> mnt:[4026532zzz]
# ...

Network Namespace

각 컨테이너는 독립적인 네트워크 스택을 가집니다.

BASH
# 컨테이너의 네트워크 인터페이스
docker exec myapp ip addr
# 1: lo: <LOOPBACK,UP> inet 127.0.0.1/8
# 42: eth0@if43: <BROADCAST,UP> inet 172.17.0.2/16

# 호스트의 네트워크 인터페이스 (완전히 다름)
ip addr
# 1: lo: <LOOPBACK,UP> inet 127.0.0.1/8
# 2: eth0: <BROADCAST,UP> inet 192.168.1.10/24
# 3: docker0: <BROADCAST,UP> inet 172.17.0.1/16
# 43: vethXXX@if42: <BROADCAST,UP>

각 컨테이너는 자신만의 IP, 라우팅 테이블, iptables 규칙, 포트 공간을 가집니다. 따라서 여러 컨테이너가 같은 포트(예: 80)를 사용해도 충돌이 없습니다.

Mount Namespace

컨테이너는 독립적인 파일 시스템 뷰를 가집니다.

BASH
# 컨테이너 내부의 파일 시스템 (OverlayFS)
docker exec myapp mount | head -5
# overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
# proc on /proc type proc (rw,nosuid,nodev,noexec)
# tmpfs on /dev type tmpfs (rw,nosuid)

호스트의 파일 시스템과 완전히 분리되어 있고, 볼륨으로 명시적으로 마운트한 경로만 공유됩니다.

UTS Namespace

컨테이너가 독립적인 호스트명을 가집니다.

BASH
# 컨테이너의 호스트명
docker exec myapp hostname
# a1b2c3d4e5f6 (컨테이너 ID)

# 호스트의 호스트명
hostname
# my-server

User Namespace

컨테이너 내부의 UID/GID를 호스트의 다른 UID/GID에 매핑합니다.

PLAINTEXT
컨테이너 내부:  root (UID 0)
     ↕ 매핑
호스트:         nobody (UID 65534) 또는 다른 일반 사용자
BASH
# User namespace 매핑 확인 (rootless Docker)
cat /proc/$(docker inspect --format '{{.State.Pid}}' myapp)/uid_map
# 0     100000     65536
# 컨테이너의 UID 0 → 호스트의 UID 100000

User namespace를 사용하면 컨테이너 탈출이 발생해도 호스트에서는 일반 사용자 권한만 가지게 됩니다.

cgroups (Control Groups)

cgroups는 프로세스 그룹의 리소스 사용량을 제한, 계측, 격리합니다.

제어 가능한 리소스

서브시스템제어하는 것
cpuCPU 시간 할당
cpuset특정 CPU 코어 할당
memory메모리 사용량 제한
blkio블록 I/O 대역폭
devices디바이스 접근 제어
pids프로세스 수 제한
net_cls네트워크 트래픽 분류

Docker에서의 cgroups 적용

BASH
# 메모리 제한
docker run --memory 512m myapp

# CPU 제한
docker run --cpus 1.5 myapp

# 현재 cgroup 설정 확인
docker inspect myapp --format '{{.HostConfig.Memory}}'
# 536870912 (512MB in bytes)

# 리소스 사용량 모니터링
docker stats myapp
# CONTAINER  CPU %  MEM USAGE / LIMIT  MEM %
# myapp      0.50%  128MiB / 512MiB    25.00%

cgroups 파일 시스템

BASH
# cgroups v1: 각 서브시스템이 별도 계층
ls /sys/fs/cgroup/
# blkio/  cpu/  cpuset/  devices/  memory/  pids/  ...

# 특정 컨테이너의 메모리 제한 확인
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes

# cgroups v2: 통합된 계층 구조
ls /sys/fs/cgroup/system.slice/docker-<container-id>.scope/
# memory.max  cpu.max  pids.max  ...

cgroups v1 vs v2

특성v1v2
계층 구조서브시스템별 별도 트리단일 통합 트리
관리 복잡성높음낮음
PSI (Pressure Stall Info)XO
Rootless 컨테이너제한적완전 지원
메모리+스왑 통합 제어XO
커널 요구2.6.24+4.5+ (5.2+ 권장)
BASH
# 현재 cgroups 버전 확인
stat -fc %T /sys/fs/cgroup/
# cgroup2fs → v2
# tmpfs → v1

# 또는
mount | grep cgroup

메모리 cgroup 동작

BASH
docker run --memory 256m --memory-swap 256m myapp
PLAINTEXT
┌── 메모리 cgroup ──────────────────────┐
│  memory.max = 256MB                   │
│  memory.swap.max = 0MB (스왑 없음)    │
│                                       │
│  사용량이 256MB 초과 시:               │
│  1. 커널이 reclaim 시도               │
│  2. reclaim 실패 시 OOM Killer 작동   │
│  3. 컨테이너 프로세스 종료 (OOMKilled)  │
└───────────────────────────────────────┘
BASH
# OOMKilled 확인
docker inspect myapp --format '{{.State.OOMKilled}}'
# true

네임스페이스와 cgroups의 조합

Docker가 컨테이너를 생성할 때 내부적으로 일어나는 일을 단순화하면 다음과 같습니다.

PLAINTEXT
1. 새 네임스페이스 생성 (PID, Net, Mount, UTS, IPC, User)
2. cgroup 생성 및 리소스 제한 설정
3. 루트 파일 시스템 구성 (OverlayFS)
4. 네트워크 인터페이스 연결 (veth pair)
5. 프로세스 실행 (ENTRYPOINT/CMD)

이 과정을 직접 수행하는 도구가 runc이고, runc가 OCI 표준에 따라 컨테이너를 실행합니다.

unshare로 직접 네임스페이스 생성

BASH
# PID + Mount 네임스페이스를 만들어 새 셸 실행 (개념 이해용)
sudo unshare --pid --mount --fork /bin/bash

# 새 네임스페이스 안에서
mount -t proc proc /proc
ps aux
# PID  COMMAND
# 1    /bin/bash   ← PID 1!

이것이 컨테이너의 핵심입니다. 새로운 네임스페이스를 만들고 그 안에서 프로세스를 실행하는 것입니다.

실제 격리 수준 확인

BASH
# 컨테이너의 네임스페이스 ID 확인
docker inspect --format '{{.State.Pid}}' myapp
# 12345

ls -la /proc/12345/ns/
# ipc -> ipc:[4026532xxx]
# mnt -> mnt:[4026532yyy]
# net -> net:[4026532zzz]
# pid -> pid:[4026532aaa]
# user -> user:[4026531837]  ← 호스트와 같으면 User NS 미사용
# uts -> uts:[4026532bbb]

# 호스트의 네임스페이스 ID와 비교
ls -la /proc/1/ns/
# 다른 숫자면 격리됨, 같으면 공유

정리

  • Namespace는 프로세스가 볼 수 있는 자원의 범위를 격리합니다. PID, Network, Mount, UTS, IPC, User, Cgroup 7가지가 있습니다.
  • cgroups는 프로세스 그룹의 CPU, 메모리, I/O 등 리소스 사용량을 제한하고 계측합니다.
  • 컨테이너는 이 두 기술의 조합일 뿐, 가상 머신처럼 별도의 커널을 가지지 않습니다.
  • cgroups v2는 통합된 계층 구조, PSI 지원, rootless 컨테이너 등에서 v1보다 개선되었습니다.
  • User namespace를 활용하면 컨테이너 내부의 root가 호스트에서는 일반 사용자로 매핑되어 보안이 크게 강화됩니다.
  • 이 기본 원리를 이해하면 Docker뿐만 아니라 Podman, containerd, Kubernetes까지 컨테이너 기술 전반을 이해하는 기반이 됩니다.
댓글 로딩 중...