> [!abstract] Introduction > [컨테이너](Container.md) 포스트에서 OverlayFS를 "레이어들을 하나의 파일 시스템처럼 보이게 만드는 유니언 파일 시스템"이라고 소개했습니다. 이 글에서는 한 걸음 더 들어가, OverlayFS가 **내부적으로 어떻게 동작하는지** — 읽기·쓰기·삭제 각각의 I/O 경로, Copy-up 메커니즘의 원자성 보장, Whiteout과 Opaque Directory로 삭제를 표현하는 방법, 그리고 Docker의 overlay2 드라이버가 이미지 레이어를 OverlayFS에 매핑하는 구조까지를 살펴봅니다. ## 유니언 파일 시스템이란 유니언 파일 시스템*union filesystem*이란 **여러 디렉토리 트리를 하나의 디렉토리로 합쳐서 보여주는** 파일 시스템이다. 물리적으로는 별개의 디렉토리에 저장된 파일들이, 하나의 마운트 포인트에서 단일 디렉토리 구조처럼 보인다. 이 개념은 컨테이너 이전부터 존재했다. 리눅스 LiveCD가 읽기 전용 CD-ROM 위에 RAM 기반 쓰기 레이어를 올려 "설치 없이 사용할 수 있는 데스크톱"을 제공할 때, 유니언 파일 시스템이 핵심 기술이었다. 그 역사를 간단히 살펴보면 다음과 같다. | 시기 | 기술 | 특징 | |------|------|------| | 2004 | **UnionFS** | 최초의 리눅스 유니언 파일 시스템. 스토니브룩 대학에서 개발 | | 2006 | **AUFS**[^aufs] | UnionFS의 완전 재작성. 초기 Docker의 기본 스토리지 드라이버. 그러나 코드 복잡성 때문에 리눅스 메인라인 커널에 병합되지 못함 | | 2014 | **OverlayFS**[^overlayfs] | 구조적 간결성을 목표로 설계. Linux 3.18에 메인라인 커널로 병합되어 사실상 표준이 됨 | AUFS는 기능적으로 풍부했지만, 메인라인 커널 밖의 아웃오브트리*out-of-tree* 패치로만 유지되어 커널 업데이트마다 수동으로 패치를 적용해야 하는 부담이 있었다. OverlayFS는 설계를 대폭 단순화하여 커널 메인라인에 포함되는 데 성공했고, 이후 Docker와 containerd의 기본 스토리지 드라이버로 자리잡았다. ## 네 개의 디렉토리: OverlayFS의 구성 요소 OverlayFS를 마운트하려면 네 개의 디렉토리를 지정해야 한다. ```bash mount -t overlay overlay \ -o lowerdir=/lower1:/lower2,upperdir=/upper,workdir=/work \ /merged ``` ```mermaid flowchart TB subgraph merged["merged (통합 뷰)"] direction LR FA["file_a (수정됨)"] FB["file_b"] FC["file_c 안 보임"] end subgraph upper["upperdir (읽기/쓰기)"] FA_MOD["file_a (수정본)"] WH[".wh.file_c (whiteout)"] end subgraph lower["lowerdir (읽기 전용)"] FA_ORIG["file_a (원본)"] FB_ORIG["file_b"] FC_ORIG["file_c"] end subgraph work["workdir (원자성 보장)"] TEMP["#NNN (임시 파일)"] end merged --> upper merged --> lower upper -.-> work ``` | 디렉토리 | 권한 | 역할 | |----------|------|------| | **lowerdir** | 읽기 전용 | 베이스 파일 시스템 트리. 콜론(`:`)으로 여러 레이어를 쌓을 수 있으며, 왼쪽이 상위, 오른쪽이 하위 레이어 | | **upperdir** | 읽기/쓰기 | 모든 변경(생성, 수정, 삭제)이 물리적으로 기록되는 공간 | | **workdir** | 내부 전용 | Copy-up의 원자성을 보장하기 위한 임시 작업 공간. upperdir과 같은 파일 시스템에 존재해야 함 | | **merged** | 통합 뷰 | 사용자와 애플리케이션이 보는 최종 결과. upper + lower가 합쳐진 단일 디렉토리 | 사용자가 merged 디렉토리에서 파일을 읽거나 쓰면, OverlayFS 커널 모듈이 요청의 종류에 따라 upper 또는 lower로 I/O를 라우팅한다. 이 라우팅 규칙이 OverlayFS의 핵심이다. ## I/O 연산별 동작 원리 OverlayFS에서 파일 I/O가 어떻게 처리되는지를 연산별로 살펴보자. 읽기 전용인 lowerdir의 불변성*immutability*을 유지하면서 사용자에게 완전한 읽기/쓰기 환경을 제공하는 것이 설계의 핵심 과제다. ### 읽기: 위에서 아래로 탐색 파일을 읽을 때 OverlayFS는 **upperdir부터 탐색**한다. upperdir에 파일이 있으면 그 파일을 반환하고, 없으면 lowerdir로 내려가서 찾는다. 여러 lowerdir이 쌓여 있으면 위쪽 레이어부터 순서대로 탐색한다. $ \text{merged에서 읽기 요청} \xrightarrow{\text{upperdir에 있나?}} \begin{cases} \text{있음} \rightarrow \text{upperdir에서 읽기} \\ \text{없음} \rightarrow \text{lowerdir에서 순차 탐색} \end{cases} $ upperdir에 파일이 존재하면 lowerdir에 동일한 이름의 원본이 있더라도 완전히 가려진다. 이것이 "overlay(덮어씌우기)"라는 이름의 유래다. ### 생성: 항상 upperdir에 직접 기록 새 파일을 만들 때는 lowerdir을 거치지 않고 **upperdir에 바로 생성**한다. lowerdir의 존재 여부와 무관하게 동작하므로 가장 단순한 경로다. ### 수정: Copy-up lowerdir에만 존재하는 파일을 수정하려 하면 OverlayFS는 **Copy-up**[^copy-up]을 수행한다. 원본 파일을 lowerdir에서 upperdir로 **통째로 복사**한 뒤, upperdir의 복사본에 수정을 적용하는 것이다. ```mermaid sequenceDiagram participant App as 애플리케이션 participant OVL as OverlayFS participant Upper as upperdir participant Work as workdir participant Lower as lowerdir App->>OVL: open("/merged/config.yaml", O_WRONLY) OVL->>OVL: upperdir에 config.yaml 없음 → Copy-up 필요 OVL->>Work: workdir에 임시 파일 생성 OVL->>Lower: config.yaml 데이터 + 메타데이터 읽기 OVL->>Work: 임시 파일에 데이터 복사 OVL->>Work: xattr (확장 속성) 복사 OVL->>Upper: rename()으로 원자적 이동 Note over Upper: 이제 upperdir에 config.yaml 존재 OVL->>App: 파일 디스크립터 반환 App->>Upper: write() — upperdir에 직접 쓰기 ``` Copy-up 과정에서 주목할 점은 **workdir의 역할**이다. 파일을 lowerdir에서 upperdir로 직접 복사하면, 복사 도중 시스템이 중단되었을 때 불완전한 파일이 upperdir에 남을 수 있다. 이를 방지하기 위해 OverlayFS는 먼저 workdir에 임시 파일을 생성하고, 복사가 완료된 후 `rename()`[^rename-atomic] 시스템 호출로 원자적으로 upperdir에 이동시킨다. `rename()`은 같은 파일 시스템 내에서 원자적 연산이 보장되므로, Copy-up 도중 시스템이 중단되더라도 upperdir에는 완전한 파일만 존재하게 된다. Copy-up은 **최초 1회만** 발생한다. 이후의 모든 읽기와 쓰기는 upperdir의 파일에 직접 접근하므로, 네이티브 파일 시스템과 동일한 성능을 달성한다. 다만 최초 쓰기 시 파일 전체를 복사해야 하므로, 대용량 파일의 경우 첫 번째 쓰기에 상당한 지연이 발생할 수 있다. > [!tip] metacopy: 대용량 파일의 Copy-up 최적화 > Linux 4.19부터 도입된 `metacopy=on` 마운트 옵션을 사용하면, `chmod`이나 `chown` 같은 메타데이터 변경 시 파일 데이터를 복사하지 않고 **메타데이터만 Copy-up**한다. 실제 데이터 복사는 파일에 쓰기가 발생하는 시점까지 지연된다. ### 삭제: Whiteout 파일 읽기 전용인 lowerdir의 파일은 물리적으로 삭제할 수 없다. 그렇다면 사용자가 `rm file_c`를 실행했을 때 OverlayFS는 어떻게 "삭제된 것처럼" 보이게 할까? 답은 **Whiteout**[^whiteout] 파일이다. lowerdir에 있는 파일을 삭제하면, OverlayFS는 upperdir에 `.wh.<파일명>` 형태의 특수 파일을 생성한다. 디렉토리를 탐색할 때 이 Whiteout 파일이 발견되면, lowerdir에 동일한 이름의 원본이 있어도 사용자에게 보이지 않도록 마스킹한다. ![[overlayfsWhiteout.svg]] Whiteout 파일의 구현은 전통적으로 디바이스 번호가 `0/0`인 문자 디바이스 노드로 이루어졌다. 최근 커널에서는 크기가 0인 일반 파일에 `trusted.overlay.whiteout` 확장 속성을 부여하는 대안적 방식도 지원한다. 이 방식은 특수 디바이스 노드 생성에 root 권한이 필요하지 않아, rootless 컨테이너 환경에서 유용하다. ### 디렉토리 삭제: Opaque Directory Whiteout이 개별 파일의 삭제를 표현한다면, **Opaque Directory**[^opaque-dir]는 디렉토리 전체의 삭제를 표현한다. lowerdir에 있는 디렉토리를 삭제하고 같은 이름으로 새 디렉토리를 만들면, upperdir의 새 디렉토리에 `trusted.overlay.opaque` 확장 속성이 `"y"`로 설정된다. 이 속성이 감지되면 커널은 해당 디렉토리에 대해 **lowerdir로의 탐색을 차단**하고 병합을 수행하지 않는다. ![[overlayfsOpaqueDir.svg]] Opaque Directory는 `rm -rf mydir && mkdir mydir` 같은 패턴에서 자동으로 적용된다. lowerdir의 디렉토리 안에 수천 개의 파일이 있더라도, Whiteout을 하나하나 생성하는 대신 디렉토리 하나에 속성 하나를 설정하는 것으로 충분하다. ## I/O 연산 요약 | 연산 | upperdir에 있을 때 | lowerdir에만 있을 때| |------|-------------------|-------------------| | **읽기** | upperdir에서 직접 읽기 | lowerdir에서 읽기 | | **쓰기** | upperdir에 직접 쓰기 | Copy-up 후 upperdir에 쓰기 | | **생성** | upperdir에 직접 생성 | — | | **삭제** | upperdir에서 직접 삭제 | Whiteout 생성 | | **디렉토리 삭제 + 재생성** | upperdir에서 직접 처리 | Opaque Directory 생성 | ## Docker overlay2 드라이버: 이미지 레이어와 OverlayFS의 매핑 [컨테이너](Container.md)에서 살펴본 이미지 레이어 구조가 실제로 OverlayFS에 어떻게 매핑되는지를 살펴보자. Docker는 `overlay2`[^overlay2] 스토리지 드라이버를 통해 이미지의 각 레이어를 OverlayFS의 lowerdir로 매핑한다. ### 디렉토리 구조 Docker가 이미지를 풀하면 `/var/lib/docker/overlay2/` 아래에 레이어별 디렉토리가 생성된다. ![[overlayfsDockerTree.svg]] ### 컨테이너 실행 시의 마운트 `docker run nginx`를 실행하면 Docker는 다음과 같은 OverlayFS 마운트를 수행한다. ```bash mount -t overlay overlay \ -o lowerdir=l/GHIJKL:l/ABCDEF,upperdir=ccc.../diff,workdir=ccc.../work \ ccc.../merged ``` 여기서 `l/` 디렉토리의 심볼릭 링크는 중요한 실용적 이유가 있다. `mount` 명령의 옵션 문자열에는 길이 제한(페이지 크기, 보통 4096바이트)이 있는데, 이미지 레이어가 많아지면 lowerdir 경로가 이 제한을 초과할 수 있다. 짧은 심볼릭 링크로 경로를 축약하여 이 문제를 회피한다. ### 레이어 공유의 실제 동일한 이미지를 사용하는 컨테이너 여러 개를 실행하면, 이미지 레이어(lowerdir)는 공유되고 각 컨테이너마다 별도의 upperdir만 생성된다. 이것이 [컨테이너](Container.md)에서 설명한 "레이어 공유"의 실체다. ```mermaid flowchart TB subgraph shared["공유 이미지 레이어 (lowerdir)"] L1["레이어 1: ubuntu:22.04"] L2["레이어 2: nginx 설치"] end subgraph c1["컨테이너 A"] U1["upperdir A<br/>(로그 파일, 설정 변경 등)"] end subgraph c2["컨테이너 B"] U2["upperdir B<br/>(다른 로그, 다른 설정)"] end subgraph c3["컨테이너 C"] U3["upperdir C"] end U1 --> shared U2 --> shared U3 --> shared ``` nginx 이미지가 150MB라고 하면, 컨테이너 100개를 실행해도 디스크에 저장되는 이미지 데이터는 150MB 하나뿐이다. 각 컨테이너의 upperdir에는 런타임에 변경된 파일만 저장되므로, 실질적 추가 디스크 사용량은 미미하다. ## 성능 특성 OverlayFS의 성능은 I/O 패턴에 따라 크게 달라진다. **읽기 성능**: upperdir에 있는 파일을 읽을 때는 네이티브 파일 시스템과 동일한 성능이다. lowerdir에서 읽을 때도 한 번의 추가 경로 탐색만 필요하므로 오버헤드가 매우 작다. **쓰기 성능**: Copy-up이 발생하는 최초 쓰기는 파일 크기에 비례하는 지연이 발생한다. 그러나 Copy-up 이후에는 네이티브 성능과 동일하다. 이를 "최초 1회 페널티*first-write penalty*"라 부른다. **디렉토리 탐색**: `ls`나 `readdir()` 시 OverlayFS는 upperdir과 lowerdir의 내용을 **병합**해야 한다. 중복 항목을 제거하고 Whiteout을 처리하는 과정에서 오버헤드가 발생하며, 항목이 많은 디렉토리일수록 비용이 커진다. ### 다른 스토리지 드라이버와의 비교 | | OverlayFS (overlay2) | AUFS | devicemapper | Btrfs | |---|---|---|---|---| | **커널 메인라인** | Linux 3.18+ | 아웃오브트리 | Linux 2.6+ | Linux 3.10+ | | **Copy-up 단위** | 파일 전체 | 파일 전체 | 블록 (64KB) | 블록 (4KB) | | **레이어 수 제한** | 128 (커널 제한) | 127 | 제한 없음 | 제한 없음 | | **메모리 사용** | 낮음 | 높음 (inode 캐시) | 중간 | 중간 | | **순차 읽기** | 우수 | 우수 | 우수 | 우수 | | **최초 쓰기** | 파일 크기에 비례 | 파일 크기에 비례 | 블록 크기에 비례 | 블록 크기에 비례 | | **Docker 기본** | Ubuntu, RHEL 8+ | 구 Ubuntu | RHEL 7 (구) | — | devicemapper와 Btrfs는 블록 단위로 Copy-on-Write를 수행하므로 대용량 파일의 일부만 수정할 때 유리하지만, 설정이 복잡하고 메모리 사용량이 높다. OverlayFS는 파일 단위 Copy-up이라는 제약이 있지만, 커널 메인라인 지원, 낮은 메모리 사용량, 단순한 구조 덕분에 컨테이너 환경의 사실상 표준으로 자리잡았다. ## 고급 마운트 옵션 OverlayFS의 초기 구현에는 몇 가지 POSIX 비호환 동작이 있었다. 커널 커뮤니티는 이를 해결하기 위해 다양한 마운트 옵션을 추가해왔다. ### `redirect_dir`: 디렉토리 이름 변경 기본 OverlayFS에서는 lowerdir에 있는 디렉토리의 이름을 변경(`rename()`)하면 `EXDEV` 에러가 발생한다. 디렉토리 내부의 모든 파일을 재귀적으로 Copy-up해야 하기 때문이다. `redirect_dir=on` 옵션을 사용하면, upperdir의 새 경로에 빈 디렉토리를 만들고 `trusted.overlay.redirect` 확장 속성으로 원본 lowerdir 경로를 기록하여, 실제 복사 없이 즉각적인 이름 변경을 지원한다. ### `index`: 하드 링크 보존 lowerdir에서 하드 링크로 연결된 두 파일(`file_a`, `file_b`)이 있을 때, `file_a`에 쓰기가 발생하면 Copy-up으로 새 inode가 생성되어 하드 링크가 끊어진다. `index=on` 옵션은 Copy-up 시 원본 inode의 식별자를 인덱스에 기록하여, `file_b`의 Copy-up 때 기존 상위 파일에 하드 링크를 연결함으로써 하드 링크 관계를 보존한다. ### `metacopy`: 메타데이터 전용 Copy-up 앞서 소개한 대로, `chmod`이나 `chown` 같은 메타데이터 변경 시 파일 데이터를 복사하지 않고 메타데이터만 Copy-up한다. 수 GB짜리 파일의 소유자만 변경하는 경우 극적인 성능 향상을 제공한다. --- [^aufs]: AUFS(Advanced Multi-Layered Unification Filesystem)는 Junjiro Okajima가 UnionFS를 완전히 재작성하여 2006년에 발표한 유니언 파일 시스템이다. 초기 Docker(2013년)의 우분투/데비안 환경에서 기본 스토리지 드라이버로 사용되었으나, 코드 복잡성으로 리눅스 메인라인 커널 병합이 거절되어 아웃오브트리 패치로만 유지되었다. [^overlayfs]: OverlayFS는 Miklos Szeredi(Novell/Red Hat)가 개발하여 2014년 Linux 3.18에 메인라인 커널로 병합된 유니언 파일 시스템이다. AUFS 대비 구조가 단순하여 커널 메인라인 병합에 성공했으며, `fs/overlayfs/` 디렉토리에 구현되어 있다. — `Documentation/filesystems/overlayfs.rst`. [^copy-up]: Copy-up은 lowerdir의 파일을 upperdir로 복사하는 OverlayFS의 핵심 메커니즘이다. 파일의 데이터, 메타데이터(소유자, 권한, 타임스탬프), 확장 속성(xattr)이 모두 복사된다. workdir을 활용한 원자적 `rename()`으로 중간 상태가 노출되지 않도록 보장한다. — `fs/overlayfs/copy_up.c`. [^rename-atomic]: 같은 파일 시스템 내에서의 `rename()` 시스템 호출은 POSIX 표준에 의해 원자적 연산이 보장된다. 즉, 시스템 관점에서 rename은 "이전 경로에 있다"와 "새 경로에 있다" 두 상태만 존재하며, 중간 상태는 관측되지 않는다. OverlayFS는 이 속성을 활용하여 Copy-up의 원자성을 보장한다. — POSIX.1-2017, §rename(). [^whiteout]: Whiteout은 OverlayFS에서 lowerdir 파일의 삭제를 표현하는 특수 마커 객체다. 전통적 구현은 디바이스 번호 `0/0`의 문자 디바이스 노드이며, 최신 커널에서는 `trusted.overlay.whiteout` 확장 속성을 가진 일반 파일도 지원한다. Whiteout 자체는 사용자에게 보이지 않는다. — `Documentation/filesystems/overlayfs.rst`, §Whiteouts and opaque directories. [^opaque-dir]: Opaque Directory는 upperdir의 디렉토리에 `trusted.overlay.opaque` 확장 속성을 `"y"`로 설정하여, 해당 디렉토리에 대한 lowerdir 탐색을 차단하는 메커니즘이다. 디렉토리 삭제 후 재생성 시 자동으로 적용되며, 개별 Whiteout을 대량 생성하는 것보다 효율적이다. — `Documentation/filesystems/overlayfs.rst`, §Whiteouts and opaque directories. [^overlay2]: Docker의 `overlay2` 스토리지 드라이버는 OverlayFS의 다중 lowerdir 기능(Linux 4.0+)을 활용하여, 각 이미지 레이어를 별도의 lowerdir로 매핑한다. 이전의 `overlay` 드라이버는 lowerdir을 하나만 지원하여 하드 링크로 레이어를 연결해야 했다. Docker 18.09+에서 `overlay2`가 기본이다. — Docker Documentation, 'Use the OverlayFS storage driver'. ## 출처 - Szeredi, M. Linux kernel documentation, `Documentation/filesystems/overlayfs.rst`. - Merkel, D. (2014) 'Docker: Lightweight Linux Containers for Consistent Development and Deployment', *Linux Journal*, 2014(239). - Docker Documentation, 'Use the OverlayFS storage driver' — https://docs.docker.com/storage/storagedriver/overlayfs-driver/. - Arch Linux Wiki, 'Overlay filesystem' — https://wiki.archlinux.org/title/Overlay_filesystem. - Linux kernel source v6.19 — `fs/overlayfs/`. - Silberschatz, A., Galvin, P. B., and Gagne, G. (2018) *Operating System Concepts*. 10th ed. Hoboken, NJ: Wiley.