"Docker 컨테이너가 가볍다고 하는데, 정확히 무엇이 가볍고 무엇을 포기한 걸까?"

컨테이너를 처음 접하면 "가벼운 VM" 정도로 이해하게 됩니다. 하지만 공부하다 보니, 컨테이너는 VM과 근본적으로 다른 개념이더라고요. 컨테이너는 VM이 아니라, 네임스페이스와 cgroup으로 격리된 프로세스입니다.

VM vs 컨테이너 — 근본적인 차이

가상 머신 (VM)

PLAINTEXT
┌─────────────────────────────────┐
│           App A    │   App B    │
│         Guest OS   │  Guest OS  │
│        (커널 포함)  │ (커널 포함) │
├─────────────────────────────────┤
│         하이퍼바이저 (VMM)       │
├─────────────────────────────────┤
│           호스트 OS + 커널       │
├─────────────────────────────────┤
│             하드웨어             │
└─────────────────────────────────┘
  • 각 VM은 ** 별도의 커널 **을 포함한 전체 OS를 실행
  • 하이퍼바이저가 하드웨어를 가상화
  • 부팅에 수십 초, 메모리 GB 단위

컨테이너

PLAINTEXT
┌─────────────────────────────────┐
│   App A    │   App B   │ App C  │
│  (격리됨)   │  (격리됨)  │(격리됨)│
├─────────────────────────────────┤
│  컨테이너 런타임 (Docker/runc)   │
├─────────────────────────────────┤
│     호스트 OS + 커널 (공유!)     │
├─────────────────────────────────┤
│             하드웨어             │
└─────────────────────────────────┘
  • 모든 컨테이너가 ** 호스트 커널을 공유**
  • 네임스페이스로 격리, cgroup으로 자원 제한
  • 시작에 밀리초 단위, 메모리 MB 단위

** 핵심 차이 **: VM은 하드웨어를 가상화하고, 컨테이너는 OS를 가상화합니다.

네임스페이스 — 격리의 핵심

네임스페이스는 프로세스가 볼 수 있는 시스템 자원의 범위를 제한합니다. 리눅스는 6가지 주요 네임스페이스를 제공합니다.

1. mnt (Mount Namespace)

파일 시스템 마운트 포인트를 격리합니다.

BASH
# 컨테이너는 자기만의 루트 파일시스템을 가짐
# 호스트의 / 와 컨테이너의 / 는 완전히 다른 파일시스템
$ docker exec my-container ls /
bin  dev  etc  home  lib  proc  root  sys  tmp  usr  var
  • 컨테이너마다 독립적인 파일 시스템 트리
  • 호스트의 디렉토리를 볼 수 없음 (명시적 마운트 제외)

2. UTS (Unix Timesharing System Namespace)

호스트명과 도메인명을 격리합니다.

BASH
# 호스트
$ hostname
my-server

# 컨테이너 내부
$ docker exec my-container hostname
a1b2c3d4e5f6  # 컨테이너 ID가 호스트명

3. IPC (Inter-Process Communication Namespace)

공유 메모리, 세마포어, 메시지 큐 등 IPC 자원을 격리합니다.

  • 같은 IPC 네임스페이스 안의 프로세스끼리만 통신 가능
  • 다른 컨테이너의 공유 메모리에 접근 불가

4. PID (Process ID Namespace)

프로세스 ID 공간을 격리합니다.

BASH
# 호스트에서 보면
$ ps aux | grep nginx
root  12345  nginx: master process  # 호스트 PID

# 컨테이너 안에서 보면
$ docker exec my-nginx ps aux
PID  USER  COMMAND
  1  root  nginx: master process   # 컨테이너 내 PID 1
  8  nginx nginx: worker process
  • 컨테이너 내부의 첫 번째 프로세스는 항상 PID 1
  • 호스트에서는 전혀 다른 PID로 보임
  • 컨테이너 안에서는 다른 컨테이너의 프로세스가 보이지 않음

5. net (Network Namespace)

네트워크 스택 전체를 격리합니다.

