Skip to main content

Поясніть поняття 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, що й попередня збірка. Переупорядкуй інструкції так, щоб дешеві, часто мінливі лежали зверху; повільні, стабільні нижче.

Швидкий приклад

bash
$ 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 у дії

bash
# Всередині 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

dockerfile
# НЕПРАВИЛЬНО: 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

dockerfile
# НЕПРАВИЛЬНО: 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, щоб клонувати велике репо, а потім видаляти

dockerfile
# НЕПРАВИЛЬНО: репо живе у шарі назавжди 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

bash
$ 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

bash
# Перший 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-шар

dockerfile
# Стейдж 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 --from=build /app/dist /usr/share/nginx/html

Фінальний image це nginx:1.27-alpine (≈20 MB) плюс твої зібрані статичні файли. 600 MB build-стейдж викинуто, він був потрібен для виробництва артефакту, не для запуску. Це шарова модель, використана на повну: збираєш важко, шиппиш легко.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів