OverlayFS

OverlayFS의 핵심은 읽기 전용(Read-Only) lower 계층과 쓰기 가능한 upper 계층을 하나의 마운트(Mount)로 통합하는 것입니다. 본 문서는 lookup 병합 규칙, copy-up 트리거 조건, whiteout과 opaque 디렉터리 의미, redirect_dir/metacopy 옵션의 차이, rename/하드링크/권한 처리 제약, Docker·containerd 오버레이(Overlay) 드라이버의 실제 I/O 패턴, 성능 저하와 일관성 이슈 대응법까지 상세히 설명합니다.

전제 조건: VFS네트워크 스택(Network Stack) 문서를 먼저 읽으세요. 원격/합성 파일시스템(Filesystem)은 로컬 경로와 다른 지연(Latency)·일관성 모델을 가지므로, 경계 계층(커널/유저/원격)을 먼저 구분해야 합니다.
일상 비유: 이 주제는 여러 창고를 겹쳐 보이는 통합 창고와 비슷합니다. 실제 보관 위치와 사용자에게 보이는 경로가 다를 수 있어, 조회 경로와 갱신 반영 시점을 분리해서 보는 것이 핵심입니다.

핵심 요약

  • 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의 레이어 구조를 활용하여 이미지를 효율적으로 저장·공유합니다.

단계별 이해

  1. Union Mount 개념 이해 — upper/lower/merged 디렉토리의 관계를 파악합니다.

    mount -t overlay에서 lowerdir, upperdir, workdir, merged가 각각 어떤 역할을 하는지 확인합니다.

  2. 파일 읽기/쓰기 경로 추적 — merged view에서 파일 접근 시 upper/lower 중 어디에서 데이터를 가져오는지 따라갑니다.

    upper에 파일이 있으면 upper 우선, 없으면 lower에서 읽으며, 쓰기 시 copy-up이 발생하는 조건을 확인합니다.

  3. Copy-Up과 Whiteout 동작 분석 — lower 파일의 수정·삭제 시 내부 메커니즘을 이해합니다.

    copy-up은 파일 전체를 upper로 복사하므로 큰 파일에서는 지연이 발생할 수 있고, whiteout은 character device(0,0)로 구현됩니다.

  4. 컨테이너 활용 사례 탐구 — Docker가 OverlayFS를 스토리지 드라이버로 사용하는 방식을 살펴봅니다.

    베이스 이미지 → 중간 레이어 → 컨테이너 쓰기 레이어 구조에서 레이어 공유와 COW(Copy-on-Write) 효과를 확인합니다.

  5. 고급 기능과 제약 사항 검토 — redirect_dir, metacopy, index, nfs_export 등 옵션의 효과와 한계를 비교합니다.

    각 옵션은 성능 또는 호환성을 개선하지만, POSIX 시맨틱 완전성과 트레이드오프가 있으므로 사용 환경에 맞게 선택합니다.

관련 표준: Union Filesystem Interface, OverlayFS Documentation (kernel.org) — 유니온 마운트 파일시스템 인터페이스입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
커널 버전: 이 문서는 Linux 커널 6.x 기준으로 작성되었습니다. OverlayFS는 커널 3.18에서 mainline에 통합되었으며, 이후 지속적으로 기능이 추가되고 있습니다. 관련 소스 경로: fs/overlayfs/

OverlayFS 개요

Union Mount 개념

Union mount는 여러 디렉토리 트리를 하나의 통합된 디렉토리 트리로 병합하여 보여주는 파일시스템 기법입니다. 사용자에게는 하나의 디렉토리로 보이지만, 실제로는 여러 레이어(layer)가 겹쳐져 있으며, 각 레이어의 파일이 우선순위(Priority)에 따라 합쳐져 표시됩니다.

OverlayFS는 이 union mount 개념을 구현한 커널 파일시스템으로, 상위 레이어(upper)하위 레이어(lower)를 겹쳐서 병합 뷰(merged)를 제공합니다. 상위 레이어는 읽기/쓰기가 가능하고, 하위 레이어는 읽기 전용입니다.

