> [!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.