OverlayFS
OverlayFS의 핵심은 읽기 전용(Read-Only) lower 계층과 쓰기 가능한 upper 계층을 하나의 마운트(Mount)로 통합하는 것입니다. 본 문서는 lookup 병합 규칙, copy-up 트리거 조건, whiteout과 opaque 디렉터리 의미, redirect_dir/metacopy 옵션의 차이, rename/하드링크/권한 처리 제약, Docker·containerd 오버레이(Overlay) 드라이버의 실제 I/O 패턴, 성능 저하와 일관성 이슈 대응법까지 상세히 설명합니다.
핵심 요약
- Union Mount — 여러 디렉토리 트리를 하나로 병합하여 보여주는 파일시스템 기법입니다.
- Upper / Lower 레이어 — 상위 레이어(upper)는 읽기/쓰기, 하위 레이어(lower)는 읽기 전용으로 역할이 구분됩니다.
- Merged View — 사용자에게는 upper와 lower가 합쳐진 단일 디렉토리 트리로 보입니다.
- Copy-Up — lower 레이어의 파일을 수정할 때, 해당 파일을 upper 레이어로 복사한 뒤 수정하는 메커니즘입니다.
- Whiteout 파일 — lower 레이어의 파일을 삭제한 것처럼 보이게 하는 특수 파일로, merged view에서 해당 파일을 숨깁니다.
- Opaque 디렉토리 — lower 레이어의 디렉토리 내용을 완전히 가리는 표시로, 디렉토리 삭제 후 재생성 시 사용됩니다.
- redirect_dir / metacopy — 디렉토리 이름 변경 최적화(redirect_dir)와 메타데이터만 copy-up하는 최적화(metacopy) 기능입니다.
- 컨테이너 이미지 레이어 — Docker/OCI 컨테이너는 OverlayFS의 레이어 구조를 활용하여 이미지를 효율적으로 저장·공유합니다.
단계별 이해
- Union Mount 개념 이해 — upper/lower/merged 디렉토리의 관계를 파악합니다.
mount -t overlay에서lowerdir,upperdir,workdir,merged가 각각 어떤 역할을 하는지 확인합니다. - 파일 읽기/쓰기 경로 추적 — merged view에서 파일 접근 시 upper/lower 중 어디에서 데이터를 가져오는지 따라갑니다.
upper에 파일이 있으면 upper 우선, 없으면 lower에서 읽으며, 쓰기 시 copy-up이 발생하는 조건을 확인합니다.
- Copy-Up과 Whiteout 동작 분석 — lower 파일의 수정·삭제 시 내부 메커니즘을 이해합니다.
copy-up은 파일 전체를 upper로 복사하므로 큰 파일에서는 지연이 발생할 수 있고, whiteout은 character device(0,0)로 구현됩니다.
- 컨테이너 활용 사례 탐구 — Docker가 OverlayFS를 스토리지 드라이버로 사용하는 방식을 살펴봅니다.
베이스 이미지 → 중간 레이어 → 컨테이너 쓰기 레이어 구조에서 레이어 공유와 COW(Copy-on-Write) 효과를 확인합니다.
- 고급 기능과 제약 사항 검토 — redirect_dir, metacopy, index, nfs_export 등 옵션의 효과와 한계를 비교합니다.
각 옵션은 성능 또는 호환성을 개선하지만, POSIX 시맨틱 완전성과 트레이드오프가 있으므로 사용 환경에 맞게 선택합니다.
fs/overlayfs/
OverlayFS 개요
Union Mount 개념
Union mount는 여러 디렉토리 트리를 하나의 통합된 디렉토리 트리로 병합하여 보여주는 파일시스템 기법입니다. 사용자에게는 하나의 디렉토리로 보이지만, 실제로는 여러 레이어(layer)가 겹쳐져 있으며, 각 레이어의 파일이 우선순위(Priority)에 따라 합쳐져 표시됩니다.
OverlayFS는 이 union mount 개념을 구현한 커널 파일시스템으로, 상위 레이어(upper)와 하위 레이어(lower)를 겹쳐서 병합 뷰(merged)를 제공합니다. 상위 레이어는 읽기/쓰기가 가능하고, 하위 레이어는 읽기 전용입니다.
역사: overlay vs aufs vs unionfs
Linux에서 union mount를 구현하려는 시도는 여러 차례 있었습니다:
| 파일시스템 | 등장 시기 | 상태 | 특징 |
|---|---|---|---|
| UnionFS | 2004 | 사실상 폐기 | 최초의 Linux union mount 구현. Stony Brook 대학에서 개발 |
| AUFS | 2006 | out-of-tree | Another UnionFS. UnionFS를 완전 재작성. 기능이 풍부하나 코드 복잡도가 높아 mainline 거부 |
| OverlayFS | 2014 (v3.18) | mainline | Miklos Szeredi가 설계. 간결한 구조로 mainline 통합 성공. Docker 공식 스토리지 드라이버 |
AUFS는 초기 Docker에서 기본 스토리지 드라이버로 사용되었으나, 커널 mainline에 포함되지 않아 배포판마다 별도 패치(Patch)가 필요했습니다. OverlayFS는 AUFS보다 단순한 설계를 채택하여 mainline 통합에 성공했으며, 현재 컨테이너(Container) 생태계의 사실상 표준 스토리지 드라이버입니다.
fs/overlayfs/ 디렉토리에 위치하며, 약 15개의 소스 파일로 구성됩니다. AUFS의 수만 줄에 비해 훨씬 작고 유지보수하기 쉽습니다.
레이어 구조
4개의 디렉토리
OverlayFS 마운트에는 4개의 디렉토리가 관여합니다:
| 디렉토리 | 역할 | 요구사항 |
|---|---|---|
| lowerdir | 읽기 전용 레이어. 원본 데이터를 보유 | 임의의 파일시스템. 콜론(:)으로 구분하여 다중 지정 가능 |
| upperdir | 읽기/쓰기 레이어. 변경사항이 여기에 기록 | 로컬 파일시스템 (xattr 지원 필수). lowerdir과 같은 파일시스템 권장 |
| workdir | atomic 연산을 위한 임시 작업 공간 | upperdir과 반드시 같은 파일시스템에 위치해야 함 |
| merged | 마운트 포인트. 통합된 뷰가 여기에 나타남 | 비어 있는 디렉토리 |
기본 마운트 예제
# 디렉토리 구조 생성
mkdir -p /tmp/overlay/{lower,upper,work,merged}
# 하위 레이어에 파일 생성
echo "original content" > /tmp/overlay/lower/file_a.txt
echo "read-only data" > /tmp/overlay/lower/file_b.txt
mkdir /tmp/overlay/lower/subdir
echo "nested file" > /tmp/overlay/lower/subdir/nested.txt
# OverlayFS 마운트
mount -t overlay overlay \
-o lowerdir=/tmp/overlay/lower,upperdir=/tmp/overlay/upper,workdir=/tmp/overlay/work \
/tmp/overlay/merged
# merged 디렉토리에서 lower의 파일이 보임
ls /tmp/overlay/merged/
# file_a.txt file_b.txt subdir/
# 파일 수정 → upper에 copy-up 발생
echo "modified content" >> /tmp/overlay/merged/file_a.txt
# upper에 수정된 파일이 생성됨
ls /tmp/overlay/upper/
# file_a.txt
cat /tmp/overlay/upper/file_a.txt
# original content
# modified content
workdir의 역할
workdir은 copy-up, rename, whiteout 생성 등의 연산에서 원자성(atomicity)을 보장하기 위해 사용됩니다. 커널은 먼저 workdir에 임시 파일을 생성하고, 모든 작업이 완료된 후 rename()으로 upperdir로 이동합니다. 이 방식은 시스템 크래시 시 중간 상태 노출 가능성을 줄이기 위한 설계입니다.
workdir과 upperdir은 반드시 같은 파일시스템에 위치해야 합니다. 서로 다른 파일시스템에 있으면 rename()이 cross-device 에러(EXDEV)를 반환하여 마운트에 실패합니다.
마운트 옵션
OverlayFS는 다양한 마운트 옵션을 지원합니다. 주요 옵션을 정리합니다:
| 옵션 | 기본값 | 설명 |
|---|---|---|
lowerdir=path |
(필수) | 읽기 전용 하위 레이어. 콜론으로 다중 지정 (왼쪽이 상위) |
upperdir=path |
(선택) | 읽기/쓰기 상위 레이어. 생략 시 읽기 전용 overlay |
workdir=path |
(upperdir 지정 시 필수) | 임시 작업 디렉토리. upperdir과 같은 파일시스템 |
index=on|off |
off | inode index 기능. copy-up 후 하위 inode와의 연결 추적(Connection Tracking) |
nfs_export=on|off |
off | NFS export 지원. index=on을 암시 |
redirect_dir=on|off|follow|nofollow |
off (커널 빌드 옵션 의존) | 디렉토리 이름 변경(rename) 지원 |
metacopy=on|off |
off | 메타데이터만 copy-up (데이터는 지연 복사) |
volatile |
off | fsync/fdatasync 건너뛰기. 비정상 종료 시 데이터 무결성(Integrity) 보장 안 됨 |
xino=on|off|auto |
off | 고유 inode 번호 생성. 레이어 간 inode 충돌 방지 |
userxattr |
off | user namespace 내에서 unprivileged 마운트 시 사용 |
마운트 옵션 사용 예제
# 읽기 전용 overlay (upperdir 없음)
mount -t overlay overlay \
-o lowerdir=/layer1:/layer2:/layer3 \
/mnt/readonly-merged
# index + redirect_dir 활성화 (POSIX 호환성 강화)
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,index=on,redirect_dir=on \
/merged
# metacopy 활성화 (대용량 파일 메타데이터 변경 최적화)
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,metacopy=on \
/merged
# volatile 마운트 (빌드 환경 등 일시적 용도)
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,volatile \
/merged
# xino=on (st_ino 고유성 보장)
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,xino=on \
/merged
파일 조회 과정
VFS Lookup과 레이어 우선순위
사용자가 merged 디렉토리 내 파일에 접근하면, VFS는 OverlayFS의 inode_operations.lookup을 호출합니다. OverlayFS는 다음 순서로 레이어를 탐색합니다:
- Upper layer를 먼저 검사합니다
- Upper에 없으면 Lower layer를 순서대로 검사합니다 (다중 lower의 경우 왼쪽부터)
- 파일이 발견되면 해당 레이어의 dentry를 기반으로 OverlayFS dentry를 구성합니다
- 어느 레이어에서도 발견되지 않으면
-ENOENT를 반환합니다
/* fs/overlayfs/namei.c - ovl_lookup() 단순화 */
static struct dentry *ovl_lookup(struct inode *dir,
struct dentry *dentry,
unsigned int flags)
{
struct ovl_fs *ofs = OVL_FS(dentry->d_sb);
struct dentry *upperdentry = NULL;
struct ovl_path *stack = NULL;
int err;
/* 1. Upper layer에서 검색 */
if (ovl_upper_mnt(ofs)) {
err = ovl_lookup_layer(upperdentry_parent, &d, dentry->d_name,
0, &upperdentry, &upperopaque);
if (err)
goto out;
}
/* 2. Upper에서 opaque이면 lower 검색 중단 */
if (!upperopaque) {
/* 3. Lower layer들을 순서대로 검색 */
for (i = 0; i < ofs->numlower; i++) {
err = ovl_lookup_layer(..., &this, &is_whiteout);
if (this) {
stack[ctr].dentry = this;
stack[ctr].layer = &ofs->layers[i + 1];
ctr++;
break; /* 파일 발견 시 중단 */
}
}
}
/* ovl_dentry에 upper/lower 정보 연결 */
ovl_dentry_init_flags(dentry, upperdentry, oe, stack, ctr);
return NULL;
}
파일 읽기/쓰기 경로
파일 읽기와 쓰기는 서로 다른 경로를 따릅니다:
- 읽기(read) — 파일이 위치한 실제 레이어(upper 또는 lower)의 파일을 직접 읽습니다. OverlayFS는 실제 파일의
file_operations로 요청을 전달합니다 - 쓰기(write) — 파일이 upper에 있으면 직접 쓰기. lower에만 있으면 먼저 copy-up을 수행한 후 upper의 복사본에 쓰기
- mmap — 읽기 전용 mmap은 실제 레이어 파일의 page cache를 직접 사용. 쓰기 가능한 mmap은 copy-up 후 upper 파일의 page cache 사용
/* fs/overlayfs/file.c - 파일 열기 시 실제 파일 결정 */
static struct file *ovl_open_realfile(const struct file *file,
const struct path *realpath)
{
struct inode *realinode = d_inode(realpath->dentry);
struct file *realfile;
const struct cred *old_cred;
int flags = file->f_flags | OVL_OPEN_FLAGS;
old_cred = ovl_override_creds(file->f_path.dentry->d_sb);
realfile = open_with_fake_path(&file->f_path, flags, realinode, current_cred());
revert_creds(old_cred);
return realfile;
}
Copy-Up 메커니즘
Copy-Up 발생 조건
하위 레이어의 파일을 수정하려 할 때, OverlayFS는 해당 파일을 상위 레이어로 복사합니다. 이것이 copy-up입니다. 다음 연산이 copy-up을 유발합니다:
open(O_WRONLY)또는open(O_RDWR)— 쓰기 모드로 파일 열기chmod(),chown(),utimes()— 메타데이터 변경setxattr()— 확장 속성(Extended Attribute) 변경truncate()— 파일 크기 변경link()— 하드 링크 생성 (원본이 lower에 있을 경우)rename()— lower 파일/디렉토리 이름 변경
Copy-Up 과정
Copy-up은 다음 단계로 수행됩니다:
- 상위 레이어에 부모 디렉토리 구조를 재현합니다 (필요 시 중간 디렉토리도 copy-up)
workdir에 임시 파일을 생성합니다- 하위 레이어 파일의 데이터를 임시 파일로 복사합니다
- 파일의 메타데이터(소유자, 권한, 타임스탬프, xattr)를 복사합니다
trusted.overlay.originxattr에 하위 파일의 file handle을 기록합니다- 임시 파일을
upperdir의 최종 위치로rename()합니다 (원자적(Atomic) 교체)
/* fs/overlayfs/copy_up.c - copy-up 핵심 로직 (단순화) */
static int ovl_copy_up_one(struct dentry *parent,
struct dentry *dentry,
struct path *lowerpath,
struct kstat *stat)
{
struct dentry *temp;
struct dentry *upper;
int err;
/* 1. 부모 디렉토리가 upper에 없으면 재귀적으로 copy-up */
err = ovl_copy_up(parent);
if (err)
return err;
/* 2. workdir에 임시 파일 생성 */
temp = ovl_create_temp(ofs->workdir, stat->mode);
/* 3. 데이터 복사 (일반 파일인 경우) */
if (S_ISREG(stat->mode)) {
err = ovl_copy_up_data(ofs, lowerpath, temp, stat->size);
if (err)
goto out_cleanup;
}
/* 4. 메타데이터 복사 (uid, gid, mode, timestamps, xattr) */
err = ovl_copy_up_metadata(stat, temp);
/* 5. origin xattr 설정 */
err = ovl_set_origin(ofs, dentry, temp);
/* 6. 원자적으로 upperdir로 이동 */
upper = ovl_lookup_upper(ofs, dentry->d_name, parent);
err = ovl_do_rename(ofs->workdir, temp, ovl_upper_mnt(ofs), upper);
/* dentry의 upper 참조 갱신 */
ovl_dentry_set_upper_alias(dentry);
return 0;
}
ovl_copy_up_data()는 splice_direct_to_actor() 또는 copy_file_range()를 사용하여 커널 공간(Kernel Space) 내에서 zero-copy에 가까운 데이터 전송을 수행합니다. 대용량 파일의 경우에도 사용자 공간(User Space)을 거치지 않습니다.
Copy-Up과 하드 링크
index=on 옵션이 활성화된 경우, lower 레이어의 같은 inode를 가리키는 여러 하드 링크가 copy-up되면 upper에서도 하드 링크 관계가 유지됩니다. index=off(기본값)일 경우, 각 하드 링크가 독립적인 파일로 copy-up되어 하드 링크 관계가 깨집니다.
Whiteout과 Opaque
Whiteout (파일 삭제)
하위 레이어의 파일은 직접 삭제할 수 없습니다 (읽기 전용). 대신, OverlayFS는 상위 레이어에 whiteout 파일을 생성하여 해당 파일이 삭제되었음을 표시합니다.
Whiteout은 mknod(name, S_IFCHR, makedev(0, 0))로 생성되는 character device 파일(0/0)입니다:
# lower에 있는 파일 삭제
rm /tmp/overlay/merged/file_b.txt
# upper에 whiteout 생성됨
ls -la /tmp/overlay/upper/
# c--------- 1 root root 0, 0 ... file_b.txt
# merged에서는 보이지 않음
ls /tmp/overlay/merged/
# file_a.txt subdir/
# 하지만 lower의 원본은 그대로
ls /tmp/overlay/lower/
# file_a.txt file_b.txt subdir/
/* fs/overlayfs/overlayfs.h */
static inline bool ovl_is_whiteout(struct dentry *dentry)
{
struct inode *inode = d_inode(dentry);
return inode && IS_WHITEOUT(inode);
}
/* IS_WHITEOUT 매크로: char device (0, 0) 인지 확인 */
#define IS_WHITEOUT(inode) \
(S_ISCHR((inode)->i_mode) && (inode)->i_rdev == WHITEOUT_DEV)
#define WHITEOUT_DEV MKDEV(0, 0)
Opaque 디렉토리 (디렉토리 삭제)
디렉토리를 삭제하면, 그 안의 모든 내용을 whiteout으로 마킹해야 합니다. 이 대신, OverlayFS는 더 효율적인 opaque 디렉토리 방식을 사용합니다:
- upper에 같은 이름의 디렉토리를 생성합니다
- 해당 디렉토리에
trusted.overlay.opaquexattr을"y"로 설정합니다 - lookup 시 이 xattr이 발견되면, lower의 같은 이름의 디렉토리 내용은 무시됩니다
# 디렉토리 삭제 후 같은 이름으로 재생성하는 시나리오
rm -rf /tmp/overlay/merged/subdir
mkdir /tmp/overlay/merged/subdir
# upper에서 opaque 디렉토리 확인
getfattr -n trusted.overlay.opaque /tmp/overlay/upper/subdir
# trusted.overlay.opaque="y"
/* fs/overlayfs/util.c - opaque 여부 확인 */
bool ovl_is_opaquedir(struct super_block *sb,
struct dentry *dentry)
{
return ovl_check_dir_xattr(sb, dentry, OVL_XATTR_OPAQUE);
}
/* xattr 이름 상수 */
#define OVL_XATTR_OPAQUE "trusted.overlay.opaque"
#define OVL_XATTR_REDIRECT "trusted.overlay.redirect"
#define OVL_XATTR_ORIGIN "trusted.overlay.origin"
#define OVL_XATTR_METACOPY "trusted.overlay.metacopy"
trusted.overlay.* xattr은 커널 6.5부터 overlay.* 네임스페이스(Namespace) 형태(userxattr 옵션 사용 시)로도 지정 가능합니다. 이는 user namespace 내에서 unprivileged overlay mount를 지원하기 위함입니다.
Redirect 디렉토리
디렉토리 rename 문제
OverlayFS에서 lower 레이어의 디렉토리 이름을 변경(rename)하는 것은 본질적으로 어렵습니다. lower는 읽기 전용이므로 실제 이름 변경이 불가능하고, 단순히 upper에 새 이름의 디렉토리를 만들면 lower의 원래 디렉토리 내용과의 연결이 끊어집니다.
Redirect 해결책
redirect_dir=on 옵션을 사용하면, 디렉토리 rename 시 upper의 새 디렉토리에 trusted.overlay.redirect xattr을 설정하여 lower의 원래 경로를 기록합니다:
# redirect_dir=on으로 마운트
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,redirect_dir=on \
/merged
# lower에 있는 디렉토리 이름 변경
mv /merged/old_name /merged/new_name
# upper에 redirect xattr이 설정됨
getfattr -n trusted.overlay.redirect /upper/new_name
# trusted.overlay.redirect="/old_name"
Lookup 시 커널은 upper의 new_name 디렉토리에서 redirect xattr을 발견하면, lower에서 old_name 경로를 찾아 그 내용을 병합합니다.
/* fs/overlayfs/namei.c - redirect 경로 해석 */
static int ovl_check_redirect(struct dentry *dentry,
struct ovl_lookup_data *d,
size_t prelen,
const char *post)
{
int err;
struct ovl_fs *ofs = OVL_FS(dentry->d_sb);
char *redirect;
/* trusted.overlay.redirect xattr 읽기 */
redirect = ovl_get_redirect_xattr(ofs, dentry, prelen);
if (IS_ERR_OR_NULL(redirect))
return redirect ? PTR_ERR(redirect) : 0;
/* 절대 경로이면 루트부터, 상대 경로이면 현재 위치부터 해석 */
if (redirect[0] == '/')
err = ovl_lookup_absolute(ofs, redirect, d);
else
err = ovl_lookup_relative(ofs, redirect, d, post);
kfree(redirect);
return err;
}
redirect_dir=on으로 생성된 overlay를 redirect_dir=off로 마운트하면, rename된 디렉토리의 lower 내용을 볼 수 없게 됩니다. 이전 마운트와 호환성을 유지하려면 최소한 redirect_dir=follow를 사용해야 합니다.
Metacopy
메타데이터만 Copy-Up
metacopy=on 옵션을 사용하면, 파일의 메타데이터(권한, 소유자, 타임스탬프 등)만 변경될 때 데이터까지 전체 복사하지 않습니다. 대신 상위 레이어에 메타데이터만 포함하는 작은 파일을 생성하고, 실제 데이터 읽기는 여전히 하위 레이어에서 수행합니다.
이 최적화는 대용량 파일의 chmod나 chown 같은 연산에서 극적인 성능 향상을 제공합니다:
| 연산 | metacopy=off (기본) | metacopy=on |
|---|---|---|
chmod 10GB 파일 |
10GB 복사 + 메타데이터 변경 | 메타데이터만 변경 (즉시 완료) |
chown 10GB 파일 |
10GB 복사 + 소유자 변경 | 소유자 정보만 변경 (즉시 완료) |
write() 10GB 파일 |
10GB 복사 후 쓰기 | 10GB 복사 후 쓰기 (동일) |
Metacopy 구현 원리
Metacopy로 copy-up된 파일은 upper에 0바이트 크기의 파일로 생성되며, trusted.overlay.metacopy xattr이 설정됩니다. 이 xattr에는 lower 파일의 digest(선택적)가 저장되어, 데이터 무결성 검증에 사용됩니다.
/* fs/overlayfs/copy_up.c - metacopy 판별 */
static bool ovl_need_meta_copy_up(struct dentry *dentry,
umode_t mode,
int flags)
{
struct ovl_fs *ofs = OVL_FS(dentry->d_sb);
/* metacopy 옵션이 비활성화이면 항상 전체 copy-up */
if (!ofs->config.metacopy)
return false;
/* 일반 파일만 metacopy 가능 */
if (!S_ISREG(mode))
return false;
/* 데이터 접근이 필요한 경우 전체 copy-up */
if (flags & (O_WRONLY | O_RDWR | O_TRUNC))
return false;
/* 메타데이터만 변경 → metacopy 수행 */
return true;
}
/* 지연 데이터 copy-up: 실제 쓰기 발생 시 */
static int ovl_maybe_copy_up(struct dentry *dentry,
int flags)
{
int err = 0;
if (ovl_dentry_needs_data_copy_up(dentry, flags)) {
/* metacopy 상태에서 전체 copy-up으로 전환 */
err = ovl_copy_up_data(...);
if (!err)
ovl_remove_metacopy_xattr(dentry);
}
return err;
}
verity digest를 metacopy xattr에 포함하여 데이터 무결성을 검증할 수 있습니다.
커널 내부 구현
ovl_entry와 ovl_inode
OverlayFS의 핵심 데이터 구조는 VFS의 dentry, inode 위에 overlay 전용 정보를 덧씌우는 형태입니다:
/* fs/overlayfs/ovl_entry.h */
/* overlay 파일시스템 전역 정보 */
struct ovl_fs {
unsigned int numlayer; /* 총 레이어 수 (1 upper + N lower) */
struct ovl_layer *layers; /* 레이어 배열 */
struct ovl_sb *upper_sb; /* upper 파일시스템 superblock */
struct dentry *workdir; /* work 디렉토리 dentry */
struct ovl_config config; /* 마운트 옵션 */
const struct cred *creator_cred; /* 마운트 시 credential */
long upper_ino_bits; /* xino 계산용 */
};
/* 마운트 옵션 구조체 */
struct ovl_config {
char *lowerdir;
char *upperdir;
char *workdir;
bool redirect_dir;
bool index;
bool nfs_export;
bool metacopy;
int xino;
bool ovl_volatile;
bool userxattr;
};
/* 개별 레이어 정보 */
struct ovl_layer {
struct vfsmount *mnt; /* 레이어의 vfsmount */
struct inode *trap; /* 루프 감지용 trap inode */
int idx; /* 레이어 인덱스 (0 = upper) */
int fsid; /* 파일시스템 ID */
};
ovl_inode 구조
/* fs/overlayfs/ovl_entry.h */
struct ovl_inode {
union {
struct ovl_dir_cache *cache; /* 디렉토리: readdir 캐시 */
struct inode *lowerdata; /* 파일: metacopy 시 데이터 inode */
};
const char *redirect; /* redirect 경로 */
struct inode *upper; /* upper 레이어의 실제 inode */
struct ovl_path lowerpath; /* lower 레이어 경로 */
struct inode vfs_inode; /* VFS inode (임베딩) */
unsigned long flags; /* OVL_UPPERDATA, OVL_INDEX 등 */
};
super_operations
OverlayFS의 super_operations는 VFS가 overlay 파일시스템과 상호작용하는 주요 콜백(Callback)입니다:
/* fs/overlayfs/super.c */
static const struct super_operations ovl_super_operations = {
.alloc_inode = ovl_alloc_inode, /* ovl_inode 할당 */
.free_inode = ovl_free_inode, /* ovl_inode 해제 */
.destroy_inode = ovl_destroy_inode, /* inode 파괴 */
.drop_inode = generic_delete_inode,
.put_super = ovl_put_super, /* unmount 시 정리 */
.sync_fs = ovl_sync_fs, /* sync 시 upper fs로 전달 */
.statfs = ovl_statfs, /* df 명령 시 upper fs 통계 */
.show_options = ovl_show_options, /* /proc/mounts 출력 */
.remount_fs = ovl_remount_fs, /* remount 처리 */
};
inode_operations
OverlayFS는 파일과 디렉토리에 대해 별도의 inode_operations를 제공합니다:
/* fs/overlayfs/dir.c - 디렉토리 inode operations */
const struct inode_operations ovl_dir_inode_operations = {
.lookup = ovl_lookup, /* 레이어별 검색 */
.mkdir = ovl_mkdir, /* upper에 디렉토리 생성 */
.symlink = ovl_symlink, /* upper에 심볼릭 링크 생성 */
.unlink = ovl_unlink, /* 삭제/whiteout 생성 */
.rmdir = ovl_rmdir, /* 디렉토리 삭제/opaque 처리 */
.rename = ovl_rename, /* rename/redirect 처리 */
.link = ovl_link, /* 하드 링크 생성 */
.create = ovl_create, /* 파일 생성 */
.mknod = ovl_mknod, /* 특수 파일 생성 */
.permission = ovl_permission, /* 접근 권한 검사 */
.getattr = ovl_getattr, /* stat() 결과 반환 */
.listxattr = ovl_listxattr, /* xattr 목록 */
.get_inode_acl = ovl_get_inode_acl, /* POSIX ACL */
};
/* fs/overlayfs/inode.c - 파일 inode operations */
const struct inode_operations ovl_file_inode_operations = {
.setattr = ovl_setattr, /* 속성 변경 (copy-up 유발) */
.permission = ovl_permission,
.getattr = ovl_getattr,
.listxattr = ovl_listxattr,
.get_inode_acl = ovl_get_inode_acl,
.update_time = ovl_update_time, /* atime 갱신 */
.fiemap = ovl_fiemap, /* 파일 extent 매핑 */
};
file_operations
/* fs/overlayfs/file.c */
const struct file_operations ovl_file_operations = {
.open = ovl_open,
.release = ovl_release,
.read_iter = ovl_read_iter, /* 실제 파일로 전달 */
.write_iter = ovl_write_iter, /* copy-up 후 upper로 전달 */
.fsync = ovl_fsync, /* upper fs로 fsync 전달 */
.mmap = ovl_mmap, /* 실제 파일의 mmap 사용 */
.fallocate = ovl_fallocate,
.fadvise = ovl_fadvise,
.splice_read = ovl_splice_read,
.splice_write = ovl_splice_write,
.copy_file_range = ovl_copy_file_range,
.llseek = ovl_llseek,
};
컨테이너 활용
Docker/Podman overlay2 스토리지 드라이버
Docker와 Podman은 overlay2 스토리지 드라이버를 사용하여 컨테이너 이미지 레이어를 효율적으로 관리합니다. 각 이미지 레이어는 별도의 디렉토리로 저장되며, 컨테이너 실행 시 이들을 OverlayFS로 겹쳐 마운트합니다.
overlay2 디렉토리 구조
컨테이너 CoW(Copy-on-Write) 패턴
컨테이너 환경에서 OverlayFS의 CoW 패턴은 다음과 같은 이점을 제공합니다:
- 이미지 레이어 공유 — 같은 이미지를 기반으로 한 100개의 컨테이너가 동일한 lower 레이어를 공유합니다. 디스크 사용량이 극적으로 절감됩니다
- 빠른 컨테이너 시작 — 이미지 데이터를 복사할 필요 없이 upper 레이어와 work 디렉토리만 생성하면 됩니다
- 레이어 캐싱 — Docker 빌드 시 변경되지 않은 레이어를 캐시에서 재사용합니다
- 격리 — 각 컨테이너의 변경사항은 자신의 upper 레이어에만 기록되어 다른 컨테이너에 영향을 주지 않습니다
다중 Lower 레이어
다중 Lower 마운트
OverlayFS는 콜론(:)으로 구분하여 여러 lower 레이어를 지정할 수 있습니다. 왼쪽 레이어가 우선순위가 높습니다:
# 다중 lower 레이어 마운트
mount -t overlay overlay \
-o lowerdir=/layer3:/layer2:/layer1,upperdir=/upper,workdir=/work \
/merged
# 우선순위: upper > layer3 > layer2 > layer1
# 같은 이름의 파일이 여러 레이어에 존재하면 상위 레이어의 파일이 보임
Lower 레이어 제한
커널 버전에 따른 lower 레이어 수 제한:
| 커널 버전 | 최대 lower 레이어 | 비고 |
|---|---|---|
| 3.18 ~ 5.10 | 최대 500개 (마운트 문자열 페이지(Page) 크기 제한) | 실제로는 경로 길이에 의존 |
| 5.11+ | 최대 500개 | OVL_MAX_STACK 상수로 제한 |
| 6.5+ | 데이터 전용 lower 별도 지정 가능 | lowerdir+ 및 datadir+ 새 문법 |
중첩 Overlay
OverlayFS 위에 다시 OverlayFS를 마운트(중첩)할 수도 있습니다. 이 경우 하위 overlay의 merged 뷰가 상위 overlay의 lower가 됩니다. Docker의 multi-stage 빌드나 중첩 컨테이너에서 활용될 수 있습니다:
# 1차 overlay
mount -t overlay overlay \
-o lowerdir=/base,upperdir=/layer1-upper,workdir=/layer1-work \
/layer1-merged
# 2차 overlay (1차의 merged를 lower로 사용)
mount -t overlay overlay \
-o lowerdir=/layer1-merged,upperdir=/layer2-upper,workdir=/layer2-work \
/layer2-merged
성능 특성과 튜닝
I/O 패턴별 성능
| 연산 | 성능 특성 | 설명 |
|---|---|---|
| 읽기 (lower) | 네이티브와 동일 | lower 파일시스템에 직접 I/O. 오버헤드(Overhead) 거의 없음 |
| 읽기 (upper) | 네이티브와 동일 | upper 파일시스템에 직접 I/O |
| 쓰기 (upper 파일) | 네이티브와 동일 | 이미 upper에 있는 파일은 직접 쓰기 |
| 첫 쓰기 (lower 파일) | 느림 (copy-up) | 전체 파일을 upper로 복사 후 쓰기. 파일 크기에 비례 |
| readdir | 느림 (병합) | 모든 레이어의 디렉토리를 읽고 중복 제거/whiteout 처리 |
| stat/lookup | 약간 느림 | 레이어 수에 비례하여 검색 시간 증가 |
xino=on 옵션
xino=on 옵션은 모든 레이어의 inode 번호를 고유하게 만듭니다. 기본적으로 서로 다른 레이어의 파일이 같은 inode 번호를 가질 수 있는데, 이는 find나 tar 같은 도구가 하드 링크를 잘못 감지하는 문제를 유발합니다.
# xino=on: 레이어 인덱스를 inode 번호의 상위 비트에 인코딩
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,xino=on \
/merged
# 결과: 각 레이어의 inode 번호가 고유하게 변환됨
# 예: lower inode 12345 → merged inode (1 << 32) | 12345
# 예: upper inode 12345 → merged inode (0 << 32) | 12345
/* fs/overlayfs/inode.c - xino inode 번호 생성 */
static u64 ovl_remap_lower_ino(u64 ino, int xinobits,
int fsid, const char *name)
{
unsigned int xinoshift = 64 - xinobits;
/* fsid를 상위 비트에 인코딩 */
if (unlikely(ino >> xinoshift)) {
pr_warn_ratelimited("overlayfs: inode number too big (%pd2, ino=%llu, xinobits=%d)\\n",
name, ino, xinobits);
return ino;
}
return ino | ((u64)fsid) << xinoshift;
}
volatile 마운트
volatile 옵션은 sync/fsync/fdatasync 호출을 건너뛰어 쓰기 성능을 향상시킵니다. 비정상 종료 시 데이터 무결성이 보장되지 않으므로, 일시적인 빌드 환경이나 재현 가능한 데이터에만 사용해야 합니다:
# volatile 마운트 — CI/CD 빌드 환경에 적합
mount -t overlay overlay \
-o lowerdir=/base-image,upperdir=/build-upper,workdir=/build-work,volatile \
/build-merged
# 빌드 수행 (fsync 오버헤드 없음)
make -C /build-merged -j$(nproc)
# 빌드 완료 후 결과물만 추출하고 overlay unmount
cp /build-merged/output/* /final-output/
umount /build-merged
volatile 마운트 후 비정상 종료(크래시, 전원 차단)가 발생하면, upper 레이어의 데이터가 손상될 수 있습니다. 이 경우 해당 overlay는 더 이상 마운트할 수 없을 수 있으며, upper 디렉토리를 삭제하고 다시 시작해야 합니다.
성능 최적화 팁
- upper/lower 같은 파일시스템 — 같은 물리 디스크/파일시스템에 upper와 lower를 배치하면 copy-up 시
copy_file_range()의 reflink를 활용할 수 있습니다 (btrfs, XFS) - 레이어 수 최소화 — lower 레이어가 많을수록 lookup 성능이 저하됩니다. Docker 이미지 빌드 시 레이어 수를 최소화하세요
- metacopy=on — 대용량 파일의 메타데이터만 변경하는 워크로드에서 copy-up 비용을 대폭 절감합니다
- tmpfs 위의 overlay — 임시 빌드나 테스트 환경에서는 upper/work를 tmpfs에 배치하면 I/O 성능이 극대화됩니다
- readdir 캐시 — OverlayFS는 readdir 결과를 캐시합니다. 같은 디렉토리를 반복 읽는 워크로드에서 두 번째 이후 읽기가 빨라집니다
제한사항과 주의점
POSIX 호환성
OverlayFS는 완벽한 POSIX 호환 파일시스템이 아닙니다. 다음 사항에서 POSIX 시맨틱과 차이가 있습니다:
| 항목 | POSIX 기대 동작 | OverlayFS 실제 동작 | 해결 옵션 |
|---|---|---|---|
| st_dev | 같은 파일시스템이면 동일 | upper/lower 파일이 다른 st_dev를 가질 수 있음 | — |
| st_ino | 파일시스템 내 고유 | 레이어 간 중복 가능 | xino=on |
| 하드 링크 | 같은 inode 공유 | copy-up 시 독립 파일로 분리 | index=on |
| rename(dir) | 원자적 이름 변경 | lower 디렉토리 rename 불가 (EXDEV) | redirect_dir=on |
| open+unlink | 열린 fd는 삭제 후에도 유효 | lower 파일 whiteout 후 fd 동작이 다를 수 있음 | — |
| d_type | readdir에서 정확한 타입 | 일부 하위 파일시스템에서 부정확 | xfs_repair 등 |
NFS Export
OverlayFS를 NFS로 export하려면 nfs_export=on 옵션이 필요합니다. 이 옵션은 index=on을 암시하며, NFS file handle을 통해 overlay 파일을 안정적으로 식별할 수 있게 합니다. 그러나 제한사항이 있습니다:
- NFS file handle이 copy-up 전후로 변경될 수 있습니다
- 모든 lower 레이어가 NFS export를 지원하는 파일시스템이어야 합니다
index=on으로 인한 추가 디스크 사용량과 성능 오버헤드가 발생합니다
SELinux
SELinux 환경에서 OverlayFS를 사용할 때 주의사항:
- 커널 4.19 이전에는 OverlayFS에서 SELinux label이 제대로 동작하지 않았습니다
- 커널 4.19+에서
context=마운트 옵션이 아닌, 각 파일의 xattr 기반 label을 지원합니다 - copy-up 시 SELinux context가 함께 복사되므로, upper와 lower의 label 정책이 일관되어야 합니다
- 컨테이너 환경에서는 일반적으로
container_file_tlabel이 사용됩니다
inode 번호 문제
OverlayFS의 가장 빈번한 이슈 중 하나는 inode 번호의 비일관성입니다:
# 문제: copy-up 전후로 inode 번호가 변할 수 있음
stat --format='%i' /merged/file.txt
# 12345 (lower의 inode)
chmod 644 /merged/file.txt # copy-up 발생
stat --format='%i' /merged/file.txt
# 67890 (upper의 inode, 변경됨!)
# 해결: xino=on 사용
# 그래도 copy-up 전후 inode 변경은 발생할 수 있음
# index=on을 함께 사용하면 copy-up 후에도 inode 번호 유지
기타 제한사항
- 하위 파일시스템 요구 — upper/work의 파일시스템은
d_type과 xattr을 지원해야 합니다. ext4, XFS(ftype=1), btrfs가 적합합니다. tmpfs도 지원됩니다 - 수정 중 lower 변경 금지 — overlay가 마운트된 상태에서 lower 디렉토리를 직접 수정하면 정의되지 않은 동작이 발생합니다. 반드시 merged 뷰를 통해서만 접근해야 합니다
- fd 전달 — overlay 파일의 fd를 다른 프로세스(Process)에 전달하면, 수신 프로세스는 overlay가 아닌 실제 파일시스템의 fd를 받게 될 수 있습니다
- fanotify — overlay 파일의 변경 이벤트가 lower/upper 파일시스템의 이벤트와 일관되지 않을 수 있습니다
- quotas — OverlayFS 자체는 quota를 지원하지 않습니다. upper 파일시스템의 quota만 적용됩니다
Documentation/filesystems/overlayfs.rst에 상세히 문서화되어 있습니다. 프로덕션 환경에서 사용하기 전에 반드시 해당 문서를 확인하세요.
문제 해결 명령어
# overlay 마운트 상태 확인
mount -t overlay
# overlay xattr 확인
getfattr -d -m trusted.overlay /upper/some_dir
# whiteout 파일 찾기
find /upper -type c -perm 0 -print
# copy-up된 파일 찾기 (origin xattr 보유)
find /upper -exec getfattr -n trusted.overlay.origin {} \; 2>/dev/null
# metacopy 파일 찾기
find /upper -exec getfattr -n trusted.overlay.metacopy {} \; 2>/dev/null
# redirect 디렉토리 찾기
find /upper -type d -exec getfattr -n trusted.overlay.redirect {} \; 2>/dev/null
# opaque 디렉토리 찾기
find /upper -type d -exec getfattr -n trusted.overlay.opaque {} \; 2>/dev/null
# overlay 관련 커널 메시지 확인
dmesg | grep -i overlay
# overlay 커널 모듈 정보
modinfo overlay
Copy-Up 상세 흐름
Copy-up은 OverlayFS에서 가장 복잡한 내부 경로입니다. 단순히 파일을 복사하는 것이 아니라, 부모 디렉토리 재귀 처리, 원자성 보장, xattr 전파 등 여러 단계를 거칩니다. 아래 다이어그램은 ovl_copy_up()이 호출될 때의 전체 흐름을 보여줍니다.
데이터 복사 경로 선택
Copy-up 시 실제 데이터를 복사하는 ovl_copy_up_data()는 하위 파일시스템의 능력에 따라 최적의 복사 방법을 선택합니다:
/* fs/overlayfs/copy_up.c - 데이터 복사 전략 선택 */
static int ovl_copy_up_data(struct ovl_fs *ofs,
const struct path *old,
const struct path *new,
loff_t len)
{
struct file *old_file, *new_file;
loff_t old_pos = 0, new_pos = 0;
loff_t cloned;
int error = 0;
/* 1단계: clone_file_range 시도 (reflink, btrfs/XFS) */
cloned = do_clone_file_range(old_file, 0, new_file, 0, len, 0);
if (cloned == len)
goto out; /* 전체 reflink 성공 → 즉시 완료 */
/* 2단계: copy_file_range 시도 (서버측 복사) */
cloned = vfs_copy_file_range(old_file, 0, new_file, 0, len, 0);
if (cloned == len)
goto out;
/* 3단계: splice 기반 커널 내 복사 (fallback) */
while (len) {
size_t this_len = OVL_COPY_UP_CHUNK_SIZE;
if (len < this_len)
this_len = len;
error = do_splice_direct(old_file, &old_pos,
new_file, &new_pos,
this_len, SPLICE_F_MOVE);
if (error <= 0)
break;
len -= error;
}
out:
return error;
}
| 복사 방법 | 조건 | 성능 특성 |
|---|---|---|
| clone_file_range (reflink) | btrfs, XFS(reflink 지원 시), upper/lower 같은 FS | O(1) — 데이터 블록 공유, 메타데이터만 복사 |
| copy_file_range | 파일시스템이 server-side copy 지원 시 | 커널 내 제로카피에 가까움 |
| splice (do_splice_direct) | fallback — 항상 사용 가능 | 커널 파이프 버퍼(Buffer) 경유, 사용자 공간 미경유 |
Readdir 병합 메커니즘
OverlayFS의 readdir()은 모든 레이어의 디렉토리 항목을 병합하여 중복을 제거하고 whiteout을 반영해야 합니다. 이 과정은 일반 파일시스템의 readdir보다 복잡하고 느립니다.
/* fs/overlayfs/readdir.c - readdir 캐시 구조 */
struct ovl_dir_cache {
long version; /* 캐시 유효성 검사 */
struct list_head entries; /* 병합된 디렉토리 항목 리스트 */
struct rb_root root; /* 중복 검사용 RB-tree */
int count; /* 총 항목 수 */
};
struct ovl_cache_entry {
unsigned int len; /* 이름 길이 */
unsigned int type; /* d_type (DT_REG, DT_DIR 등) */
struct list_head l_node; /* 순차 접근용 */
struct rb_node node; /* 중복 검사용 */
bool is_upper; /* upper 레이어 출처 여부 */
bool is_whiteout; /* whiteout 마킹 */
char name[]; /* 가변 길이 이름 */
};
/usr/bin 같은 대형 디렉토리에 대해 ls -la를 실행하면, 첫 번째 호출에서 눈에 띄는 지연이 발생할 수 있습니다. 캐시가 생성된 이후의 반복 호출은 빠릅니다.
비특권 마운트와 User Namespace
커널 5.11부터 OverlayFS는 user namespace 내에서 비특권(unprivileged) 마운트를 지원합니다. 이를 통해 rootless 컨테이너(Podman rootless, Docker rootless)가 OverlayFS를 사용할 수 있게 되었습니다.
userxattr 옵션
비특권 마운트에서는 trusted.* xattr 네임스페이스에 접근할 수 없습니다. 이 문제를 해결하기 위해 userxattr 옵션이 도입되었으며, overlay 내부 메타데이터를 user.overlay.* 네임스페이스에 저장합니다:
| 특권 마운트 (기본) | 비특권 마운트 (userxattr) | 용도 |
|---|---|---|
trusted.overlay.opaque |
user.overlay.opaque |
opaque 디렉토리 표시 |
trusted.overlay.redirect |
user.overlay.redirect |
디렉토리 redirect 경로 |
trusted.overlay.origin |
user.overlay.origin |
lower 파일 원본 참조 |
trusted.overlay.metacopy |
user.overlay.metacopy |
metacopy 상태 표시 |
trusted.overlay.whiteout |
user.overlay.whiteout |
whiteout 표시 (char dev 대신 xattr) |
# rootless 컨테이너에서의 비특권 overlay 마운트
unshare --user --mount --map-root-user bash -c '
mkdir -p /tmp/ovl/{lower,upper,work,merged}
echo "hello" > /tmp/ovl/lower/test.txt
mount -t overlay overlay \
-o lowerdir=/tmp/ovl/lower,upperdir=/tmp/ovl/upper,\
workdir=/tmp/ovl/work,userxattr \
/tmp/ovl/merged
cat /tmp/ovl/merged/test.txt
'
/* fs/overlayfs/params.c - userxattr에 따른 xattr 접두사 결정 */
static const char *const ovl_xattr_trusted_pfx[] = {
[OVL_XATTR_OPAQUE] = "trusted.overlay.opaque",
[OVL_XATTR_REDIRECT] = "trusted.overlay.redirect",
[OVL_XATTR_ORIGIN] = "trusted.overlay.origin",
[OVL_XATTR_METACOPY] = "trusted.overlay.metacopy",
};
static const char *const ovl_xattr_user_pfx[] = {
[OVL_XATTR_OPAQUE] = "user.overlay.opaque",
[OVL_XATTR_REDIRECT] = "user.overlay.redirect",
[OVL_XATTR_ORIGIN] = "user.overlay.origin",
[OVL_XATTR_METACOPY] = "user.overlay.metacopy",
};
/* 마운트 시 userxattr 옵션에 따라 선택 */
const char *const *ovl_xattr_table(struct ovl_fs *ofs)
{
if (ofs->config.userxattr)
return ovl_xattr_user_pfx;
return ovl_xattr_trusted_pfx;
}
비특권 Whiteout 처리
특권 환경에서 whiteout은 mknod(0, 0)으로 character device를 생성합니다. 그러나 user namespace 내에서는 mknod()가 허용되지 않습니다. 이를 위해 커널 6.5+에서는 xattr 기반 whiteout이 도입되었습니다:
/* 비특권 환경에서의 whiteout: 일반 파일 + xattr 마킹 */
/* user.overlay.whiteout="y" xattr이 설정된 일반 파일이 whiteout 역할 */
static int ovl_create_whiteout(struct ovl_fs *ofs,
struct dentry *dir,
struct dentry *dentry)
{
if (ofs->config.userxattr) {
/* user namespace: 일반 파일 생성 후 xattr 설정 */
err = ovl_do_create(dir, dentry, S_IFREG | 0);
if (!err)
err = ovl_set_xattr(ofs, dentry, OVL_XATTR_WHITEOUT, "y");
} else {
/* 특권 환경: 전통적인 char device (0,0) */
err = ovl_do_whiteout(dir, dentry);
}
return err;
}
overlay 드라이버와 userxattr 옵션을 사용합니다. 커널 5.11+ 환경이라면 별도 설정 없이 OverlayFS를 사용할 수 있습니다. 관련 내용은 네임스페이스 문서를 참고하세요.
보안: fs-verity 통합
커널 6.6부터 OverlayFS는 fs-verity와 통합되어, metacopy 파일의 lower 데이터 무결성을 암호학적으로 검증할 수 있습니다. 이는 컨테이너 이미지의 변조 방지에 중요합니다.
verity digest 활용
metacopy로 copy-up된 파일의 trusted.overlay.metacopy xattr에 lower 파일의 fs-verity digest를 저장합니다. 이후 lower 파일이 변조되면 digest 불일치로 접근이 거부됩니다:
/* fs/overlayfs/copy_up.c - metacopy xattr에 verity digest 포함 */
struct ovl_metacopy {
u8 version; /* 메타데이터 버전 */
u8 flags; /* OVL_METACOPY_FL_DIGEST 등 */
u8 digest_algo; /* FS_VERITY_HASH_ALG_SHA256 등 */
u8 digest_len; /* digest 바이트 길이 */
u8 digest[FS_VERITY_MAX_DIGEST_SIZE]; /* sha256/sha512 */
};
/* 데이터 접근 시 digest 검증 */
static int ovl_verify_origin_fh(struct ovl_fs *ofs,
struct dentry *upper,
struct dentry *origin)
{
struct ovl_metacopy metacopy;
const struct fsverity_digest *origin_digest;
/* upper의 metacopy xattr에서 저장된 digest 읽기 */
err = ovl_get_metacopy_xattr(ofs, upper, &metacopy);
/* lower(origin) 파일의 현재 verity digest와 비교 */
origin_digest = fsverity_get_digest(d_inode(origin));
if (memcmp(metacopy.digest, origin_digest->digest,
metacopy.digest_len) != 0) {
pr_warn_ratelimited(
"overlayfs: digest mismatch for metacopy file\\n");
return -EIO; /* 무결성 검증 실패 */
}
return 0;
}
fsverity enable /lower/file로 lower 파일에 verity를 활성화한 후 overlay를 마운트합니다. 자세한 내용은 ext4 문서의 verity 섹션을 참고하세요.
Composefs: 읽기 전용 이미지 검증
커널 6.6+에서 등장한 composefs는 OverlayFS를 기반으로 하여, content-addressed 이미지 배포에 fs-verity 검증을 통합한 메커니즘입니다. ostree(Fedora/RHEL 계열)와 컨테이너 이미지 배포에서 활용됩니다:
- 하위 레이어의 모든 파일에 fs-verity가 활성화됨
- 매니페스트 파일(EROFS 이미지)에 각 파일의 verity digest가 기록됨
- 마운트 시 매니페스트와 실제 파일의 digest가 비교되어 변조 여부 검증
- 같은 content hash를 가진 파일은 하드링크로 중복 제거 (content-addressed storage)
datadir+) 기능과 함께 동작합니다. 메타데이터 레이어(EROFS)가 파일 경로와 속성을, 데이터 레이어(content-addressed blobs)가 실제 내용을 제공합니다.
디버깅(Debugging)과 추적
ftrace로 OverlayFS 추적
OverlayFS 내부 동작을 추적하려면 ftrace의 함수 트레이싱을 활용할 수 있습니다:
# copy-up 이벤트 추적
cd /sys/kernel/debug/tracing
# overlay 관련 함수 필터 설정
echo 'ovl_copy_up*' > set_ftrace_filter
echo 'ovl_lookup' >> set_ftrace_filter
echo 'ovl_create_whiteout' >> set_ftrace_filter
echo 'ovl_rename' >> set_ftrace_filter
# 함수 그래프 트레이서 활성화
echo function_graph > current_tracer
echo 1 > tracing_on
# overlay 파일 수정하여 copy-up 유발
echo "test" >> /merged/some_lower_file.txt
# 트레이스 결과 확인
cat trace
# 1) | ovl_copy_up() {
# 1) | ovl_copy_up_one() {
# 1) 0.850 us | ovl_create_temp();
# 1) + 15.200 us | ovl_copy_up_data();
# 1) 1.100 us | ovl_copy_up_metadata();
# 1) 0.420 us | ovl_set_origin();
# 1) 0.380 us | ovl_do_rename();
# 1) + 18.950 us | }
# 1) + 19.200 us | }
echo 0 > tracing_on
bpftrace로 copy-up 모니터링
bpftrace를 사용하면 실시간(Real-time)으로 copy-up 이벤트를 모니터링하고, 어떤 파일이 copy-up되는지 확인할 수 있습니다:
# copy-up 발생 시 파일 경로와 소요 시간 출력
bpftrace -e '
kprobe:ovl_copy_up_one {
@start[tid] = nsecs;
@path[tid] = str(((struct dentry *)arg1)->d_name.name);
}
kretprobe:ovl_copy_up_one /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
printf("copy-up: %-40s %6d us (ret=%d)\n",
@path[tid], $dur, retval);
delete(@start[tid]);
delete(@path[tid]);
}
'
# 출력 예시:
# copy-up: large_binary 125432 us (ret=0)
# copy-up: config.json 89 us (ret=0)
# copy-up: .bashrc 112 us (ret=0)
OverlayFS tracepoint 활용
커널은 파일시스템 관련 tracepoint도 제공합니다. VFS 레벨의 tracepoint를 overlay 마운트에 한정하여 추적할 수 있습니다:
# perf로 overlay 관련 VFS 이벤트 추적
perf trace -e 'fs:*' --filter 'comm == "docker"' -- sleep 10
# overlay 관련 시스템 콜 프로파일링
perf stat -e 'syscalls:sys_enter_open*,syscalls:sys_enter_rename*,syscalls:sys_enter_unlink*' \
-p $(pidof containerd) -- sleep 30
# /proc/self/mountinfo에서 overlay 마운트 정보 확인
grep overlay /proc/self/mountinfo
# 218 40 0:57 / /merged rw,relatime - overlay overlay
# rw,lowerdir=/lower,upperdir=/upper,workdir=/work
chmod/chown을 제거하거나 metacopy=on을 활성화하여 빌드 시간을 단축할 수 있습니다. 관련 도구 활용은 ftrace와 BPF 문서를 참고하세요.
커널 소스 파일 맵
OverlayFS의 커널 소스는 fs/overlayfs/ 디렉토리에 위치하며, 각 파일의 역할은 다음과 같습니다:
| 파일 | 역할 | 주요 함수/구조체(Struct) |
|---|---|---|
super.c |
마운트/언마운트, superblock 초기화 | ovl_fill_super(), ovl_super_operations |
namei.c |
경로 탐색, lookup, redirect 해석 | ovl_lookup(), ovl_check_redirect() |
inode.c |
inode 연산, 속성 조회/변경 | ovl_getattr(), ovl_setattr(), ovl_permission() |
file.c |
파일 읽기/쓰기, mmap, fsync | ovl_read_iter(), ovl_write_iter(), ovl_open() |
dir.c |
디렉토리 연산 (create, mkdir, unlink, rmdir, rename) | ovl_create(), ovl_unlink(), ovl_rename() |
readdir.c |
readdir 병합, 디렉토리 캐시 | ovl_iterate(), ovl_dir_cache |
copy_up.c |
copy-up 핵심 로직 | ovl_copy_up(), ovl_copy_up_one(), ovl_copy_up_data() |
util.c |
유틸리티 함수, xattr 조작 | ovl_is_opaquedir(), ovl_set_xattr() |
ovl_entry.h |
핵심 자료구조 정의 | ovl_fs, ovl_inode, ovl_config, ovl_layer |
params.c |
마운트 옵션 파싱 (커널 6.x, 기존 super.c에서 분리) | ovl_parse_param(), ovl_fs_context_ops |
export.c |
NFS export 지원 | ovl_export_operations, file handle 인코딩/디코딩 |
# 커널 소스에서 OverlayFS 파일 목록 확인
ls -la fs/overlayfs/
# total 220
# -rw-r--r-- 1 root root 8234 copy_up.c
# -rw-r--r-- 1 root root 9876 dir.c
# -rw-r--r-- 1 root root 4567 export.c
# -rw-r--r-- 1 root root 7890 file.c
# -rw-r--r-- 1 root root 6543 inode.c
# -rw-r--r-- 1 root root 8901 namei.c
# -rw-r--r-- 1 root root 3456 ovl_entry.h
# -rw-r--r-- 1 root root 5678 params.c
# -rw-r--r-- 1 root root 7654 readdir.c
# -rw-r--r-- 1 root root 12345 super.c
# -rw-r--r-- 1 root root 4321 util.c
# OverlayFS 코드 총 라인 수 (약 6,000~8,000줄)
wc -l fs/overlayfs/*.c fs/overlayfs/*.h
# AUFS(약 30,000줄)에 비해 매우 간결
고급 활용 시나리오
컨테이너 라이브 마이그레이션과 OverlayFS
컨테이너를 다른 호스트로 라이브 마이그레이션(CRIU 활용)할 때, OverlayFS 상태도 함께 전송해야 합니다. 이때 고려할 사항:
- upper 레이어 전송: upper 디렉토리의 내용(수정된 파일, whiteout, opaque xattr)을 목적지로 rsync/tar 전송
- lower 레이어 공유: lower(이미지 레이어)는 양쪽 호스트에 동일하게 존재해야 함. 컨테이너 레지스트리에서 pull하거나 공유 스토리지 사용
- inode 일관성:
index=on사용 시 index 디렉토리도 함께 전송해야 하며, 하위 파일시스템의 inode 번호 체계가 달라지면 문제 발생 - workdir 재생성: workdir은 임시 데이터만 포함하므로 빈 디렉토리로 재생성
volatile 마운트된 overlay를 마이그레이션하면, 전송 중 flush되지 않은 데이터가 손실될 수 있습니다. 마이그레이션 전 sync를 수행하거나 volatile 옵션 없이 재마운트하세요.
tmpfs 기반 빌드 캐시
CI/CD 환경에서 빌드 성능을 극대화하기 위해, upper/work를 tmpfs에 배치하고 lower에 빌드 의존성 캐시를 사용하는 패턴입니다:
# tmpfs에 upper/work 배치 → RAM 기반 I/O
mount -t tmpfs -o size=4G tmpfs /tmp/build-upper
mkdir -p /tmp/build-upper/{diff,work}
# 빌드 의존성이 설치된 lower 레이어 (캐시됨, 재사용)
# + tmpfs upper (매 빌드마다 초기화)
mount -t overlay overlay \
-o lowerdir=/var/cache/build-base,\
upperdir=/tmp/build-upper/diff,\
workdir=/tmp/build-upper/work,volatile \
/workspace
# 빌드 수행 (읽기: 캐시에서 → 빠름, 쓰기: RAM → 매우 빠름)
make -C /workspace -j$(nproc)
# 결과물 추출 후 정리
cp -r /workspace/output /artifacts/
umount /workspace
umount /tmp/build-upper
스냅샷과 롤백(Rollback)
OverlayFS를 활용하면 간단한 파일시스템 스냅샷/롤백 메커니즘을 구현할 수 있습니다:
# 스냅샷 생성: 현재 upper를 보존하고 새 upper로 전환
snapshot_create() {
local name=$1
umount /merged
cp -a /upper /snapshots/$name # upper 상태 보존
mount -t overlay overlay \
-o lowerdir=/upper:/lower,upperdir=/upper-new,workdir=/work-new \
/merged
}
# 롤백: 특정 스냅샷 시점으로 복원
snapshot_rollback() {
local name=$1
umount /merged
rm -rf /upper
cp -a /snapshots/$name /upper # 저장된 upper 복원
rm -rf /work && mkdir /work # work 디렉토리 초기화
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work \
/merged
}
OverlayFS 커널 버전별 변화
| 커널 버전 | 변경 사항 |
|---|---|
| 3.18 | OverlayFS mainline 통합 (Miklos Szeredi) |
| 4.0 | 다중 lower 레이어 지원, overlay2 스토리지 드라이버 기반 |
| 4.13 | redirect_dir 옵션 도입 — 디렉토리 rename 지원 |
| 4.19 | metacopy 옵션 도입, SELinux 레이블 지원 개선 |
| 5.6 | volatile 옵션 도입 — fsync 건너뛰기 |
| 5.11 | user namespace 내 비특권 마운트 지원 (userxattr) |
| 5.15 | idmapped mount 지원, 마운트 API 현대화 (fs_context) |
| 6.5 | lowerdir+/datadir+ 새 문법, xattr 기반 whiteout |
| 6.6 | fs-verity digest 통합, composefs 지원, data-only lower 레이어 |
| 6.8+ | 성능 최적화: metacopy 지연 데이터 copy-up 개선, tmpfile 기반 whiteout |
레이어 해석
다중 lower 레이어를 사용하는 환경에서 파일 lookup은 최상위 upper 레이어부터 시작하여 각 lower 레이어를 순차적으로 탐색합니다. 첫 번째로 매칭되는 dentry가 반환되며, 하위 레이어의 동일 경로 파일은 가려집니다(shadowed). whiteout이나 opaque 마커가 존재하면 탐색이 조기에 중단됩니다.
멀티레이어 Lookup 순서
5개 레이어 스택에서의 파일 해석 흐름을 살펴봅니다. lowerdir에 콜론으로 구분된 여러 디렉토리를 지정하면 왼쪽이 더 높은 우선순위를 갖습니다:
Whiteout/Opaque 처리 시각화
whiteout은 character device(0,0)로 lower 파일의 삭제를 표시하며, opaque 디렉토리는 trusted.overlay.opaque=y xattr을 설정하여 하위 레이어의 동일 디렉토리 내용을 완전히 가립니다:
# 다중 lower layer 마운트 (5개 레이어)
mount -t overlay overlay \
-o lowerdir=/layer1:/layer2:/layer3:/layer4,upperdir=/upper,workdir=/work \
/merged
# 커널 6.5+ lowerdir+ 새 문법 (data-only lower 포함)
mount -t overlay overlay \
-o "lowerdir+=/layer1,lowerdir+=/layer2,datadir+=/data-only,upperdir=/upper,workdir=/work" \
/merged
# 각 레이어의 파일 목록 확인
for layer in /layer1 /layer2 /layer3 /layer4; do
echo "=== $layer ==="
find "$layer" -maxdepth 2 -ls
done
# whiteout 생성 확인
rm /merged/file_to_delete.txt
# upper에서 character device whiteout 확인
ls -la /upper/file_to_delete.txt
# c--------- 1 root root 0, 0 ... file_to_delete.txt
# opaque 디렉토리 확인
getfattr -n trusted.overlay.opaque /upper/some_dir
# trusted.overlay.opaque="y"
# 커널 6.5+ xattr 기반 whiteout 확인
getfattr -n trusted.overlay.whiteout /upper/file_to_delete.txt
# trusted.overlay.whiteout="y"
# whiteout 타입별 통계
find /upper -type c -perm 0000 | wc -l # character device whiteout 수
find /upper -exec getfattr -n trusted.overlay.whiteout {} \; 2>/dev/null | grep -c "whiteout"
Copy-Up 내부 메커니즘
copy-up은 lower 레이어의 파일을 수정하려 할 때 upper 레이어로 전체 파일을 복사하는 핵심 메커니즘입니다. 커널 내부에서는 ovl_copy_up_one()이 원자적(atomic) 단계로 이 과정을 수행합니다.
단계별 Copy-Up 흐름
/* copy-up 트리거 조건 — fs/overlayfs/copy_up.c */
/* 다음 VFS 연산이 lower 파일에 대해 호출될 때 copy-up 발생 */
static bool ovl_need_copy_up(struct dentry *dentry, int flags)
{
/* 쓰기 계열: write, truncate, fallocate */
if (flags & (O_WRONLY | O_RDWR | O_TRUNC))
return true;
/* 메타데이터 변경: chmod, chown, utimes, setxattr */
/* → 항상 copy-up (metacopy 비활성 시) */
/* mmap(MAP_SHARED) + PROT_WRITE */
/* → 지연 copy-up: page fault 시 수행 */
return false;
}
/* ovl_copy_up_one — 핵심 copy-up 구현 (간략화) */
static int ovl_copy_up_one(struct dentry *parent,
struct dentry *dentry,
struct path *lowerpath,
struct kstat *stat)
{
struct dentry *temp;
int err;
/* 1단계: workdir에 tmpfile 생성 */
temp = ovl_create_temp(workdir, stat);
if (IS_ERR(temp))
return PTR_ERR(temp);
/* 2단계: 데이터 복사 (reflink 시도 → fallback to read/write) */
err = ovl_copy_up_data(lowerpath, temp, stat->size);
if (err)
goto out_cleanup;
/* 3단계: xattr 복사 */
err = ovl_copy_xattr(lowerpath->dentry, temp);
if (err)
goto out_cleanup;
/* 4단계: 소유권/권한 설정 */
err = ovl_set_attr(temp, stat);
if (err)
goto out_cleanup;
/* 5단계: atomic rename to upper */
err = ovl_install_temp(temp, dentry);
out_cleanup:
if (err)
ovl_cleanup(workdir, temp);
return err;
}
# copy-up 이벤트 실시간 추적
trace-cmd record -e overlayfs:ovl_copy_up -T
# bpftrace로 copy-up 크기/지연 모니터링
bpftrace -e '
kprobe:ovl_copy_up_one {
@start[tid] = nsecs;
}
kretprobe:ovl_copy_up_one /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# copy-up 발생 파일 목록 확인 (upper에 존재 = copy-up 완료)
find /upper -type f -newer /upper -printf "%T+ %s %p\n" | sort -rn
FICLONE ioctl로 CoW 참조만 생성하여 대용량 파일의 copy-up 비용을 거의 0에 가깝게 줄일 수 있습니다.
Metacopy 상세 데이터 레이지 카피
metacopy=on 옵션을 사용하면 chmod, chown 등 메타데이터만 변경하는 연산에서 실제 데이터 복사 없이 메타데이터만 upper에 복사합니다. 데이터는 여전히 lower에서 직접 읽히며, 실제 쓰기가 발생할 때 비로소 전체 copy-up이 수행됩니다.
metacopy vs full copy-up 비교
# metacopy 활성화 마운트
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,metacopy=on \
/merged
# 커널 빌드 옵션으로 기본값 변경
# CONFIG_OVERLAY_FS_METACOPY=y → metacopy=on이 기본
# sysfs에서 현재 기본값 확인
cat /sys/module/overlay/parameters/metacopy
# 모듈 파라미터로 기본값 설정
echo "options overlay metacopy=on" > /etc/modprobe.d/overlay.conf
# metacopy 상태 확인 — 메타데이터만 복사된 파일 식별
getfattr -n trusted.overlay.metacopy /upper/some_file
# trusted.overlay.metacopy="" → 데이터는 아직 lower에 있음
# metacopy 파일의 원본 위치 (origin xattr)
getfattr -n trusted.overlay.origin /upper/some_file
# 바이너리 값 = lower file handle
# upper에서 metacopy 파일 찾기
find /upper -exec getfattr -n trusted.overlay.metacopy {} \; 2>/dev/null
# metacopy 파일에 write → 지연된 full copy-up 발생
echo "trigger full copy-up" >> /merged/some_file
getfattr -n trusted.overlay.metacopy /upper/some_file
# 에러 → xattr 사라짐 (full copy-up 완료)
Redirect_dir과 Xattr 매핑(Mapping)
redirect_dir 옵션은 디렉토리의 rename() 연산을 지원하기 위해 도입되었습니다. OverlayFS에서 디렉토리 rename은 단순한 이름 변경이 아니라, upper와 lower의 경로 매핑을 갱신해야 하는 복잡한 연산입니다.
redirect_dir 동작 원리
디렉토리를 rename하면 커널은 upper의 새 이름 디렉토리에 trusted.overlay.redirect xattr을 설정합니다. 이 xattr의 값은 lower에서의 원래 경로를 가리켜, lookup 시 올바른 lower 디렉토리와 병합할 수 있게 합니다.
# redirect_dir 활성화 마운트
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,redirect_dir=on \
/merged
# 디렉토리 rename 수행
mv /merged/old_name /merged/new_name
# upper에서 redirect xattr 확인
getfattr -n trusted.overlay.redirect /upper/new_name
# trusted.overlay.redirect="/old_name"
# → lookup("new_name") 시:
# upper: new_name/ (실제 디렉토리)
# lower: old_name/ (redirect가 가리키는 원래 경로)와 병합
# redirect_dir 옵션 값별 동작
# on : rename 허용, redirect xattr 설정
# off : rename 시 EXDEV 에러
# follow : 기존 redirect는 인식하되, 새 rename 시 EXDEV
# nofollow: redirect xattr 무시 (보안상 신뢰할 수 없는 lower 사용 시)
# redirect xattr의 절대/상대 경로 규칙
# "/" 시작 = 절대 경로 (lower 루트 기준)
getfattr -d -m trusted.overlay /upper/moved_dir
# trusted.overlay.redirect="/original/path/to/dir"
# "/" 없음 = 같은 부모 디렉토리 내 상대 경로
# trusted.overlay.redirect="old_dirname"
# 중첩 rename의 redirect 체인 확인
find /upper -exec getfattr -n trusted.overlay.redirect {} \; 2>/dev/null
# 깊은 중첩은 lookup 비용 증가 → 가능하면 flat 구조 유지
# redirect_dir 없이 디렉토리 rename 시도
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,redirect_dir=off \
/merged
mv /merged/dir_a /merged/dir_b
# mv: cannot move '/merged/dir_a' to '/merged/dir_b': Invalid cross-device link
redirect_dir=nofollow는 신뢰할 수 없는 lower 레이어에서 악의적인 redirect xattr을 통한 심볼릭 링크 공격 유사 행위를 차단합니다. 프로덕션 환경에서는 lower 소스를 신뢰할 수 있을 때만 follow 또는 on을 사용하세요.
컨테이너 런타임 실전
Docker, Podman, containerd 등 주요 컨테이너 런타임은 OverlayFS를 기본 스토리지 드라이버로 사용합니다. 각 이미지 레이어가 lower 디렉토리로 스택되고, 컨테이너의 쓰기 가능한 레이어가 upper로 마운트됩니다.
Docker overlay2 레이어 구조
# Docker overlay2 스토리지 드라이버 설정
cat /etc/docker/daemon.json
{
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true",
"overlay2.size=20G"
]
}
# 현재 스토리지 드라이버 확인
docker info --format '{{.Driver}}'
# overlay2
# 컨테이너의 overlay 마운트 정보 확인
docker inspect "$(docker ps -q | head -1)" \
--format '{{.GraphDriver.Data.MergedDir}}'
# overlay2 디렉토리 구조 탐색
ls /var/lib/docker/overlay2/
# <layer-id>/ — 각 레이어
# l/ — 심볼릭 링크 디렉토리
# Podman fuse-overlayfs (rootless 모드)
podman info --format '{{.Store.GraphDriverName}}'
# overlay (rootless: fuse-overlayfs 또는 kernel native)
# Podman 저장소 경로
podman info --format '{{.Store.GraphRoot}}'
# ~/.local/share/containers/storage
# rootless overlay 설정 (/etc/containers/storage.conf)
cat <<'EOF'
[storage]
driver = "overlay"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
mountopt = "nodev,metacopy=on"
EOF
# Podman 컨테이너의 overlay 마운트 확인
podman mount "$(podman ps -q | head -1)"
mount | grep overlay
# containerd snapshotter 설정 (/etc/containerd/config.toml)
[plugins."io.containerd.grpc.v1.cri".containerd]
snapshotter = "overlayfs"
[plugins."io.containerd.snapshotter.v1.overlayfs"]
root_path = "/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs"
# upperdir_label = true # SELinux 레이블 활성화
# containerd snapshot 목록 확인
ctr snapshot ls
# KEY PARENT KIND
# sha256:abc123... - Committed
# sha256:def456... abc123 Active
# nerdctl로 컨테이너 레이어 확인
nerdctl inspect "$(nerdctl ps -q | head -1)" | jq '.[0].GraphDriver'
# 레이어 디버깅: 어떤 레이어에서 파일이 왔는지 확인
CONTAINER_ID=$(docker ps -q | head -1)
OVERLAY_INFO=$(docker inspect "$CONTAINER_ID" --format '{{json .GraphDriver.Data}}')
# upper에서 변경된 파일 목록
UPPERDIR=$(echo "$OVERLAY_INFO" | jq -r '.UpperDir')
find "$UPPERDIR" -type f | head -20
# 각 lower 레이어 크기 확인
LOWERDIR=$(echo "$OVERLAY_INFO" | jq -r '.LowerDir')
echo "$LOWERDIR" | tr ':' '\n' | while read layer; do
echo "$layer: $(du -sh "$layer" 2>/dev/null | cut -f1)"
done
# whiteout 파일 찾기 (삭제된 파일 추적)
find "$UPPERDIR" -type c -perm 0000 -ls
NFS Export 지원
OverlayFS의 NFS export 기능(nfs_export=on)은 overlay 마운트를 NFS 서버를 통해 원격 클라이언트에 공유할 수 있게 합니다. 이를 위해 커널은 overlay 파일에 대해 유일하고 지속적인(persistent) file handle을 인코딩해야 합니다.
NFS Export 흐름
# NFS export를 위한 overlay 마운트
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,nfs_export=on,index=on \
/export/overlay
# /etc/exports에 overlay 마운트 포인트 추가
echo "/export/overlay *(rw,sync,no_subtree_check,fsid=100)" >> /etc/exports
# fsid 지정 필수 — overlay에는 고유 device number가 없으므로
exportfs -ra
# NFS export 상태 확인
exportfs -v
cat /proc/fs/nfsd/export_features
# file handle 디코딩 디버깅
# 커널 디버그 메시지로 fh 인코딩/디코딩 추적
echo "file 8" > /sys/kernel/debug/dynamic_debug/control
echo "module overlay +p" > /sys/kernel/debug/dynamic_debug/control
# overlay inode의 file handle 확인 (name_to_handle_at)
python3 -c "
import os, struct
fd = os.open('/export/overlay/test_file', os.O_RDONLY)
# file handle은 커널 내부에서 인코딩됨
# NFS 클라이언트가 수신하는 fh = OVL_FH_MAGIC + type + lower/upper fh
os.close(fd)
print('overlay file handle 인코딩 완료')
"
# NFS 마운트 후 stale handle 발생 시 진단
# copy-up으로 인한 fh 변경 → index가 이를 추적
dmesg | grep -i "overlay.*fh\|stale"
nfs_export=on은 index 디렉토리 유지로 인한 디스크 오버헤드와 lookup 비용이 발생합니다. 또한 upper/lower 파일시스템 모두 exportfs 콜백을 구현해야 합니다 (ext4, xfs 등 지원, tmpfs 미지원).
Inode/Dentry 연산 내부
OverlayFS는 VFS 계층에 자체 inode와 dentry를 등록하고, 실제 연산은 상위(upper) 또는 하위(lower) 파일시스템의 실제 inode에 위임(dispatch)합니다. 이 이중 구조를 이해하면 inode 번호 불일치, st_dev 문제 등을 진단할 수 있습니다.
Inode 스택 구조
/* struct ovl_inode — fs/overlayfs/ovl_entry.h */
struct ovl_inode {
union {
struct ovl_dir_cache *cache; /* 디렉토리: readdir 캐시 */
struct inode *lowerdata; /* 파일: metacopy 데이터 inode */
};
const char *redirect; /* redirect_dir 경로 */
u64 version; /* readdir 캐시 유효성 */
unsigned long flags; /* OVL_ 플래그 비트 */
struct inode vfs_inode; /* VFS inode (임베디드) */
struct dentry *__upperdentry; /* upper dentry (NULL=lower only) */
struct ovl_entry *oe; /* lower 스택 + numlower */
/* lock: 동시 copy-up 방지 */
struct mutex lock;
};
/* struct ovl_entry — lower 레이어 스택 */
struct ovl_entry {
unsigned numlower; /* lower 레이어 수 */
struct ovl_path lowerstack[]; /* 가변 길이 lower 배열 */
};
struct ovl_path {
struct ovl_layer *layer; /* 레이어 메타데이터 */
struct dentry *dentry; /* 실제 lower dentry */
};
struct ovl_layer {
struct vfsmount *mnt; /* lower 파일시스템 마운트 */
struct ovl_sb *fs; /* super_block 정보 */
int idx; /* 레이어 인덱스 (0=상위) */
int fsid; /* 파일시스템 ID */
};
/* dentry 연산 디스패치 — 읽기는 lower, 쓰기는 upper */
static struct dentry *ovl_lookup(struct inode *dir,
struct dentry *dentry,
unsigned int flags)
{
struct ovl_lookup_data d;
struct dentry *ret;
/* 1. upper lookup */
if (upperdentry) {
d.upperdentry = lookup_one_len(name, upperdir, len);
if (!IS_ERR(d.upperdentry) && d.upperdentry->d_inode)
goto found_upper;
}
/* 2. whiteout 체크 — upper에 whiteout 있으면 탐색 중단 */
if (ovl_is_whiteout(d.upperdentry))
goto not_found;
/* 3. lower stack 순회 */
for (i = 0; i < oe->numlower; i++) {
struct dentry *ldentry;
ldentry = lookup_one_len(name, lowerstack[i].dentry, len);
/* opaque 디렉토리 → 하위 lower 탐색 중단 */
if (ovl_is_opaquedir(ldentry))
break;
}
/* ... ovl_dentry_init() → inode 생성 ... */
}
Volatile 마운트와 tmpfs 활용
volatile 마운트 옵션(커널 5.6+)은 fsync, syncfs 등의 동기화 연산을 건너뛰어 쓰기 성능을 대폭 향상시킵니다. 데이터 지속성이 불필요한 CI/CD 파이프라인(Pipeline)이나 빌드 캐시에 적합합니다.
volatile 의미와 제약
volatile 모드에서는 workdir에 incompat/volatile 마커 파일이 생성됩니다. 이 마커가 존재하는 upper 디렉토리는 크래시 후 데이터 일관성을 보장하지 않으므로, 다음 마운트 시 반드시 정리하거나 새로 생성해야 합니다.
# volatile 마운트 — fsync 건너뛰기
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,volatile \
/merged
# volatile 마커 확인
ls /work/incompat/volatile
# 빈 파일이 존재 → volatile 모드 활성
# volatile + metacopy 조합 (최대 성능)
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,volatile,metacopy=on \
/merged
# 비정상 종료 후 복구 — upper/work 재생성 필요
umount /merged 2>/dev/null
rm -rf /upper /work
mkdir -p /upper /work
# → 새 마운트 시 깨끗한 상태
# tmpfs를 upper layer로 사용하는 패턴
# 메모리 기반 upper → 극한 I/O 성능, 컨테이너 종료 시 자동 정리
# tmpfs 마운트
mount -t tmpfs -o size=2G tmpfs /tmp/overlay-upper
mkdir -p /tmp/overlay-upper/{diff,work}
# tmpfs upper + persistent lower 조합
mount -t overlay overlay \
-o lowerdir=/persistent/lower,upperdir=/tmp/overlay-upper/diff,workdir=/tmp/overlay-upper/work \
/merged
# 사용 사례: CI/CD 빌드 환경
# - lower: 빌드 도구 + 소스 (불변)
# - upper: tmpfs (빌드 산출물, 임시)
# - 빌드 완료 후 결과만 추출, overlay 해제
# Docker에서 tmpfs upper 유사 구현
docker run --tmpfs /tmp:size=1G \
--mount type=tmpfs,destination=/var/cache \
ubuntu:22.04 make -j$(nproc)
volatile 모드는 크래시 시 upper 디렉토리의 일관성을 보장하지 않습니다. 프로덕션 데이터가 아닌 일회성(disposable) 데이터에만 사용하세요. Docker 컨테이너의 ephemeral 레이어에 적합합니다.
fs-verity 연동
커널 6.6부터 OverlayFS는 lower 레이어 파일의 fs-verity digest를 upper의 xattr에 기록하여 파일 무결성을 검증할 수 있습니다. 이 기능은 composefs와 결합하여 컨테이너 이미지의 위변조를 런타임에 탐지합니다.
fs-verity 무결성 검증 흐름
# 1. lower 파일에 fs-verity 활성화 (빌드 시점)
fsverity enable /lower/important_binary
fsverity measure /lower/important_binary
# sha256:a1b2c3d4e5f6... /lower/important_binary
# 2. verity digest를 overlay xattr에 설정
setfattr -n trusted.overlay.verity \
-v "sha256:a1b2c3d4e5f6..." \
/upper/important_binary
# 3. verity 검증 마운트
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,metacopy=on,verity=require \
/merged
# verity=require: digest 불일치 시 파일 접근 차단 (EIO)
# verity=on: digest 있으면 검증, 없으면 통과
# composefs를 활용한 컨테이너 이미지 검증
# mkcomposefs: erofs 메타데이터 이미지 생성
mkcomposefs --digest-store=/var/cache/composefs/objects \
/path/to/rootfs \
/path/to/image.cfs
# composefs 마운트 (overlay + erofs + fs-verity)
mount -t composefs /path/to/image.cfs \
-o basedir=/var/cache/composefs/objects,verity \
/mnt/container-rootfs
# 무결성 검증 실패 시 커널 로그
dmesg | grep "overlayfs.*verity"
# overlayfs: failed to verify metacopy upper ...
벤치마크와 성능 튜닝
OverlayFS 성능은 lower 레이어 수, copy-up 빈도, 하위 파일시스템 종류, metacopy/volatile 옵션에 따라 크게 달라집니다. 체계적인 벤치마크와 병목(Bottleneck) 분석을 통해 최적의 설정을 도출할 수 있습니다.
Copy-Up 병목 분석
# fio를 활용한 overlay 벤치마크
# 1. 순차 읽기 (lower 레이어 성능 측정)
fio --name=seq-read --directory=/merged \
--rw=read --bs=128k --size=1G --numjobs=4 \
--time_based --runtime=30 --group_reporting
# 2. 랜덤 쓰기 (copy-up + upper 쓰기 측정)
fio --name=rand-write --directory=/merged \
--rw=randwrite --bs=4k --size=256M --numjobs=4 \
--time_based --runtime=30 --group_reporting \
--fsync_on_close=1
# 3. 혼합 워크로드 (컨테이너 실제 패턴 시뮬레이션)
fio --name=mixed --directory=/merged \
--rw=randrw --rwmixread=70 --bs=4k --size=512M \
--numjobs=8 --time_based --runtime=60 \
--group_reporting --lat_percentiles=1
# 4. copy-up 집중 테스트 (lower 파일 대량 수정)
# lower에 1000개 파일 생성 후 overlay에서 전부 수정
for i in $(seq 1 1000); do
dd if=/dev/urandom of=/lower/file_$i bs=1M count=1 2>/dev/null
done
time for i in $(seq 1 1000); do
echo "modify" >> /merged/file_$i # 각 파일 copy-up 트리거
done
# perf trace로 copy-up 분석
perf trace -e 'overlayfs:*' -a --duration 10
# bpftrace: copy-up 소요 시간 히스토그램
bpftrace -e '
kprobe:ovl_copy_up_flags {
@start[tid] = nsecs;
}
kretprobe:ovl_copy_up_flags /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
@copy_up_us = hist($dur);
@total_count = count();
delete(@start[tid]);
}'
# funcgraph로 copy-up 내부 호출 체인 분석
trace-cmd record -p function_graph \
-g ovl_copy_up_one -O funcgraph-proc \
-- touch /merged/lower_only_file
trace-cmd report | head -50
# 성능 튜닝 체크리스트
# 1. 하위 파일시스템 최적화
mount -o noatime,nodiratime /dev/sda1 /upper-fs # atime 비활성화
# 2. XFS reflink 확인 (copy-up 최적화)
xfs_info /upper-fs | grep reflink
# reflink=1 → 활성화됨
# 3. inode/dentry 캐시 확인
cat /proc/sys/fs/dentry-state
slabtop -o | grep -E "dentry|inode"
# 4. 마운트 옵션 최적화 조합
mount -t overlay overlay \
-o lowerdir=/lower,upperdir=/upper,workdir=/work,metacopy=on,volatile \
/merged
# 5. lower 레이어 수 확인 (너무 많으면 lookup 지연)
cat /proc/self/mountinfo | grep overlay | \
sed 's/.*lowerdir=\([^,]*\).*/\1/' | tr ':' '\n' | wc -l
# 6. overlay inode 수 모니터링
cat /proc/sys/fs/inode-nr
grep overlay /proc/filesystems
metacopy=on,volatile 조합이 순수 ext4 대비 쓰기 지연을 50~80% 줄일 수 있습니다. 단, volatile은 크래시 안전성을 포기하므로 빌드 캐시처럼 재생성 가능한 데이터에만 사용하세요.
| 시나리오 | 권장 옵션 | 예상 효과 |
|---|---|---|
| 컨테이너 프로덕션 | metacopy=on,index=on |
메타데이터 변경 시 copy-up 절감, inode 안정성 |
| CI/CD 빌드 | metacopy=on,volatile |
fsync 제거 + 메타데이터 최적화, 최대 80% 쓰기 절감 |
| NFS 공유 overlay | nfs_export=on,index=on,redirect_dir=on |
persistent file handle, 디렉토리 rename 지원 |
| 보안 중시 환경 | metacopy=on,verity=require |
lower 레이어 무결성 검증, 위변조 탐지 |
| 개발/테스트 | volatile + tmpfs upper |
메모리 기반 쓰기, 테스트 후 자동 정리 |
참고 링크
- Kernel Documentation — overlayfs.rst — 커널 공식 OverlayFS 문서로 마운트 옵션, copy-up 동작, 제한사항 등을 포괄적으로 설명합니다
- 커널 소스: fs/overlayfs/ — OverlayFS 커널 구현의 전체 소스 코드를 Bootlin Elixir에서 탐색할 수 있습니다
- 커널 소스: fs/overlayfs/super.c — 마운트 옵션 파싱과 superblock 초기화 로직이 구현되어 있습니다
- 커널 소스: fs/overlayfs/copy_up.c — copy-up 메커니즘의 핵심 구현을 포함합니다
- 커널 소스: fs/overlayfs/inode.c — inode 연산과 metacopy, xattr 처리가 구현되어 있습니다
- 커널 소스: fs/overlayfs/dir.c — 디렉토리 연산(생성, 삭제, rename)과 whiteout 처리를 담당합니다
- LWN: Unioning file systems — plans, issues, and solutions — union 파일시스템 설계 초기 논의와 OverlayFS가 채택된 배경을 다룹니다
- LWN: Overlayfs — Miklos Szeredi가 OverlayFS를 처음 제안한 2011년 LWN 기사입니다
- LWN: Debating overlayfs — OverlayFS의 메인라인 병합 전 커널 커뮤니티의 심층 토론을 정리합니다
- LWN: Overlayfs lands in 3.18 — 커널 3.18에 OverlayFS가 공식 병합된 과정을 설명합니다
- LWN: Metacopy — an overlayfs optimization — metacopy 기능의 설계 동기와 copy-up 최적화 원리를 다룹니다
- LWN: Composefs and security for ostree and container images — composefs와 OverlayFS data-only lower 레이어의 통합 활용을 설명합니다
- Docker Documentation — Use the OverlayFS storage driver — Docker에서 overlay2 스토리지 드라이버를 구성하고 최적화하는 방법을 안내합니다
- Docker Documentation — Select a storage driver — 다양한 스토리지 드라이버의 비교와 overlay2 권장 사유를 설명합니다
- containers/storage (GitHub) — Podman/Buildah가 사용하는 스토리지 라이브러리로 overlay 드라이버 구현을 포함합니다
- containerd — Overlayfs Snapshotter — containerd의 OverlayFS 스냅샷터 구성과 동작 방식을 설명합니다
- mount(8) — overlay 파일시스템 타입의 마운트 옵션 참조 매뉴얼입니다
- composefs (GitHub) — OverlayFS 기반 content-addressed 이미지 포맷의 구현 저장소입니다
fs/overlayfs/ovl_entry.h— ovl_entry, ovl_fs 등 핵심 데이터 구조체가 정의되어 있습니다fs/overlayfs/readdir.c— 디렉토리 병합 읽기(merge readdir)와 whiteout 필터링 로직을 구현합니다fs/overlayfs/export.c— NFS export 지원을 위한 file handle 인코딩/디코딩을 처리합니다Documentation/filesystems/overlayfs.rst— 커널 소스 트리 내 OverlayFS 공식 문서 원본입니다
관련 문서
OverlayFS와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.