← all posts
DEV 2026.05.02 · 16 min read Intermediate

컨테이너는 어떻게 격리되면서도 빠른가

VM과의 근본적 차이부터 Namespace·Cgroups·OverlayFS의 내부 동작까지, Docker가 프로세스 격리를 통해 성능을 지키는 방식을 추적한다.


“컨테이너는 가벼운 VM이다”라는 말을 자주 듣는다. 하지만 이 비유는 틀렸다. VM은 하드웨어를 가상화하고, 컨테이너는 프로세스를 격리한다. 이 차이가 왜 중요한가? 그리고 Docker는 수십 개의 컴포넌트를 어떻게 조합해 격리와 성능을 동시에 달성하는가?

VM vs 컨테이너 — 추상화 수준이 다르다

VM은 하이퍼바이저 위에서 완전한 게스트 OS를 실행한다. CPU, 메모리, 디스크, 네트워크 전부를 가상화한다. 덕분에 격리는 강력하지만 대가가 있다 — 게스트 OS 자체가 수백 MB에서 수 GB를 차지하고, 부팅에 수십 초가 걸린다.

컨테이너는 다르다. 호스트 커널을 공유하고, OS 레이어 없이 애플리케이션 프로세스만 격리한다. 50개의 마이크로서비스를 VM으로 배포하면 50개의 OS가 필요하다. 컨테이너로 배포하면 OS는 하나, 애플리케이션만 50개다.

# VM: 웹 서버 하나를 위한 비용
메모리: 512MB(OS) + 50MB(앱) = 562MB
시작 시간: ~30초

# 컨테이너: 같은 웹 서버
메모리: ~50MB(앱만)
시작 시간: ~0.5초

컨테이너 내부에서 ps aux를 실행하면 자기 프로세스만 보인다. uname -r을 실행하면 호스트와 동일한 커널 버전이 출력된다. 격리된 것처럼 보이지만, 실제로는 호스트의 프로세스다.

Docker의 컴포넌트 계층 — 단일 프로그램이 아니다

많은 개발자가 docker run을 실행할 때 무슨 일이 일어나는지 모른다. Docker는 단일 프로그램이 아니라 독립적인 컴포넌트의 연쇄다.

Docker CLI
  ↓ REST API (Unix Socket)
dockerd (Docker Daemon)
  ↓ gRPC
containerd (컨테이너 런타임 관리자)
  ↓ exec
runc (OCI 런타임 구현체)

Linux Kernel (Namespace, Cgroups)

docker run nginx를 입력하면: CLI가 REST 요청을 만들어 dockerd에 전달하고, dockerd가 이미지를 준비해 containerd에 위임하고, containerd가 runc를 호출해 실제 컨테이너를 생성한다. runc는 Namespace를 생성하고 Cgroup을 설정하고 nginx 프로세스를 실행한 뒤 종료된다. nginx만 남는다.

이 계층화는 유연성을 낳는다. Kubernetes는 dockerd를 거치지 않고 containerd와 직접 통신한다. Podman은 데몬 없이 runc를 직접 호출한다.

Namespace — 무엇이 보이는가를 결정한다

컨테이너의 격리는 Linux Namespace가 만든다. Namespace는 프로세스에게 “독립적인 시스템 뷰”를 제공하는 커널 메커니즘이다. Docker는 7가지 Namespace를 조합한다.

Namespace격리 대상
PID프로세스 ID
NET네트워크 스택, 인터페이스
MNT파일시스템 마운트
UTS호스트명, 도메인명
IPC공유 메모리, 세마포어
USERUID/GID 매핑
CGROUPCgroup 뷰

PID Namespace의 결과: 컨테이너 내부에서 nginx는 PID 1이다. 호스트에서는 PID 12345다. 같은 프로세스를 다른 관점으로 보는 것이다. 컨테이너에서 호스트의 프로세스는 보이지 않는다.

Network Namespace의 결과: 각 컨테이너는 독립적인 네트워크 스택을 갖는다. 컨테이너 내부의 eth0은 호스트의 veth 인터페이스와 가상 케이블(veth pair)로 연결된다. 컨테이너가 80번 포트를 열어도 호스트의 80번 포트와 충돌하지 않는다.

Namespace 공유의 위험

--pid=host--net=host로 Namespace를 공유하면 격리가 깨진다. --pid=host로 실행된 컨테이너에서는 호스트의 PID 1을 종료하는 것도 이론적으로 가능하다. 필요 최소한의 Namespace만 공유해야 한다.

Cgroups — 얼마나 쓸 수 있는가를 결정한다

Namespace가 “무엇이 보이는가”를 제어한다면, Cgroups(Control Groups)는 “얼마나 사용할 수 있는가”를 제어한다. 제한이 없으면 컨테이너 하나가 CPU와 메모리를 독점해 다른 컨테이너와 호스트 전체를 마비시킬 수 있다.

