cd ../
Misc·2026-05-13·6 min read·# entry/041

Docker 내부 구조: OS 자원 격리와 mmap 아키텍처

dockerd, containerd, runc의 3단계 구조부터 Namespace와 cgroups로 자원을 격리하는 방법, 그리고 bbolt DB가 mmap을 사용하는 이유까지 Docker 내부를 계층별로 정리합니다.

Docker는 단순한 컨테이너 관리 도구가 아닙니다. Linux 커널의 자원을 빌려 쓰고, 격리하고, 제한하는 정교한 계층 구조 위에서 동작합니다.

VM은 OS를 통째로 복사한다. Docker는 OS를 공유하면서 "내 것인 것처럼" 속인다.


1. Docker의 3단계 실행 구조

Docker는 하나의 거대한 프로그램이 아닌, 역할이 분리된 컴포넌트의 협력체입니다.

CLI (docker run ...)
    ↓ HTTP 요청
Docker Engine (dockerd)   ← 고수준 관리 (이미지, 네트워크, 볼륨)
    ↓
containerd                ← 컨테이너 생명주기 관리 (생성/실행/중지)
    ↓
runc                      ← 커널 기능으로 프로세스 격리 후 종료
컴포넌트역할특징
dockerdAPI 요청 수신, 고수준 관리항상 실행 중인 데몬
containerd컨테이너 생명주기 관리Docker에서 분리된 독립 데몬
runcLinux 커널로 프로세스 격리·실행실행 후 즉시 종료되는 경량 도구
runc는 왜 실행 후 바로 종료될까?

runc는 컨테이너 프로세스를 실행하는 데만 집중합니다. 프로세스를 생성하고 나면 containerd가 그 생명주기를 이어받아 관리합니다.


2. OS 자원을 격리하는 두 가지 핵심 기술

Docker가 VM보다 가벼운 이유는 OS를 공유하기 때문입니다. 이때 "내 것인 것처럼" 속이는 두 가지 Linux 커널 기술이 핵심입니다.

커널이 프로세스에게 "너만 이 리소스를 보고 있어"라고 말하는 기술입니다.

Namespace격리 대상효과
PID프로세스 ID컨테이너 안의 프로세스가 자신을 1번(init)이라 믿는다
NET네트워크 인터페이스각 컨테이너마다 독립적인 IP와 네트워크 인터페이스를 갖는다
MNT파일 시스템 마운트컨테이너가 자신의 루트 파일 시스템만 볼 수 있다
격리의 본질

호스트 입장에서는 일반 프로세스지만, 컨테이너 입장에서는 자신이 독립된 OS 위에서 실행되는 것처럼 보입니다.


3. Docker ↔ 호스트 통신: docker.sock

Docker Client와 Daemon은 어떻게 대화할까요?

Docker CLI
    ↓ HTTP over Unix Domain Socket
/var/run/docker.sock
    ↓
dockerd (Daemon)
Unix Domain Socket의 장점
  • 네트워크 스택을 거치지 않아 TCP보다 빠름 - 같은 호스트 내에서만 접근 가능하여 외부 노출 없음 - 파일 권한으로 접근 제어 가능
보안 주의사항
  • 이 소켓에 접근하는 것은 호스트 OS의 루트 권한을 갖는 것과 동일 - 컨테이너에 소켓을 마운트할 때는 각별히 주의 필요

4. 스토리지 레이어: Copy-on-Write

Docker 이미지는 여러 개의 읽기 전용(Read-Only) 레이어가 쌓인 구조입니다.

컨테이너 실행 시 레이어 구조

┌─────────────────────────┐
│  Writeable Layer (thin) │  ← 컨테이너 실행 시 추가되는 쓰기 레이어
├─────────────────────────┤
│  Image Layer 3 (RO)     │
│  Image Layer 2 (RO)     │  ← 이미지 레이어 (읽기 전용)
│  Image Layer 1 (RO)     │
└─────────────────────────┘
CoW(Copy-on-Write) 동작 원리

기존 파일을 수정할 때만 원본을 Writeable Layer로 복사한 뒤 수정합니다. 읽기만 한다면 원본 레이어를 그대로 참조합니다. 덕분에 같은 이미지를 기반으로 한 수백 개의 컨테이너가 이미지 레이어를 공유하면서 디스크 공간을 절약합니다.


5. mmap 아키텍처: 파일과 메모리의 직결

Docker의 네트워크 상태를 관리하는 내부 DB인 bbolt는 일반적인 read()/write() 시스템 콜 대신 mmap()을 사용합니다.

일반 파일 I/O와 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하면, 물리적으로 단 하나의 메모리 페이지만 사용하여 메모리를 절약합니다.


6. 하드웨어 결함 시 mmap의 취약점

하드웨어 결함이 개입하면 mmap의 세련된 구조가 다음과 같이 붕괴됩니다.

디스크의 특정 섹터가 자성을 잃거나 물리적으로 손상되면, 커널은 해당 데이터를 읽어올 수 없습니다.

파일 시스템 인덱스: "이 데이터는 섹터 #4821에 있다" ✓
실제 섹터 #4821:   손상됨, 읽기 불가 ✗
결과: 논리적 불일치 상태
파일 시스템은 모른다

파일 시스템의 인덱스는 여전히 데이터가 존재한다고 믿습니다. 실제로 접근하기 전까지 이 불일치를 알 수 없습니다.


7. mmap vs read(): 무엇을 선택할까?

mmap이 유리한 경우
  • bbolt, SQLite처럼 B-tree 노드를 파일 이곳저곳에서 무작위 접근하는 DB 엔진 - Docker overlay2처럼 여러 컨테이너가 같은 이미지 레이어를 공유하는 경우 - 파일 크기가 커서 전체 로딩 없이 필요한 부분만 로드해야 하는 경우
read()가 더 나은 경우
  • 로그 파일처럼 순서대로 읽는(Sequential) 패턴 - NFS 같은 네트워크 파일시스템 (mmap 지원이 불완전) - "이 시점에 정확히 데이터를 읽었다"는 타이밍이 중요한 실시간 시스템

정리

Docker의 내부 구조는 세 가지 계층으로 요약됩니다.

[격리]   Namespaces → 프로세스에게 독립된 공간을 제공
[제한]   cgroups    → 격리된 공간의 자원 사용량을 하드 리밋
[저장]   CoW + mmap → 이미지 레이어를 공유하고, DB는 복사 없이 파일을 읽는다
mmap이 Docker에서 중요한 이유

여러 컨테이너가 같은 이미지 레이어를 참조할 때, mmap 덕분에 물리적으로 단 하나의 메모리 페이지만 존재합니다. 수백 개의 컨테이너가 같은 기반 이미지를 공유해도 메모리 사용량이 선형적으로 증가하지 않는 이유입니다.