OverlayFS Union Mount 구조 Merged View (사용자에게 보이는 통합 디렉토리) Upper Layer (RW) 수정/생성/삭제 기록 Lower Layer (RO) 원본 데이터 (변경 불가) Work Directory (임시 작업 공간) mount -t overlay overlay -o lowerdir=...,upperdir=...,workdir=... merged/ upper: file_a (수정됨), file_c (신규) lower: file_a (원본), file_b
OverlayFS는 upper(RW)와 lower(RO) 레이어를 병합하여 하나의 통합 뷰를 제공합니다

역사: 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) 생태계의 사실상 표준 스토리지 드라이버입니다.

팁: OverlayFS는 커널 소스의 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로 이동합니다. 이 방식은 시스템 크래시 시 중간 상태 노출 가능성을 줄이기 위한 설계입니다.

주의: workdirupperdir은 반드시 같은 파일시스템에 위치해야 합니다. 서로 다른 파일시스템에 있으면 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는 다음 순서로 레이어를 탐색합니다:

  1. Upper layer를 먼저 검사합니다
  2. Upper에 없으면 Lower layer를 순서대로 검사합니다 (다중 lower의 경우 왼쪽부터)
  3. 파일이 발견되면 해당 레이어의 dentry를 기반으로 OverlayFS dentry를 구성합니다
  4. 어느 레이어에서도 발견되지 않으면 -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;
}

파일 읽기/쓰기 경로

파일 읽기와 쓰기는 서로 다른 경로를 따릅니다:

