open atlas
↑ К треку
Docker: контейнеры как система DOCK · 01 · 01

Что такое образ на самом деле: конфиг плюс стек контентно-адресуемых слоёв, а не снапшот машины

Образ — это не tar запущенной машины, а JSON-конфиг плюс упорядоченный список контентно-адресуемых read-only слоёв. Конфиг несёт env, entrypoint и дайджесты rootfs; рантайм складывает слои и добавляет один записываемый слой. Это убивает модель «образ = снапшот ВМ».

DOCK Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Счёт за реестр не сходился. Платформенная команда держала 40 микросервисов, каждый — Java-образ ~900 МБ, и по арифметике хранилище должно было занимать 40 × 900 МБ = 36 ГБ на поколение тегов. Реестр показывал 4.1 ГБ. Кто-то решил, что дашборд сломан, и завёл тикет. Ответ был в docker history: каждый из этих 40 образов собирался FROM eclipse-temurin:21-jre, и эта база — JRE, userland Debian, CA-сертификаты, ~640 МБ — была одним набором слоёв с одним набором sha256-дайджестов, хранимым однажды. Различался лишь тонкий верхний слой с jar каждого сервиса, ~40 МБ. Багом была сама модель «40 × 900 МБ»: она трактовала каждый образ как независимый снапшот машины. Это не так. Образ — это конфиг-файл, указывающий на стек общих, контентно-адресуемых слоёв, и реестр, демон и диск дедуплицируют по этим дайджестам. Команда рассуждала об образах как о ВМ, а ВМ свой диск не разделяют.

Через десять минут у тебя будет модель, которая объясняет странные счета за хранилище, неожиданные image ID и ephemeral-запись в контейнерах — без единого домысла.

Что ты на самом деле тянешь

Когда ты делаешь docker pull nginx:1.27, ты скачиваешь не машину. По порядку ты получаешь: маленький манифест (JSON, несколько КБ) со списком всего; блоб конфига (JSON) с переменными окружения, entrypoint, рабочей директорией, открытыми портами и упорядоченным списком дайджестов слоёв, образующих корневую ФС; и затем сами слои — gzip-tar-архивы, каждый назван по sha256 своего содержимого. Загляни прямо в него:

$ docker manifest inspect nginx:1.27
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    { "platform": {"architecture":"amd64","os":"linux"}, "digest": "sha256:7f0a...", "size": 2295 },
    { "platform": {"architecture":"arm64","os":"linux"}, "digest": "sha256:e3c1...", "size": 2295 }
  ]
}

Этот верхний объект — индекс: один тег, несколько манифестов под архитектуры. Демон выбирает манифест под твой CPU, затем тянет его конфиг и слои. Для nginx:1.27 на amd64 это 7 слоёв общим объёмом ~67 МБ в сжатом виде; база Debian (~29 МБ) и бинарники nginx доминируют, а прикладной слой на тег часто весит несколько сотен КБ.

Конфиг — это идентичность образа

Почему правка одной переменной окружения даёт совершенно новый image ID, хотя ни один файл на диске не изменился? Потому что image ID — это не хеш слоёв, а хеш конфига. Блоб конфига — та часть, о существовании которой забывают. Это обычный JSON:

{
  "architecture": "amd64", "os": "linux",
  "config": {
    "Env": ["PATH=/usr/local/sbin:/usr/local/bin:...", "NGINX_VERSION=1.27.0"],
    "Entrypoint": ["/docker-entrypoint.sh"],
    "Cmd": ["nginx", "-g", "daemon off;"],
    "ExposedPorts": {"80/tcp": {}}
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:1f3e... ",
      "sha256:8a2c... ",
      "sha256:b91d... "
    ]
  },
  "history": [ { "created_by": "RUN apt-get install ..." } ]
}

Здесь важны две вещи. Во-первых, rootfs.diff_ids — это упорядоченный рецепт файловой системы: сложи эти слои снизу вверх — и получишь корневую директорию контейнера. Во-вторых, sha256 самого этого JSON-конфига — это image ID, тот sha256:..., что ты видишь в docker images --digests. Поменяй одну переменную окружения, пересобери — и байты конфига изменятся, поэтому image ID изменится, даже если каждый дайджест слоя идентичен. Образ — это его конфиг; слои — общий контент, на который конфиг ссылается.

Викторина

Ты пересобираешь образ, изменив в Dockerfile лишь значение ENV, и сборка попадает в кеш на каждом шаге, меняющем ФС. Изменится ли image ID?

Почему это работает

Зачем вообще отделять конфиг от слоёв? Чтобы один набор слоёв обслуживал много образов. Tar базового слоя контентно-адресуется по тому, что внутри него, поэтому два образа, делящие базу, ссылаются на один дайджест блоба, и реестр хранит его однажды. Конфиг — дешёвая, попообразная часть, которая варьируется (разный env, разный entrypoint, другой верхний слой), а дорогие байты ФС под ним дедуплицируются между всеми образами на той же базе. Считай конфиг идентичностью, а слои — общим контентом, и вся модель хранения выводится сама.

Что добавляет контейнер

Образ read-only — каждый слой неизменяем, заморожен своим дайджестом. Как же запущенный контейнер пишет лог-файл? При docker run рантайм складывает read-only слои образа через union-файловую систему (overlay2 на Linux) и добавляет ровно один тонкий записываемый слой сверху. Запись попадает туда по copy-on-write: меняешь /etc/hosts — overlay2 копирует файл вверх, в записываемый слой, и правит копию; нижний read-only слой не меняется. Удалишь контейнер — этот записываемый слой отбрасывается, и именно поэтому важные данные должны жить в томе, а не в контейнере. Образ — неизменяемый стек; контейнер — этот стек плюс один одноразовый слой-черновик.

Поэтому же «это снапшот машины» вводит в заблуждение. Снапшот ВМ — монолитный непрозрачный образ диска, копируемый целиком. OCI-образ — структурированный, адресуемый, общий артефакт: тянешь дельтами (только недостающие слои), делишь базы между сотнями образов и детерминированно пересобираешь из рецепта-конфига.

Викторина

Сорок сервисов собираются FROM одной базы 640 МБ. В реестре и на хосте, гоняющем все сорок, сколько примерно стоит эта база 640 МБ в хранилище?

Вспомните перед уходом
  1. 01
    Перечисли три вида артефактов из pull (манифест/индекс, конфиг, слои) и скажи, что в каждом.
  2. 02
    Объясни, почему image ID может измениться без изменения слоёв и почему сорок образов на общей базе стоят базу однажды.
Итог

Образ — это две вещи, склеенные вместе: конфиг и стек слоёв. Конфиг — обычный JSON: Env, Entrypoint, Cmd, ExposedPorts, рабочая директория, массив history и, главное, rootfs.diff_ids — упорядоченный список дайджестов слоёв, восстанавливающих корневую ФС. sha256 этого JSON-конфига — это image ID, поэтому изменение лишь значения ENV даёт совершенно новый image ID, даже когда каждый шаг ФС попал в кеш: байты конфига сдвинулись, слои — нет. Сами слои — gzip-tar-архивы, каждый назван по sha256 своего содержимого, что делает их контентно-адресуемыми и потому общими: сорок сервисов FROM одной базы 640 МБ ссылаются на одни дайджесты слоёв, поэтому реестр и каждый хост хранят эту базу ровно однажды — арифметика «40 × 900 МБ», сломавшая оценку хранилища, была на деле протёкшей моделью снапшота ВМ. Pull — это манифест (или multi-arch индекс, указывающий на манифесты под платформы), затем конфиг, затем лишь недостающие слои. На рантайме неизменяемый стек получает один тонкий записываемый copy-on-write слой сверху, куда попадает каждая запись и который исчезает при удалении контейнера — причина, по которой устойчивое состояние принадлежит тому. Держи модель точно: конфиг — идентичность, слои — общий контент, записываемый слой (copy-on-write, эфемерный слой поверх read-only стека) — одноразовый черновик, и ничто из этого не снапшот машины. Теперь, когда увидишь число в реестре, которое не сходится с арифметикой, сначала проверь docker history на общие дайджесты базы — дедупликация почти наверняка уже работает.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.