CPU 제한에는 두 가지 방식이 있다. --cpu-shares는 상대적 우선순위다 — CPU 여유가 있으면 모두 쓸 수 있고, 경합 시에만 비율대로 나눈다. --cpus=0.5는 절대적 제한이다 — 아무리 CPU가 남아도 50% 이상 쓰지 못한다.

메모리 제한(--memory=512m)을 초과하면 OOM Killer가 발동한다. 컨테이너 내에서 가장 높은 OOM 스코어를 가진 프로세스가 SIGKILL을 받는다. --oom-kill-disable은 이 동작을 막지만, 그러면 호스트 전체가 메모리 부족에 빠질 수 있다 — 사용하지 마라.

# 0.5 CPU, 256MB 메모리 제한
docker run --cpus=0.5 --memory=256m nginx

# Cgroup 파일로 직접 확인
cat /sys/fs/cgroup/cpu/docker/<id>/cpu.cfs_quota_us
# 50000  (50ms / 100ms 주기 = 50%)

cat /sys/fs/cgroup/memory/docker/<id>/memory.limit_in_bytes
# 268435456  (256MB)

OverlayFS — 레이어를 하나로 합친다

Docker 이미지는 읽기 전용 레이어의 스택이다. FROM ubuntu가 레이어 1이고, RUN apt-get install nginx가 레이어 2다. 컨테이너를 실행하면 맨 위에 쓰기 가능한 레이어가 하나 추가된다.

이 레이어들을 하나의 파일시스템으로 통합하는 것이 OverlayFS다. 읽기는 위에서 아래로 레이어를 검색해 첫 번째 발견된 파일을 반환한다. 쓰기는 Copy-on-Write(CoW) 방식이다 — 하위 레이어의 파일을 수정하면 상위 레이어로 전체 파일을 복사한 뒤 수정한다. 원본 레이어는 불변이다.

파일 삭제도 실제 삭제가 아니다. 상위 레이어에 .wh.<filename> 형태의 whiteout 파일을 만들어 “이 파일은 없는 것으로 처리하라”고 표시한다.

CoW 성능 함정

컨테이너 내부에서 큰 파일을 수정하면 그 파일 전체가 상위 레이어로 복사된다. 100MB 파일의 1바이트를 바꿔도 100MB 복사가 발생한다. 데이터베이스 파일처럼 자주 수정되는 큰 파일은 반드시 Volume으로 마운트해야 한다.

캐시 전략도 여기서 나온다. Dockerfile에서 자주 변경되는 명령(소스 코드 복사)을 뒤로, 잘 변경되지 않는 명령(의존성 설치)을 앞으로 배치하면 중간 레이어가 캐시되어 빌드 시간이 수 분에서 수 초로 줄어든다.

트레이드오프

컨테이너가 VM보다 항상 나은 것은 아니다.

컨테이너가 유리한 경우: 같은 OS 커널을 공유해도 되는 워크로드, 빠른 스케일링이 필요한 마이크로서비스, CI/CD 파이프라인, 클라우드 네이티브 애플리케이션.

VM이 필요한 경우: 서로 다른 OS 커널이 필요한 워크로드, 멀티테넌트 환경에서 완전한 하드웨어 수준의 격리가 필요한 경우, 커널 모듈 로드가 필요한 레거시 앱.

컨테이너의 보안도 맥락이 있다. Namespace와 Cgroups만으로는 VM 수준의 격리에 미치지 못한다. User Namespace(userns-remap)를 활성화하면 컨테이너 내부의 root가 호스트에서는 일반 사용자로 매핑되어 탈출 시 피해를 줄인다. Seccomp 프로파일과 AppArmor/SELinux를 함께 쓰면 격리 수준을 높일 수 있다.

정리

  • 컨테이너는 VM이 아니라 격리된 프로세스다 — 호스트 커널을 공유하고, 그래서 빠르다.
  • Docker는 단일 프로그램이 아니라 dockerd → containerd → runc → 커널의 계층이다.
  • Namespace가 “무엇이 보이는가”를, Cgroups가 “얼마나 쓸 수 있는가”를, OverlayFS가 “파일시스템을 어떻게 합치는가”를 각각 담당한다.
  • CoW는 효율적이지만 큰 파일 수정에 숨겨진 비용이 있다 — 데이터는 Volume으로 분리하라.

다음 글에서는 Docker 네트워킹의 내부를 추적한다 — bridge, overlay, host 모드가 각각 어떤 Namespace와 iptables 규칙으로 구현되는지, 그리고 컨테이너 간 통신이 어떤 경로로 흐르는지.