/* 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을 유발합니다:

Copy-Up 과정

Copy-up은 다음 단계로 수행됩니다:

  1. 상위 레이어에 부모 디렉토리 구조를 재현합니다 (필요 시 중간 디렉토리도 copy-up)
  2. workdir에 임시 파일을 생성합니다
  3. 하위 레이어 파일의 데이터를 임시 파일로 복사합니다
  4. 파일의 메타데이터(소유자, 권한, 타임스탬프, xattr)를 복사합니다
  5. trusted.overlay.origin xattr에 하위 파일의 file handle을 기록합니다
  6. 임시 파일을 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;
}
Copy-Up과 데이터 복사: copy-up 시 ovl_copy_up_data()splice_direct_to_actor() 또는 copy_file_range()를 사용하여 커널 공간(Kernel Space) 내에서 zero-copy에 가까운 데이터 전송을 수행합니다. 대용량 파일의 경우에도 사용자 공간(User Space)을 거치지 않습니다.

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 디렉토리 방식을 사용합니다:

  1. upper에 같은 이름의 디렉토리를 생성합니다
  2. 해당 디렉토리에 trusted.overlay.opaque xattr을 "y"로 설정합니다
  3. 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 옵션을 사용하면, 파일의 메타데이터(권한, 소유자, 타임스탬프 등)만 변경될 때 데이터까지 전체 복사하지 않습니다. 대신 상위 레이어에 메타데이터만 포함하는 작은 파일을 생성하고, 실제 데이터 읽기는 여전히 하위 레이어에서 수행합니다.

이 최적화는 대용량 파일의 chmodchown 같은 연산에서 극적인 성능 향상을 제공합니다:

연산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;
}
보안 고려: metacopy는 lower 파일의 데이터를 그대로 참조하므로, lower 레이어가 변조될 경우 보안 문제가 발생할 수 있습니다. 커널 6.x에서는 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로 겹쳐 마운트합니다.

Docker overlay2 레이어 스택 Container Layer (upper, RW) 런타임 변경사항 기록 Image Layer 3 (lower, RO) - apt install nginx Image Layer 2 (lower, RO) - COPY app.py Image Layer 1 (lower, RO) - Base: ubuntu:22.04 Merged View (컨테이너 rootfs) mount -t overlay overlay -o lowerdir=layer1:layer2:layer3,upperdir=container,workdir=work merged/ 이미지 레이어는 여러 컨테이너가 공유 (CoW)
Docker overlay2: 이미지 레이어(lower)를 공유하고 컨테이너별 upper 레이어로 변경사항 격리(Isolation)

overlay2 디렉토리 구조

Docker overlay2 디렉토리 구조 /var/lib/docker/overlay2/ l/ ← 짧은 경로 심볼릭 링크 모음 ABCDEF1234 → ../abc123.../diff GHIJKL5678 → ../def456.../diff abc123.../ ← 레이어 1 (base image) diff/ ← 실제 파일 내용 (bin/, etc/, usr/) link ← 이 레이어의 짧은 이름 (ABCDEF1234) committed ← 레이어 완료 표시 def456.../ ← 레이어 2 (컨테이너 쓰기 레이어) diff/ ← 변경/추가된 파일만 (upper) lower ← 하위 레이어 참조 (l/ABCDEF1234) work/ ← OverlayFS 내부 임시 작업 디렉토리 merged/ ← 마운트 포인트 (컨테이너 실행 시) docker inspect 출력 예시 LowerDir: /var/lib/docker/overlay2/abc123.../diff UpperDir: /var/lib/docker/overlay2/def456.../diff WorkDir: /var/lib/docker/overlay2/def456.../work MergedDir: /var/lib/docker/overlay2/def456.../merged mount: overlay on .../merged (lowerdir=l/ABCDEF1234,upperdir=.../diff,workdir=.../work) ※ 실제 컨테이너 실행 시 merged가 컨테이너의 루트 파일시스템으로 노출됨

컨테이너 CoW(Copy-on-Write) 패턴

컨테이너 환경에서 OverlayFS의 CoW 패턴은 다음과 같은 이점을 제공합니다:

다중 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+ 새 문법
OverlayFS lowerdir 문법 (fs/overlayfs/super.c) #define OVL_MAX_STACK 500 ← 최대 lower 레이어 수 (커널 상수) 기존 문법 (~ 커널 6.4) lowerdir=/path1:/path2:/path3 콜론(:)으로 구분, 왼쪽이 상위 우선순위 ※ 경로에 콜론 포함 불가 새 문법 (커널 6.5+) lowerdir+=/path1,lowerdir+=/path2 반복 옵션으로 지정, 경로에 콜론 허용 ※ 더 유연한 경로 지원 데이터 전용 Lower (커널 6.5+) datadir+=/path ← lookup에 참여하지 않는 data-only lower 레이어. 파일 내용만 제공 사용 예: 읽기 전용 데이터 레이어 분리, 메타데이터와 데이터 분산 저장 레이어 우선순위: upper > lowerdir 첫번째 > ... > 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
주의: 중첩 overlay는 lookup 경로가 길어져 성능이 저하될 수 있습니다. 가능하면 다중 lower 레이어를 사용하는 것이 더 효율적입니다.

성능 특성과 튜닝

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 번호를 가질 수 있는데, 이는 findtar 같은 도구가 하드 링크를 잘못 감지하는 문제를 유발합니다.

# 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 디렉토리를 삭제하고 다시 시작해야 합니다.

성능 최적화 팁

제한사항과 주의점

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 파일을 안정적으로 식별할 수 있게 합니다. 그러나 제한사항이 있습니다:

SELinux

SELinux 환경에서 OverlayFS를 사용할 때 주의사항:

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 번호 유지

기타 제한사항

참고: OverlayFS의 제한사항과 알려진 이슈는 커널 소스의 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) write/chmod/chown 등 트리거 발생 이미 upper에 존재하는가? 직접 연산 수행 Yes No 부모 디렉토리가 upper에 존재하는가? No: 부모 먼저 copy-up (재귀) Yes workdir에 임시 파일 생성 (ovl_create_temp) 데이터 복사 (splice/copy_file_range) metacopy=on이면 이 단계 건너뜀 메타데이터 복사 (uid, gid, mode, xattr, timestamps) trusted.overlay.origin xattr 설정 원자적 rename: workdir → upperdir
Copy-up은 부모 디렉토리부터 재귀적으로 처리되며, workdir을 활용한 원자적 교체로 완료됩니다

데이터 복사 경로 선택

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) 경유, 사용자 공간 미경유
팁: btrfs나 XFS(mkfs.xfs -m reflink=1)에서 upper와 lower를 같은 파일시스템에 배치하면, copy-up 시 reflink가 활용되어 대용량 파일도 즉시 완료됩니다. BtrfsXFS 문서에서 reflink 설정을 참고하세요.

Readdir 병합 메커니즘

OverlayFS의 readdir()은 모든 레이어의 디렉토리 항목을 병합하여 중복을 제거하고 whiteout을 반영해야 합니다. 이 과정은 일반 파일시스템의 readdir보다 복잡하고 느립니다.

Readdir 병합 흐름 (ovl_iterate) Upper Layer readdir file_a (수정됨) file_c (신규) file_b (whiteout) Lower Layer readdir file_a (원본) file_b (원본) file_d (원본) ovl_dir_cache RB-tree 기반 캐시 이름으로 중복 검사 두 번째 readdir부터 재사용 병합 로직 (ovl_dir_read_merged) 1. Upper 항목을 캐시에 삽입 (whiteout은 마킹) 2. Lower 항목 순회: 캐시에 이미 있으면 건너뜀 (upper 우선), whiteout 마킹이면 제외 3. opaque 디렉토리이면 해당 lower 이하 전체 무시 병합된 결과 (사용자에게 반환) file_a (upper), file_c (upper), file_d (lower) file_b는 whiteout으로 제외됨
Readdir은 upper를 먼저 읽고, lower 항목 중 중복과 whiteout을 제거하여 병합합니다
/* 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[];                     /* 가변 길이 이름 */
};
주의: 파일 수가 매우 많은 디렉토리(수만 개 이상)에서 readdir 병합은 상당한 메모리와 CPU를 소비합니다. 컨테이너 환경에서 /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;
}
Podman과 rootless 컨테이너: Podman은 기본적으로 rootless 모드에서 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;
}
팁: fs-verity와 metacopy를 함께 사용하려면 lower 파일시스템이 fs-verity를 지원해야 합니다 (ext4, btrfs, f2fs). fsverity enable /lower/file로 lower 파일에 verity를 활성화한 후 overlay를 마운트합니다. 자세한 내용은 ext4 문서의 verity 섹션을 참고하세요.

