OverlayFS 심화
Linux OverlayFS(union mount 파일시스템)의 레이어 구조, copy-up, whiteout, redirect, metacopy 메커니즘부터 커널 내부 구현과 컨테이너 스토리지 드라이버 활용까지 종합적으로 다룹니다.
fs/overlayfs/
OverlayFS 개요
Union Mount 개념
Union mount는 여러 디렉토리 트리를 하나의 통합된 디렉토리 트리로 병합하여 보여주는 파일시스템 기법입니다. 사용자에게는 하나의 디렉토리로 보이지만, 실제로는 여러 레이어(layer)가 겹쳐져 있으며, 각 레이어의 파일이 우선순위에 따라 합쳐져 표시됩니다.
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에 포함되지 않아 배포판마다 별도 패치가 필요했습니다. OverlayFS는 AUFS보다 단순한 설계를 채택하여 mainline 통합에 성공했으며, 현재 컨테이너 생태계의 사실상 표준 스토리지 드라이버입니다.
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와의 연결 추적 |
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 건너뛰기. 비정상 종료 시 데이터 무결성 보장 안 됨 |
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()— 확장 속성 변경truncate()— 파일 크기 변경link()— 하드 링크 생성 (원본이 lower에 있을 경우)rename()— lower 파일/디렉토리 이름 변경
Copy-Up 과정
Copy-up은 다음 단계로 수행됩니다:
- 상위 레이어에 부모 디렉토리 구조를 재현합니다 (필요 시 중간 디렉토리도 copy-up)
workdir에 임시 파일을 생성합니다- 하위 레이어 파일의 데이터를 임시 파일로 복사합니다
- 파일의 메타데이터(소유자, 권한, 타임스탬프, xattr)를 복사합니다
trusted.overlay.originxattr에 하위 파일의 file handle을 기록합니다- 임시 파일을
upperdir의 최종 위치로rename()합니다 (원자적 교체)
/* 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()를 사용하여 커널 공간 내에서 zero-copy에 가까운 데이터 전송을 수행합니다. 대용량 파일의 경우에도 사용자 공간을 거치지 않습니다.
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.* 네임스페이스 형태(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 파일시스템과 상호작용하는 주요 콜백입니다:
/* 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 디렉토리 구조
# Docker overlay2 스토리지 드라이버의 디렉토리 구조
/var/lib/docker/overlay2/
├── l/ # 레이어별 심볼릭 링크 (짧은 경로)
│ ├── ABCDEF1234 -> ../abc123.../diff
│ ├── GHIJKL5678 -> ../def456.../diff
│ └── ...
├── abc123.../ # 레이어 1 (base image)
│ ├── diff/ # 실제 파일 내용
│ │ ├── bin/
│ │ ├── etc/
│ │ ├── usr/
│ │ └── ...
│ ├── link # 이 레이어의 짧은 이름 (l/ 디렉토리의 링크명)
│ └── committed # 이 레이어가 완료됨을 표시
├── def456.../ # 레이어 2
│ ├── diff/ # 이 레이어에서 추가/변경된 파일만
│ ├── link
│ ├── lower # 하위 레이어 참조 (l/ABCDEF1234)
│ ├── work/ # OverlayFS work 디렉토리
│ └── merged/ # 마운트 포인트 (컨테이너 실행 시)
└── ...
# 실행 중인 컨테이너의 mount 정보 확인
docker inspect --format '{{.GraphDriver.Data}}' <container_id>
# map[LowerDir:/var/lib/docker/overlay2/.../diff:...
# MergedDir:/var/lib/docker/overlay2/.../merged
# UpperDir:/var/lib/docker/overlay2/.../diff
# WorkDir:/var/lib/docker/overlay2/.../work]
# mount 명령으로 확인
mount | grep overlay
# overlay on /var/lib/docker/overlay2/.../merged type overlay
# (rw,relatime,lowerdir=l/ABCDEF1234:l/GHIJKL5678,
# upperdir=.../diff,workdir=.../work,xino=on)
컨테이너 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개 (마운트 문자열 페이지 크기 제한) | 실제로는 경로 길이에 의존 |
| 5.11+ | 최대 500개 | OVL_MAX_STACK 상수로 제한 |
| 6.5+ | 데이터 전용 lower 별도 지정 가능 | lowerdir+ 및 datadir+ 새 문법 |
/* fs/overlayfs/super.c */
#define OVL_MAX_STACK 500
/* 커널 6.5+: 새로운 lowerdir 문법 */
/* lowerdir=/path1:/path2 → 기존 문법 (콜론 구분) */
/* lowerdir+=/path1,lowerdir+=/path2 → 새 문법 (반복 옵션) */
/* datadir+=/path → 데이터 전용 lower (lookup에 참여하지 않음) */
중첩 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. 오버헤드 거의 없음 |
| 읽기 (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를 다른 프로세스에 전달하면, 수신 프로세스는 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