BASH
# 각 컨테이너는 독립적인 네트워크 인터페이스를 가짐
$ docker exec my-container ip addr
1: lo: <LOOPBACK,UP> ...
2: eth0@if10: <BROADCAST,UP> ...  # 가상 이더넷 (veth pair)
  • 독립적인 IP 주소, 라우팅 테이블, iptables 규칙
  • veth pair로 호스트 네트워크와 연결
  • 포트 매핑(-p 8080:80)의 원리

6. user (User Namespace)

UID/GID를 격리합니다.

BASH
# 컨테이너 안에서 root(UID 0)로 보이지만
# 호스트에서는 일반 사용자(UID 65534 등)로 매핑
  • 컨테이너 안의 root ≠ 호스트의 root
  • 보안상 중요: 컨테이너 탈출(escape) 시에도 호스트 root 권한을 갖지 못함
  • rootless 컨테이너의 기반 기술

cgroup — 자원 제한의 핵심

네임스페이스가 "무엇을 볼 수 있는지"를 제어한다면, cgroup(Control Groups)은 "얼마나 쓸 수 있는지"를 제어합니다.

cgroup으로 제한하는 자원

자원설명예시
CPUCPU 시간 할당--cpus=2 (2코어 제한)
** 메모리**메모리 사용량 제한--memory=512m (512MB 제한)
I/O디스크 읽기/쓰기 대역폭--device-read-bps=/dev/sda:100mb
** 네트워크**네트워크 대역폭 (tc 연동)간접적으로 제어
PID 수생성 가능한 프로세스 수--pids-limit=100

cgroup v1 → v2 변화

cgroup v1 (레거시):

PLAINTEXT
/sys/fs/cgroup/
├── cpu/          # CPU 컨트롤러 (별도 계층)
│   └── docker/
│       └── <container-id>/
├── memory/       # 메모리 컨트롤러 (별도 계층)
│   └── docker/
│       └── <container-id>/
└── blkio/        # IO 컨트롤러 (별도 계층)
  • 컨트롤러마다 ** 별도의 계층 트리**
  • 한 프로세스가 서로 다른 계층에서 다른 그룹에 속할 수 있어 관리가 복잡

cgroup v2 (현재 대부분의 최신 리눅스 배포판에서 기본값):

PLAINTEXT
/sys/fs/cgroup/
└── docker/
    └── <container-id>/
        ├── cgroup.controllers   # 사용 가능한 컨트롤러 목록
        ├── cpu.max              # CPU 제한
        ├── memory.max           # 메모리 제한
        └── io.max               # IO 제한
  • ** 단일 통합 계층 구조** — 모든 컨트롤러가 하나의 트리
  • 관리가 단순하고 일관적
  • 압력 감시(PSI, Pressure Stall Information) 지원 — 자원 부족 상태를 정량적으로 측정 가능

Docker가 컨테이너를 만드는 과정

docker run 명령 뒤에서 일어나는 일을 단계별로 보면 컨테이너의 본질이 명확해집니다.

전체 흐름

PLAINTEXT
docker run nginx


Docker 데몬 (dockerd)


containerd (컨테이너 관리)


runc (OCI 런타임)

    ├─ 1. clone() 시스템 콜 with 네임스페이스 플래그
    │     CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | ...
    │     → 새로운 네임스페이스를 가진 프로세스 생성

    ├─ 2. /sys/fs/cgroup 디렉토리 생성
    │     → cgroup 트리에 새 그룹 생성

    ├─ 3. 자원 제한 쓰기
    │     echo "512M" > /sys/fs/cgroup/docker/<id>/memory.max
    │     echo "200000 100000" > /sys/fs/cgroup/docker/<id>/cpu.max

    ├─ 4. 프로세스를 cgroup에 할당
    │     echo <pid> > /sys/fs/cgroup/docker/<id>/cgroup.procs

    └─ 5. pivot_root()로 루트 파일시스템 변경
          → 컨테이너 이미지를 루트로 마운트

clone() 시스템 콜이 핵심