Composefs: 읽기 전용 이미지 검증

커널 6.6+에서 등장한 composefs는 OverlayFS를 기반으로 하여, content-addressed 이미지 배포에 fs-verity 검증을 통합한 메커니즘입니다. ostree(Fedora/RHEL 계열)와 컨테이너 이미지 배포에서 활용됩니다:

참고: composefs는 OverlayFS의 data-only lower 레이어(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
팁: 컨테이너 빌드 시 어떤 파일이 copy-up을 유발하는지 파악하면 Dockerfile을 최적화할 수 있습니다. 불필요한 chmod/chown을 제거하거나 metacopy=on을 활성화하여 빌드 시간을 단축할 수 있습니다. 관련 도구 활용은 ftraceBPF 문서를 참고하세요.

커널 소스 파일 맵

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 상태도 함께 전송해야 합니다. 이때 고려할 사항:

주의: 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
}
팁: 보다 정교한 스냅샷이 필요하다면, Btrfs의 서브볼륨 스냅샷이나 Device Mapper의 thin provisioning을 OverlayFS와 조합하여 사용할 수 있습니다.

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에 콜론으로 구분된 여러 디렉토리를 지정하면 왼쪽이 더 높은 우선순위를 갖습니다:

5-Layer Stack 파일 Lookup 흐름 Upper Layer (RW) — 최우선 검색 Lower 1 (최상위 lower) Lower 2 Lower 3 Lower 4 (최하위 lower — base image) lookup 순서 file_a ✓ (수정됨 → 반환) file_a (가려짐) file_b ✓ (여기서 발견) whiteout file_c file_c (삭제 표시됨) Lookup 규칙 1. upper에서 발견 → 즉시 반환 2. whiteout 발견 → ENOENT (하위 탐색 중단) 3. opaque dir → 해당 디렉토리 하위만 반환 4. 미발견 → 다음 lower로 진행 주의사항 • d_type 미지원 FS → 정상 동작 불가 • 레이어 수 ↑ → lookup 지연 증가 • lower 변경 → 정합성 보장 불가 • 500+ 레이어: 스택 오버플로 위험
5개 레이어 스택에서 파일 lookup이 upper→lower 순으로 진행되는 흐름

Whiteout/Opaque 처리 시각화

