Поясніть поняття Docker layers (шари) та Union File System.
Docker layers і Union File System це те, як Docker перетворює послідовність Dockerfile-інструкцій на один runnable image, тримаючи використання диску і час білду розумними. Уся історія image-кешування стоїть на цьому дизайні.
Теорія
TL;DR
- Кожна Dockerfile-інструкція зазвичай дає один шар: read-only diff файлової системи після виконання інструкції.
- Image це впорядкований стек шарів + config blob. Шари незмінні і ідентифіковані SHA256-hash контенту.
- Union File System (OverlayFS у сучасному Docker) зливає ці read-only шари плюс один writable-шар у єдину в'юху файлової системи, що бачить container.
- Шари дедуплікуються: десять image, що поділяють
python:3.13, тримають цю основу на диску один раз. - Запис всередині запущеного container іде у writable-шар через copy-on-write (CoW). Зникає при видаленні container, persistent-стан належить у volume.
- Build cache спрацьовує, коли інструкція має ті самі inputs, що й попередня збірка. Переупорядкуй інструкції так, щоб дешеві, часто мінливі лежали зверху; повільні, стабільні нижче.
Швидкий приклад
$ docker history nginx:1.27-alpine
IMAGE CREATED CREATED BY SIZE
4f06b3e2c0c1 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemo… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 2 weeks ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 2 weeks ago /bin/sh -c set -x && addgroup -g 101 -S… 8.94MB
<missing> 2 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.27.4 0B
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:abcd1234… 7.79MBКожен рядок це один шар. Alpine-основа (ADD file:...) знизу; nginx-специфічні шари стекаються зверху. Зроби pull nginx:1.27-alpine і node:22-alpine, і Alpine-основа поділяється, скачана раз, збережена раз.
Що таке шар насправді
Шар це tarball, що містить:
- Файли, додані або змінені інструкцією.
- Для видалень: спеціальний whiteout-файл (
.wh.<name>), що каже union FS «сховай файл від нижніх шарів».
Тож якщо RUN apt-get install vim додає /usr/bin/vim, цей файл лягає у tar шару. Якщо пізніший RUN rm /usr/bin/vim його видаляє, у тому шарі з'являється whiteout .wh.vim, але реальний файл все ще на диску у попередньому шарі. Розмір image включає все, що ти колись додав, навіть якщо потім видалив.
Шари content-addressed: їхня ідентичність це SHA256 tarball'а. Той самий tarball = той самий шар = збережено один раз.
Union File System (OverlayFS)
Docker за роки підтримував кілька union FS-драйверів (AUFS, btrfs, devicemapper, zfs, OverlayFS). На сучасному Linux (kernel 4.x+) дефолтом є OverlayFS, він швидкий, у самому kernel, добре підтримуваний.
OverlayFS об'єднує чотири директорії в одну точку монтування:
- lowerdir одна або кілька read-only директорій (твої шари image, стекнуті).
- upperdir одна read-write директорія (writable-шар container).
- workdir scratch-директорія, що kernel використовує внутрішньо.
- merged єдина в'юха, що container бачить як
/.
+------------------+
| merged/ | <- що бачить container
+------------------+
↑ ↑ ↑
| | |
+--------+ +--------+ +--------+
|upperdir| |lowerdir| |lowerdir|
| (RW) | |layer N | |layer 1 |
+--------+ +--------+ +--------+Читання спочатку перевіряє upper-шар, провалюється до нижніх шарів. Записи йдуть у upperdir. Модифікація файлу з нижнього шару тригерить copy-up: файл копіюється у upperdir, потім модифікується там. Оригінал у нижньому шарі не зачеплений.
Copy-on-write у дії
# Всередині container на основі alpine:
/ # cat /etc/hostname
a3f9d2b8c1e4
# Цей файл у alpine-шарі (нижній, RO)
/ # echo new-name > /etc/hostname
# Тепер /etc/hostname у writable-шарі (upper).
# Копія у alpine-шарі не змінилася, інші container з
# того самого image все ще бачать оригінал.Copy-up на файл. Модифікація одного байта 100 MB файлу спочатку копіює всі 100 MB у writable-шар, тому «бази у writable-шарі» працюють погано. Використовуй volume.
Build cache і перевикористання шарів
Коли docker build виконує інструкцію, він рахує cache key з:
- Digest попереднього шару (щоб ланцюг був детермінованим).
- Самої інструкції (текст
RUN/COPY). - Для
COPYіADD: digest файлів, що копіюються. - Для
RUN: лише рядка команди. Docker НЕ перевіряє, що команда робить; та сама строка = cache hit, навіть якщоapt-getзавантажив би нові версії.
Якщо cache key збігається з попереднім білдом, Docker перевикористовує існуючий шар. Якщо ні, виконує інструкцію і створює новий шар. Як тільки крок промахнувся повз кеш, кожен крок після нього також промах.
Оптимізація порядку Dockerfile під cache hits
# НЕПРАВИЛЬНО: source копіюється перед встановленням deps
FROM node:22-alpine
WORKDIR /app
COPY . . # будь-яка зміна коду інвалідує все нижче
RUN npm ci --omit=dev # перезапускається на кожну зміну коду
# ПРАВИЛЬНО: deps встановлено перед копіюванням source
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./ # міняється тільки при зміні deps
RUN npm ci --omit=dev # кешовано, поки package.json не міняється
COPY . . # міняється при зміні source, тільки це перезапускаєтьсяДля типового Node-застосунку зі стабільними deps це перетворює 60-секундний rebuild на 2-секундний. Кешований шар npm ci перевикористовується, поки package.json і package-lock.json без змін.
Наслідки для розміру image
# НЕПРАВИЛЬНО: cleanup у окремому RUN не допомагає
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Шар 2 все ще містить apt-кеш.
# Шар 3 лише додає whiteout, кеш на диску.
# ПРАВИЛЬНО: cleanup у тому самому RUN, що і install
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# Один шар, без кеш-файлів всередині.Whiteout ховають файли від union-в'юхи; не видаляють їх з попередніх шарів. Cleanup має статися у тому самому RUN, що створив сміття.
Типові помилки
Додати файл в одному шарі, видалити в іншому, очікувати менший image
Не працює. 200 MB файл, доданий у шарі 4 і видалений у шарі 5, дає 200 MB image, не нуль. Whiteout лише ховає; байти все ще там. Використовуй multi-stage білди, коли треба тримати щось під час білду, але не у фінальному image.
Перезбирати npm install на кожну зміну коду
Класичний симптом: білди займають 60 секунд навіть для однорядкової зміни. Фікс: install перед копіюванням source. Lock-файл має лягти у свій шар, install виконується проти нього, потім source копіюється зверху.
Модифікувати змонтовані файли з нижнього шару, очікуючи безкоштовності
Перший запис у файл з нижнього шару тригерить copy-up всього файлу. Для маленького config-файлу нормально. Для multi-GB файлу бази повільно і безглуздо, використовуй volume, що оминає union FS повністю.
Використовувати RUN, щоб клонувати велике репо, а потім видаляти
# НЕПРАВИЛЬНО: репо живе у шарі назавжди
RUN git clone https://github.com/large/repo.git /tmp/r && \
cp /tmp/r/binary /usr/local/bin && \
rm -rf /tmp/r
# Шар містить репо + бінар; rm лише додає whiteout.
# Чекай, насправді це В одному RUN, тому cleanup ОК.Неочевидний нюанс: якщо все це у одному RUN, шар це знімок у КІНЦІ команди, після rm. Тому такий патерн ОК. Помилка трапляється, коли кожен крок свій RUN.
Зберігати шари image на повільному диску
OverlayFS швидкий, але обмежений сховищем. Старти container на network-mounted Docker root або повільному USB відчуваються жахливо. Тримай /var/lib/docker на тому самому швидкому SSD, що і твій kernel.
Реальне застосування
- Multi-stage білди канонічний спосіб тримати build-tools поза фінальним image. Stage 1 з компіляторами, stage 2 лише з бінарем; 1.5 GB build-image стає 30 MB runtime image.
- Distroless і scratch image беруть multi-stage ідею далі. Фінальний стейдж це
FROM scratchабоFROM gcr.io/distroless/base, лишаючи лише твій бінар на диску. Без shell, без package manager, near-zero поверхня атаки. - BuildKit cache mounts
RUN --mount=type=cache,target=/root/.cache/pip ...тримає build-cache між білдами без запікання у шар. Шар лишається чистим; кеш живе поза image. - Layer-aware registry ECR, GCR, GHCR всі дедуплікують blob'и на рівні registry. Push двох image, що поділяють 90 відсотків шарів, заливає лише нові 10 відсотків.
Питання для поглиблення
Q: Чому б не зробити один великий шар на все?
A: Втратиш кеш. З одним шаром будь-яка зміна перезбирає весь image. З багатьма шарами тільки шари нижче зміни перевикористовуються. Trade-off в overhead на шар (кожен додає bookkeeping); сучасна best practice це приблизно один логічний крок на шар.
Q: Docker layers і Git commits схожі?
A: Концептуально схожі (впорядковані diff, що можна стекати), механічно різні. Шари це tarball'и файлової системи; Git зберігає об'єкти, дедупльовані по content hash. Обидва content-addressed і незмінні. OCI image spec насправді запозичив ідеї прямо з того, як працює Git.
Q: Що таке docker history --no-trunc <image>?
A: Показує кожен шар image з повною інструкцією, що його продукувала. Найкорисніша команда для розуміння, чому image такий великий і де основна вага. Парь з dive для інтерактивного inspector'а шарів.
Q: Чому Alpine дає менші image, ніж Debian або Ubuntu?
A: Alpine використовує musl замість glibc і busybox замість GNU coreutils. Базовий image приблизно 7 MB проти 30+ MB для Debian slim. Caveat: musl може викликати ледь помітні compat-проблеми з бінарями, що очікують поведінки glibc; використовуй Alpine, коли контролюєш бінарі, Debian-slim, коли ні.
Q: (Senior) Як BuildKit робить білди швидшими за legacy-builder?
A: Кількома шляхами. Паралельне виконання стейджів (стейджі multi-stage без залежностей білдяться паралельно). Розумніші cache key (COPY інвалідує лише при реальній зміні файлів, не на mtime директорії). Cache mounts, що персистять між білдами без роздування шарів. Frontend-синтаксис (# syntax=docker/dockerfile:1.7) дає новим Dockerfile-фічам шиппити без апгрейду daemon. Варто вмикати всюди зараз; це дефолт на Docker 23+.
Приклади
Інспекція розмірів шарів через docker history
$ docker history --no-trunc node:22-alpine | awk '{print $7, $8}' | head -10
74.4MB /bin/sh -c addgroup -g 1000 node
80.6MB /bin/sh -c apk add --no-cache python3 ...
5.2MB /bin/sh -c #(nop) COPY file:abc...
0B /bin/sh -c #(nop) WORKDIR /home/nodeРядок apk add це 80 MB. Це шар, який атакувати першим, якщо треба менший image (розглянь тоншу основу або --no-cache).
Демонстрація dedup шарів через docker pull
# Перший pull: завантажує все
$ docker pull node:22-alpine
22-alpine: Pulling from library/node
9824c27679d3: Pull complete # alpine-основа
f52e5f1a8a45: Pull complete # node-бінарі
ca7239f1a5a6: Pull complete # node-сетап
# Другий pull: ділить alpine-основу
$ docker pull python:3.13-alpine
3.13-alpine: Pulling from library/python
9824c27679d3: Already exists # ТА Ж alpine-основа, не перезавантажується
8b1d5c8d2e7f: Pull complete # python-бінарі9824c27679d3 це Alpine-основа. Два різних image її поділяють, скачана раз, збережена раз на диску. Рядок Already exists це OverlayFS dedup у дії.
Multi-stage, щоб скинути build-шар
# Стейдж 1: повний Node toolchain (~600 MB)
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Стейдж 2: крихітний runtime (~20 MB)
FROM nginx:1.27-alpine
COPY /app/dist /usr/share/nginx/htmlФінальний image це nginx:1.27-alpine (≈20 MB) плюс твої зібрані статичні файли. 600 MB build-стейдж викинуто, він був потрібен для виробництва артефакту, не для запуску. Це шарова модель, використана на повну: збираєш важко, шиппиш легко.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів