OverlayFS: union mount и слои Docker¶
OverlayFS — это union mount файловая система: несколько каталогов на разных носителях склеиваются в один логический вид. Запись идёт в один отдельный слой, не трогая остальные. Именно это свойство сделало OverlayFS стандартным storage driver для Docker, containerd и Podman: образ контейнера хранится один раз, а каждый запущенный контейнер получает свой писательский слой поверх него.
Изначально union mount в Linux реализовывала aufs (Another Union FS), но её патчи так и не приняли в mainline ядро. OverlayFS написана Miklos Szeredi (автор FUSE), вошла в ядро в Linux 3.18 (2014) и быстро вытеснила aufs.
Что такое union mount¶
Union mount берёт несколько каталогов и показывает их как один:
/lower1 /lower2 /upper
┌──────────┐ ┌──────────┐ ┌──────────┐
│ a.txt │ │ b.txt │ │ c.txt │
│ doc.pdf │ │ doc.pdf │ │ b.txt │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└──────────────────────┼──────────────────────┘
▼
/merged (overlay)
┌──────────────┐
│ a.txt │ ← из lower1
│ b.txt │ ← из upper (перекрывает lower2)
│ c.txt │ ← из upper
│ doc.pdf │ ← из lower1 (перекрывает lower2)
└──────────────┘
Lookup идёт сверху вниз: сначала upper, затем lower-каталоги в порядке перечисления. Первое совпадение выигрывает. Запись и удаление меняют только upper — lower остаётся неизменным, что и позволяет шарить его между процессами и контейнерами.
Терминология¶
OverlayFS оперирует четырьмя ролями каталогов:
| Каталог | Назначение | Доступ |
|---|---|---|
lowerdir |
read-only слой (или цепочка слоёв) | только чтение |
upperdir |
writable слой | чтение и запись |
workdir |
служебный каталог для атомарных операций | внутреннее ядро |
merged |
точка монтирования, видимый результат | чтение и запись |
lowerdir — нижний слой. Можно перечислить несколько через : — тогда они образуют стек, где левый перекрывает
правые. Левый ближе к upper, правый — самый «древний».
upperdir — единственный слой, в который OverlayFS пишет. Все изменения, создания и удаления складируются здесь.
workdir — каталог в той же файловой системе, что и upperdir. Ядро использует его для подготовки файлов при copy-up
и whiteout — операции должны быть атомарны через rename(2), а атомарный rename работает только в пределах одной ФС.
Workdir должен быть пустым на момент монтирования и не должен пересекаться с upperdir.
merged — точка монтирования, через которую пользователь видит объединённый результат.
Mount синтаксис¶
Монтирование вручную:
mount -t overlay overlay \
-o lowerdir=/lower1:/lower2:/lower3,upperdir=/upper,workdir=/work \
/merged
Что важно:
lowerdir=A:B:C— A перекрывает B, B перекрывает C. Цепочка читается слева направо.upperdirиworkdirопциональны: без них получится read-only overlay (можно объединить только lower-слои).workdirобязан быть на той же ФС, что иupperdir.mergedне должен совпадать ни с одним из lower/upper/workdir.
Размонтирование — обычный umount /merged. Lower и upper при этом не пропадают: они продолжают существовать как
обычные каталоги.
Lookup: как файл находится¶
При обращении к файлу через merged ядро ищет его сначала в upper, потом в lowerdir-слоях по порядку:
read /merged/foo.txt
│
▼
┌─────────────────────────────────────────────────┐
│ upper/foo.txt существует? │
└──────┬──────────────────────────────────┬───────┘
│ да │ нет
▼ ▼
возвращаем upper/foo.txt ┌───────────────────────────────┐
│ lower1/foo.txt существует? │
└────┬──────────────────────┬───┘
│ да │ нет
▼ ▼
возвращаем lower1/foo.txt ...
(дальше lower2, lower3)
Если файл не найден ни в одном слое — ENOENT. Если в upper лежит whiteout (см. ниже) — поиск тоже завершается с
ENOENT, даже если файл есть в lower.
Copy-up¶
Файлы в lower нельзя менять: они read-only. Когда пользователь открывает lower-файл на запись или вызывает chmod,
chown, truncate, OverlayFS делает copy-up — копирует файл целиком в upper и дальше работает с копией.
write /merged/big.bin
│
▼
┌────────────────────────────────────────────────────┐
│ big.bin лежит в lower → нужно copy-up │
└────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 1. создать копию в workdir │
│ (атомарность операции) │
├──────────────────────────────────┤
│ 2. скопировать содержимое │
│ lower/big.bin → workdir/tmp │
├──────────────────────────────────┤
│ 3. перенести метаданные: │
│ mode, uid/gid, xattrs, time │
├──────────────────────────────────┤
│ 4. rename(workdir/tmp, │
│ upperdir/big.bin) │
├──────────────────────────────────┤
│ 5. выполнить write поверх │
│ upperdir/big.bin │
└──────────────────────────────────┘
Copy-up — основной источник задержек в OverlayFS. Первая запись в 10-гигабайтный файл из lower означает копирование всех 10 GB на диск (или хотя бы в page cache), даже если меняется один байт.
Granularity copy-up — весь файл целиком. OverlayFS не умеет копировать «по странице» или хранить дельты на блочном уровне — для этого существуют CoW-файловые системы (btrfs, ZFS), но это другой подход.
С Linux 4.19 появился metadata-only copy-up: при операциях, меняющих только метаданные (chmod, chown,
utimensat), содержимое файла не копируется — в upper создаётся пустой stub-файл с обновлёнными метаданными и xattr,
указывающим на оригинал в lower. Контент читается из lower напрямую. Первая же запись данных триггерит полный copy-up.
Опция включается через redirect_dir=on,metacopy=on.
Whiteouts: удаление файлов из lower¶
Удалить файл из lower физически невозможно — lower read-only. Чтобы пользователь видел «удаление», OverlayFS пишет в upper специальный маркер — whiteout. В классической OverlayFS whiteout — это character device с major=0, minor=0:
rm /merged/foo.txt
│
▼
┌────────────────────────────────────────────┐
│ foo.txt существует только в lower │
└────────────────────────┬───────────────────┘
▼
mknod(upper/foo.txt, S_IFCHR, makedev(0,0))
│
▼
┌────────────────────────────────┐
│ /upper │
│ foo.txt (char dev 0,0) │ ← whiteout
└────────────────────────────────┘
Lookup /merged/foo.txt:
upper/foo.txt → char dev 0,0 → возвращаем ENOENT,
не идём в lower
Альтернативная форма — расширенный атрибут trusted.overlay.whiteout на обычном пустом файле, появилась для совместимости
с ФС, где mknod запрещён (например, в user namespace). Включается опцией userxattr (Linux 5.11+).
Opaque directories¶
Что делать, если пользователь делает rm -rf каталога, у которого есть содержимое в lower? Поставить whiteout на каждый
файл изнутри — затратно и не работает для файлов, которых пользователь не видел. Решение — пометить каталог в upper как
opaque (непрозрачный):
Когда merged/dir помечен как opaque:
│
▼
┌─────────────────────────────────────────────────┐
│ readdir(/merged/dir) │
│ игнорирует все lower/dir/* │
│ показывает только upper/dir/* │
└─────────────────────────────────────────────────┘
OverlayFS автоматически ставит opaque, когда пользователь:
- Удаляет каталог из lower (
rm -rf /merged/dir, затемmkdir /merged/dir). - Переименовывает каталог в lower при включённой опции
redirect_dir.
Без opaque на свежесозданном пустом каталоге через merged всё ещё было бы видно содержимое старого lower/dir.
Docker и слои образов¶
Docker использует OverlayFS (драйвер overlay2) для построения файловой системы контейнера из слоёв образа. Каждая
инструкция RUN, COPY или ADD в Dockerfile создаёт новый слой — каталог с дельтой относительно предыдущего:
FROM debian:12 # слой 1: базовый rootfs
RUN apt-get update # слой 2: обновлённый /var/lib/apt
RUN apt-get install -y nginx # слой 3: nginx + зависимости
COPY app.conf /etc/nginx/ # слой 4: конфиг
CMD ["nginx", "-g", "daemon off;"]
При запуске контейнера Docker монтирует overlay из этих слоёв плюс новый upper:
docker run nginx-image
│
▼
┌──────────────────────────────────────────────────────────┐
│ │
│ /var/lib/docker/overlay2/ │
│ │
│ layer4/diff ← /etc/nginx/app.conf │
│ layer3/diff ← nginx и зависимости │
│ layer2/diff ← apt update │
│ layer1/diff ← debian:12 rootfs │
│ │
│ container-xxx/diff ← writable upper для контейнера │
│ container-xxx/work ← workdir │
│ container-xxx/merged ← rootfs контейнера │
│ │
└──────────────────────────────────────────────────────────┘
Mount overlay:
lowerdir = layer4/diff : layer3/diff : layer2/diff : layer1/diff
upperdir = container-xxx/diff
workdir = container-xxx/work
merged = container-xxx/merged
Стек lowerdir строится снизу вверх (от старого слоя к новому), но при mount-команде указывается сверху вниз: левый —
самый верхний (последняя инструкция Dockerfile), правый — самый нижний (FROM-образ). Это соответствует семантике
OverlayFS: левый перекрывает правые.
Получаем ровно ту экономию, ради которой union mount и придуман:
- Образ хранится в overlay2 один раз.
- Сто контейнеров из одного образа добавляют только сто маленьких upperdir.
- Чтение nginx-бинарника всеми контейнерами идёт через один и тот же inode в page cache.
Посмотреть текущие overlay-mount'ы:
Посмотреть структуру слоёв образа:
Лимиты и нюансы¶
Глубина стека. Раньше OverlayFS ограничивала число lowerdir в 500, в современных ядрах — 500 на один mount и 128 в
случае nested overlay. Docker сжимает образы при сборке (docker build --squash), чтобы не упереться в лимит.
Поведение rename(2) через слои. Переименование файла из lower в новое место в merged по умолчанию делает copy-up
плюс whiteout — это дорого. Опция redirect_dir=on позволяет переименовать каталог без копирования содержимого, записав
в upper xattr trusted.overlay.redirect с путём оригинала.
inode numbers. Два процесса, открывшие один и тот же lower-файл из разных контейнеров, могут получить разные
st_ino (зависит от опции xino и того, был ли copy-up). Программы, считающие inode уникальными идентификаторами в
пределах процесса, на этом ломаются. Опция xino=on стабилизирует inode-номера, кодируя слой в верхние биты.
fsync. fsync файла после copy-up синхронизирует upper, но не гарантирует ничего про lower (которому и так синхрон
не нужен — он read-only).
Page cache. Lower-файл и его копия в upper после copy-up имеют разные inode и разные записи в page cache. Память, потраченная на кеш lower-файла до copy-up, не освобождается автоматически.
Альтернативы¶
| Решение | Подход | Особенности |
|---|---|---|
aufs |
union mount | deprecated, не в mainline ядре |
btrfs subvol |
snapshot на уровне ФС | CoW на блочном уровне, быстрые snapshots |
zfs clone |
snapshot + writable clone | CoW + integrity checks + RAID, не в mainline |
device-mapper |
thin-provisioned LVM | блочный CoW, использовался Docker до overlay2 |
fuse-overlayfs |
userspace-реализация поверх FUSE | для rootless контейнеров без CAP_SYS_ADMIN |
В современном Linux выбор почти всегда падает на OverlayFS: она в mainline, не требует специальной нижней ФС (работает поверх ext4, XFS, btrfs), хорошо протестирована и быстра благодаря работе в kernel space.
Диагностика¶
Проверить тип драйвера Docker:
Посмотреть слои конкретного контейнера:
Whiteouts и opaque в upper:
# Найти whiteouts (char dev 0,0):
find /var/lib/docker/overlay2/<id>/diff -type c
# Найти opaque-каталоги:
getfattr -R -d -m trusted.overlay /var/lib/docker/overlay2/<id>/diff
Связанные темы¶
- Основы файловых систем — VFS, inode, типы файлов; OverlayFS — VFS-уровневая ФС
- Linux namespaces — mount namespace, в котором живёт overlay контейнера
- cgroups: углублённо — ограничение ресурсов запущенного из overlay контейнера
- Блочные и символьные устройства — char device 0,0 в роли whiteout
Источники¶
- overlayfs.rst — kernel.org Documentation
- Docker storage drivers — docs.docker.com
man 5 overlayfs(где доступно)- LWN: "Overlay filesystem"
- LWN: "Metadata-only copy-up for overlayfs"