dockerd, containerd, runc의 3단계 구조부터 Namespace와 cgroups로 자원을 격리하는 방법, 그리고 bbolt DB가 mmap을 사용하는 이유까지 Docker 내부를 계층별로 정리합니다.
Docker는 단순한 컨테이너 관리 도구가 아닙니다. Linux 커널의 자원을 빌려 쓰고, 격리하고, 제한하는 정교한 계층 구조 위에서 동작합니다.
“VM은 OS를 통째로 복사한다. Docker는 OS를 공유하면서 "내 것인 것처럼" 속인다.
Docker는 하나의 거대한 프로그램이 아닌, 역할이 분리된 컴포넌트의 협력체입니다.
CLI (docker run ...)
↓ HTTP 요청
Docker Engine (dockerd) ← 고수준 관리 (이미지, 네트워크, 볼륨)
↓
containerd ← 컨테이너 생명주기 관리 (생성/실행/중지)
↓
runc ← 커널 기능으로 프로세스 격리 후 종료
| 컴포넌트 | 역할 | 특징 |
|---|---|---|
| dockerd | API 요청 수신, 고수준 관리 | 항상 실행 중인 데몬 |
| containerd | 컨테이너 생명주기 관리 | Docker에서 분리된 독립 데몬 |
| runc | Linux 커널로 프로세스 격리·실행 | 실행 후 즉시 종료되는 경량 도구 |
runc는 컨테이너 프로세스를 실행하는 데만 집중합니다. 프로세스를 생성하고 나면 containerd가 그 생명주기를 이어받아 관리합니다.
Docker가 VM보다 가벼운 이유는 OS를 공유하기 때문입니다. 이때 "내 것인 것처럼" 속이는 두 가지 Linux 커널 기술이 핵심입니다.
커널이 프로세스에게 "너만 이 리소스를 보고 있어"라고 말하는 기술입니다.
| Namespace | 격리 대상 | 효과 |
|---|---|---|
| PID | 프로세스 ID | 컨테이너 안의 프로세스가 자신을 1번(init)이라 믿는다 |
| NET | 네트워크 인터페이스 | 각 컨테이너마다 독립적인 IP와 네트워크 인터페이스를 갖는다 |
| MNT | 파일 시스템 마운트 | 컨테이너가 자신의 루트 파일 시스템만 볼 수 있다 |
호스트 입장에서는 일반 프로세스지만, 컨테이너 입장에서는 자신이 독립된 OS 위에서 실행되는 것처럼 보입니다.
Docker Client와 Daemon은 어떻게 대화할까요?
Docker CLI
↓ HTTP over Unix Domain Socket
/var/run/docker.sock
↓
dockerd (Daemon)
Docker 이미지는 여러 개의 읽기 전용(Read-Only) 레이어가 쌓인 구조입니다.
컨테이너 실행 시 레이어 구조
┌─────────────────────────┐
│ Writeable Layer (thin) │ ← 컨테이너 실행 시 추가되는 쓰기 레이어
├─────────────────────────┤
│ Image Layer 3 (RO) │
│ Image Layer 2 (RO) │ ← 이미지 레이어 (읽기 전용)
│ Image Layer 1 (RO) │
└─────────────────────────┘
기존 파일을 수정할 때만 원본을 Writeable Layer로 복사한 뒤 수정합니다. 읽기만 한다면 원본 레이어를 그대로 참조합니다. 덕분에 같은 이미지를 기반으로 한 수백 개의 컨테이너가 이미지 레이어를 공유하면서 디스크 공간을 절약합니다.
Docker의 네트워크 상태를 관리하는 내부 DB인 bbolt는 일반적인 read()/write() 시스템 콜 대신 mmap()을 사용합니다.
일반 read() 경로
디스크 → [커널 버퍼] → [유저 공간 버퍼] → 프로세스
↑ 복사 1 ↑ 복사 2
mmap() 경로
디스크 → [페이지 캐시] ← 프로세스가 직접 참조 (복사 없음)
mmap은 디스크의 특정 영역을 프로세스의 가상 주소 공간에 직접 매핑하여 복사 단계를 제거합니다.
1. 매핑(Mapping): 복사 없는 접근
파일의 데이터 블록을 메모리 주소값(예: 0x7f...)에 1:1로 대응시킵니다. 개발자 입장에서는 파일이 마치 거대한 배열처럼 느껴지고, 포인터 연산만으로 파일의 특정 지점에 접근할 수 있습니다.
2. 페이지 폴트(Page Fault): 필요한 순간 로딩
mmap()을 호출한다고 파일 전체가 즉시 RAM에 올라가지는 않습니다. 커널의 '영리한 게으름'이 작동합니다.
프로세스가 주소 0x7f1234 접근 시도
↓ 해당 데이터가 RAM에 없음
커널: Page Fault 발생
↓ 디스크에서 해당 페이지만 RAM으로 로딩
프로세스: 아무 일 없다는 듯 데이터 읽음
3. 페이지 캐시(Page Cache) 공유
mmap은 커널이 관리하는 페이지 캐시를 직접 공유합니다. 여러 프로세스가 같은 파일을 mmap하면, 물리적으로 단 하나의 메모리 페이지만 사용하여 메모리를 절약합니다.
하드웨어 결함이 개입하면 mmap의 세련된 구조가 다음과 같이 붕괴됩니다.
디스크의 특정 섹터가 자성을 잃거나 물리적으로 손상되면, 커널은 해당 데이터를 읽어올 수 없습니다.
파일 시스템 인덱스: "이 데이터는 섹터 #4821에 있다" ✓
실제 섹터 #4821: 손상됨, 읽기 불가 ✗
결과: 논리적 불일치 상태
파일 시스템의 인덱스는 여전히 데이터가 존재한다고 믿습니다. 실제로 접근하기 전까지 이 불일치를 알 수 없습니다.
Docker의 내부 구조는 세 가지 계층으로 요약됩니다.
[격리] Namespaces → 프로세스에게 독립된 공간을 제공
[제한] cgroups → 격리된 공간의 자원 사용량을 하드 리밋
[저장] CoW + mmap → 이미지 레이어를 공유하고, DB는 복사 없이 파일을 읽는다
여러 컨테이너가 같은 이미지 레이어를 참조할 때, mmap 덕분에 물리적으로 단 하나의 메모리 페이지만 존재합니다. 수백 개의 컨테이너가 같은 기반 이미지를 공유해도 메모리 사용량이 선형적으로 증가하지 않는 이유입니다.