whiteout은 character device(0,0)로 lower 파일의 삭제를 표시하며, opaque 디렉토리는 trusted.overlay.opaque=y xattr을 설정하여 하위 레이어의 동일 디렉토리 내용을 완전히 가립니다:

Whiteout vs Opaque 처리 Whiteout (파일 삭제) upper: file_x = c 0,0 (character device) 차단 (ENOENT) lower: file_x = "실제 데이터" (가려짐) merged에서 file_x → 존재하지 않음 커널 6.5+ xattr 기반 whiteout: trusted.overlay.whiteout="y" → character device 대신 일반 파일 사용 → unprivileged overlay 가능 Opaque (디렉토리 차폐) upper: dir_y/ (xattr: opaque=y) dir_y/new_file 완전 차폐 lower: dir_y/ (전체 내용 가려짐) dir_y/old_file1, old_file2 ... merged/dir_y/ → new_file만 표시 opaque 생성 시점: 1. rm -rf dir → whiteout 2. mkdir dir → opaque dir 생성
whiteout은 개별 파일 삭제를, opaque는 디렉토리 전체 차폐를 나타냅니다
# 다중 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 흐름

ovl_copy_up_one() 내부 단계 1. tmpfile 생성 workdir에 임시파일 2. 데이터 복사 vfs_copy_file_range 3. xattr 복사 보안 레이블 포함 4. 권한 설정 uid/gid/mode 5. link/rename atomic swap reflink 최적화 경로 (Btrfs/XFS) vfs_copy_file_range() → FICLONE ioctl → CoW 참조만 생성 → 데이터 복사 비용 ≈ 0 일반 경로 (ext4 등) read() + write() 루프 → 전체 데이터 복사 → 대용량 파일 시 I/O 병목 발생 에러 처리와 원자성 보장 • 단계 1~4 실패: tmpfile 삭제 → lower 원본 유지 (부작용 없음) • 단계 5 실패: workdir의 orphan 파일 → 다음 마운트 시 자동 정리 (work/incompat/volatile) • 크래시 복구: workdir에 잔존 파일 → ovl_cleanup() → 마운트 시 경고 로그 출력
copy-up은 workdir에서 임시 파일을 조립한 후 atomic rename으로 upper에 배치합니다
/* 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
최적화 팁: Btrfs나 XFS(reflink 지원)를 하위 파일시스템으로 사용하면, copy-up 시 FICLONE ioctl로 CoW 참조만 생성하여 대용량 파일의 copy-up 비용을 거의 0에 가깝게 줄일 수 있습니다.

Metacopy 상세 데이터 레이지 카피

metacopy=on 옵션을 사용하면 chmod, chown 등 메타데이터만 변경하는 연산에서 실제 데이터 복사 없이 메타데이터만 upper에 복사합니다. 데이터는 여전히 lower에서 직접 읽히며, 실제 쓰기가 발생할 때 비로소 전체 copy-up이 수행됩니다.

metacopy vs full copy-up 비교

Metacopy vs Full Copy-Up Full Copy-Up (metacopy=off) chmod 644 file (1GB) upper: file (1GB 전체 복사) 메타데이터 + 전체 데이터 = 1GB I/O 소요 시간: ~3초 (HDD) 디스크 사용량: +1GB 후속 read: upper에서 직접 Metacopy (metacopy=on) chmod 644 file (1GB) upper: file (메타데이터만, ~4KB) xattr: trusted.overlay.metacopy="" 설정 소요 시간: <1ms 디스크 사용량: +4KB 후속 read: lower에서 (데이터 지연 복사) write 시 → full copy-up 트리거
metacopy=on은 메타데이터 변경 시 데이터 복사를 지연시켜 I/O와 디스크 사용을 절감합니다
# 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 완료)
주의: metacopy는 lower 레이어의 데이터 무결성에 의존합니다. lower가 변경되거나 손상되면 metacopy 파일의 읽기 시 오류가 발생합니다. 컨테이너 이미지 레이어처럼 불변(immutable) lower에서만 사용하는 것이 안전합니다.

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 스토리지 드라이버 Container Layer (upperdir, RW) /var/lib/docker/overlay2/<container-id>/diff Image Layer 3 (최상위 lower) — 앱 바이너리 설치 Image Layer 2 (lower) — 패키지 설치 (apt-get) Image Layer 1 (base lower) — ubuntu:22.04 rootfs link 파일 각 레이어의 짧은 ID 심볼릭 → mount 인자 길이 제한 우회 lower 파일 l/LINK3:l/LINK2 :l/LINK1 → lowerdir 경로 구성에 사용 Merged View → 컨테이너 rootfs (/) 이미지 레이어 공유 같은 이미지의 컨테이너 100개 → lower 1벌 + upper 100개 제한사항 mount 인자 최대 길이: PAGE_SIZE (4KB) → link 심볼릭으로 경로 축약
Docker overlay2는 이미지 레이어를 lower로, 컨테이너 writable 레이어를 upper로 마운트합니다
# 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 흐름

OverlayFS NFS Export File Handle 흐름 NFS Client open("/mnt/file") knfsd fh_compose() ovl_encode_fh() overlay file handle = type + upper/lower fh NFS Client stale handle → reopen knfsd fh_verify() ovl_fh_to_dentry() fh → upper or lower dentry index로 copy-up 추적 Index Dir lower fh → upper 매핑 유지 copy-up 후에도 handle 유효 nfs_export=on 요구사항 • index=on 자동 활성화 • upper/lower 모두 NFS export 가능한 FS 필요 • redirect_dir=on 권장 • 성능: index 유지 오버헤드 존재
NFS export 시 overlay file handle은 상위/하위 레이어의 file handle을 인코딩하여 구성됩니다
# 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 스택 구조

OverlayFS Inode 스택 구조 VFS Layer struct inode → i_op = ovl_file_inode_operations struct ovl_inode (overlay 고유) __upperdentry → upper dentry (RW) oe (ovl_entry) → lower stack 배열 redirect → lower 원래 경로 flags → OVL_UPPERDATA, OVL_WHITEOUTS Upper Real Inode ext4/xfs/btrfs inode (쓰기 대상) Lower Real Inode squashfs/ext4 inode (읽기 전용) stat() 호출 시: st_ino = ovl_inode 번호 (index=on이면 persistent), st_dev = overlay pseudo dev copy-up 전후 st_ino 변경 가능 → 애플리케이션 호환성 문제 발생 가능
overlay inode는 VFS와 실제 파일시스템 사이의 가상 계층으로 동작합니다
/* 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 무결성 검증 흐름

OverlayFS + fs-verity 무결성 검증 빌드 시점 (이미지 생성) 1. lower 파일에 fs-verity 활성화 2. digest를 upper xattr에 기록 런타임 (마운트 후) 3. open() → xattr digest vs 파일 digest 비교 4. 불일치 → EIO (접근 차단) xattr 저장 형식 trusted.overlay.verity = sha256:a1b2c3d4... (digest hex) metacopy xattr과 함께 저장 위변조 탐지 시 lower 파일 변경/교체 → digest 불일치 → -EIO 반환 + 커널 로그 경고 → 컨테이너 이미지 오염 실시간 탐지 composefs (커널 6.6+) — overlay + erofs + fs-verity erofs 이미지(메타데이터) + 개별 blob 파일(데이터/fs-verity) → 전체 이미지 무결성 보장 + 중복 제거
fs-verity digest를 xattr에 기록하여 lower 레이어 파일의 무결성을 런타임에 검증합니다
# 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 병목 분석

OverlayFS 성능 병목 분석 흐름 성능 저하 감지 copy-up 빈도 높음? Yes 해결책 metacopy=on + reflink No readdir 느림? Yes 해결책 lower 레이어 수 줄이기 fsync 병목? Yes 해결책 volatile 마운트 성능 튜닝 우선순위 1. metacopy=on (chmod/chown 빈번 시) → 2. reflink FS (Btrfs/XFS) → 3. volatile (CI/CD) → 4. lower 레이어 수 축소 → 5. tmpfs upper 측정 도구: perf trace -e 'overlayfs:*' | bpftrace copy-up 지연 | fio 혼합 워크로드 | /proc/self/mountinfo
성능 병목의 원인에 따라 적절한 최적화 옵션을 선택합니다
# 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 메모리 기반 쓰기, 테스트 후 자동 정리

참고 링크

OverlayFS와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.