C
// runc가 내부적으로 실행하는 것의 단순화
int flags = CLONE_NEWPID   // 새 PID 네임스페이스
          | CLONE_NEWNS    // 새 Mount 네임스페이스
          | CLONE_NEWNET   // 새 Network 네임스페이스
          | CLONE_NEWUTS   // 새 UTS 네임스페이스
          | CLONE_NEWIPC   // 새 IPC 네임스페이스
          | CLONE_NEWUSER; // 새 User 네임스페이스

pid_t child = clone(container_main, stack, flags, args);

clone()fork()의 확장판으로, 플래그를 통해 어떤 네임스페이스를 새로 만들지 지정합니다. ** 컨테이너 생성은 결국 특별한 플래그를 가진 프로세스 생성 **입니다.

보안 관점 — 격리의 한계

컨테이너는 빠르고 가볍지만, VM만큼의 격리 수준을 제공하지는 않습니다.

호스트 커널 공유의 위험

PLAINTEXT
VM:
  App → Guest 커널 → 하이퍼바이저 → Host 커널
  (커널 취약점이 Guest에만 영향)

컨테이너:
  App → Host 커널 (직접!)
  (커널 취약점이 모든 컨테이너에 영향)
  • 커널 취약점 하나로 모든 컨테이너의 격리가 무너질 수 있음
  • --privileged 플래그는 거의 모든 격리를 해제 → 프로덕션에서 금지

보안 강화 레이어

기술설명
seccomp사용할 수 있는 시스템 콜을 화이트리스트로 제한
AppArmor/SELinux파일 접근, 네트워크 등에 대한 강제 접근 제어
rootless 컨테이너user 네임스페이스로 호스트 root 권한 없이 실행
gVisor유저스페이스 커널로 시스템 콜을 중간에서 필터링
Kata Containers경량 VM 안에서 컨테이너 실행 (VM 수준 격리)

실전에서 자주 보는 실수

BASH
# 절대 하면 안 되는 것
docker run --privileged my-app          # 거의 모든 격리 해제
docker run -v /:/host my-app            # 호스트 전체 파일시스템 노출
docker run --pid=host my-app            # 호스트 프로세스 전체 노출

# 권장 사항
docker run --read-only my-app           # 읽기 전용 파일시스템
docker run --security-opt=no-new-privileges my-app  # 권한 상승 방지
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-app  # 최소 권한

컨테이너 오버헤드 — 정말 제로일까?

컨테이너는 "베어메탈 성능"이라고 하지만, 완전히 제로 오버헤드는 아닙니다.

항목오버헤드설명
CPU~0%네이티브 실행과 거의 동일
** 메모리**~수 MB네임스페이스 메타데이터
** 네트워크**~1-3%veth pair + NAT + iptables
** 디스크 I/O**~1-5%overlay 파일시스템 오버헤드
** 시작 시간**100ms수 초이미지 크기와 init 프로세스에 의존

네트워크와 디스크에서 약간의 오버헤드가 있지만, VM의 오버헤드(CPU 가상화, 메모리 중복)에 비하면 미미합니다.

정리

  • ** 컨테이너는 VM이 아닙니다** — 호스트 커널을 공유하는 격리된 프로세스입니다
  • ** 네임스페이스 6종 **: mnt(파일시스템), uts(호스트명), ipc(프로세스 통신), pid(프로세스 ID), net(네트워크), user(UID 매핑) → "무엇을 볼 수 있는지" 제어
  • cgroup: CPU, 메모리, I/O 등 자원 사용량 제한 → "얼마나 쓸 수 있는지" 제어
  • **Docker의 핵심 **: runc가 clone()에 네임스페이스 플래그를 전달하고, /sys/fs/cgroup에 제한값을 기록
  • cgroup v2 가 현재 대부분의 최신 배포판에서 기본값 — 통합 계층 구조로 관리 단순화
  • 보안 격리는 VM보다 약함 — 커널 공유가 근본 원인, seccomp/AppArmor로 보강

공부하면서 "컨테이너 = 가벼운 VM"이라는 비유가 오히려 이해를 방해한다고 느꼈습니다. **네임스페이스로 격리하고 cgroup으로 제한한 프로세스 **, 이 한 문장이 컨테이너의 본질입니다.

댓글 로딩 중...