Главная/Ограничения Docker/Урок

19.3. Когда Docker применять не следует

Цели

После этого материала вы сможете:

  • назвать семь классов задач, где контейнеризация не оправдана;
  • для каждого объяснить механизм, а не только вывод;
  • назвать конкретную альтернативу и то, чем за неё платят;
  • отличить «не подходит» от «не умеем готовить»;
  • аргументированно отказаться от контейнеризации в конкретном случае;
  • назвать случай, где отказ был бы ошибкой.

Предварительные знания

Docker на машине курса не установлен. Альтернативы, доступные здесь, измеряются по-настоящему; сравнение с Docker остаётся за читателем.

Ключевые термины

ТерминОбъяснение
bare metalЗапуск без виртуализации и контейнеризации
порталМеханизм доступа изолированного приложения к ресурсам рабочего стола
изменяемое состояние базыОбраз операционной системы, зафиксированный процедурой сертификации
кратковременная задачаРабота, живущая секунды или минуты

Теория

Правило отбора

Каждый случай ниже проходит одну проверку:

Что перестанет работать, если убрать Docker?

Если ответ — «ничего, кроме привычки», случай попадает в этот список. Если ответ называет конкретное свойство, которое пропадёт, — не попадает.

Список не про то, что Docker плох. Он про задачи, где решаемой проблемы нет.

1. Жёсткие требования к задержкам

Механизм. Расходы концентрируются в вводе-выводе и сети (урок 19.1): OverlayFS, veth, NAT, фильтр seccomp. Способы их снять отключают изоляцию — то есть то, ради чего container взят.

К этому добавляется то, чего в container'е нет вовсе или что требует привилегий:

ПриёмСостояние в container'е
Привязка к ядрам процессораТребует --cpuset-cpus и совпадения с настройками хоста
Большие страницы памятиТребует настройки хоста и часто привилегий
Обход ядра при работе с сетьюТребует --privileged и проброса устройств
Настройка планировщика реального времениТребует CAP_SYS_NICE
Изоляция прерыванийНастраивается на хосте, не в container'е

Пять из пяти настроек делаются на хосте. Container их не приносит, а лишь не мешает — если ему разрешить.

Альтернатива. Запуск на bare metal с настройкой хоста; упаковка через пакет дистрибутива или один самодостаточный файл.

Чем платят. Теряется одинаковость окружения между машинами: настройку придётся воспроизводить средствами управления конфигурацией.

2. Приложения с графическим интерфейсом

Механизм. Графическая подсистема работает через сокет сессии пользователя. Чтобы приложение в container'е нарисовало окно, внутрь пробрасывают:

Что пробрасываютЧто это открывает
Сокет X11 или WaylandДоступ к сессии рабочего стола
/dev/driВидеоускоритель
Сокет звукаУстройства воспроизведения и записи
Шину сообщений сессииЗначительную часть рабочего окружения
Каталоги пользователяФайлы пользователя

К концу списка от изоляции остаётся немного, а зависимость от версии драйвера возвращает привязку к хосту (урок 19.1).

Альтернатива. Flatpak, Snap или AppImage. Существенно, что первые два используют те же механизмы ядра — namespaces, seccomp, cgroups, — но добавляют то, чего у Docker нет: порталы. Портал даёт приложению доступ к выбору файла, буферу обмена, камере через посредника, который спрашивает пользователя, вместо проброса сокета целиком.

Чем платят. Форматов несколько, и упаковывать придётся под каждый; для внутренних инструментов это иногда дороже, чем просто установить пакет.

3. Прямой доступ к оборудованию

Механизм. Устройство пробрасывается, а не виртуализируется. Модуль ядра остаётся на хосте, пользовательская часть — в образе, и версии должны совпадать (урок 19.1).

Отсюда: образ, работающий на одной машине, на другой может не запуститься. Главное обещание не выполняется именно там, где его труднее всего проверить.

Альтернатива. Пакет дистрибутива, устанавливающий и пользовательскую часть, и совместимый модуль.

Чем платят. Возвращаются различия между машинами — ровно та задача, ради которой Docker обычно и берут.

Когда это не аргумент. Если весь парк машин одинаков и драйвер на них один, привязка перестаёт быть проблемой. Именно так работают вычислительные кластеры с ускорителями: там контейнеризация оправдана.

4. Однократные и кратковременные скрипты

Механизм. Стоимость запуска складывается из сборки образа, его хранения и передачи. Для работы, длящейся секунды, эта стоимость превышает саму работу.

Дополнительно: чтобы скрипт что-то сделал, ему нужны данные — значит, монтирование каталогов, значит, права и владельцы файлов (раздел 07).

Альтернатива. Виртуальное окружение — и современный инструментарий делает это почти мгновенно. uv run --with пакет script.py создаёт окружение и запускает скрипт одной командой.

Чем платят. Зависимости берутся из системы; на другой машине с другой версией Python результат может отличаться.

5. Простые статические сайты

Механизм. Задача — отдать файлы. Контейнеризация добавляет к ней образ, слои, публикацию порта и обновление базового образа веб-сервера, не убирая ни одной существующей проблемы.

Что действительно нужно статике: раздача с ближайшего узла, кэширование, сертификат, сжатие. Container не даёт ни одного из четырёх.

Альтернатива. Статический хостинг или раздающая сеть; для своего сервера — веб-сервер, установленный из пакета.

Чем платят. Практически ничем. Это самый чистый случай списка.

Когда это не аргумент. Если сайт собирается сложным конвейером с зафиксированными версиями инструментов, контейнеризация сборки оправдана. Не оправдана контейнеризация раздачи.

6. Команда не готова поддерживать инфраструктуру

Механизм. Стоимость постоянная, а не разовая (урок 18.4): хранилище образов, обновление базовых, кэш, сканирование, очистка. Если этим никто не занимается, система приходит в состояние, худшее исходного: образы годовой давности с известными уязвимостями и никого, кто умеет их пересобрать.

Существенно, что это не довод «команда недостаточно квалифицирована». Это вопрос распределения времени: у команды из трёх человек, поддерживающей один сервис, время лучше тратится на сам сервис.

Альтернатива. Пакеты дистрибутива и systemd (урок 19.1), обновления штатным средством системы.

Чем платят. Нет одинаковости окружения между машинами и нет единообразия доставки.

7. Жёсткие требования к прослеживаемости состава

Механизм. Там, где состав системы зафиксирован процедурой и обновления проходят утверждение, контейнеризация добавляет второй путь обновления: пакеты хоста и содержимое образов. Оба должны быть описаны, проверены и утверждены.

Что появляетсяЧто требует
Второй набор пакетов — внутри образовОтдельного учёта и сканирования
Изменяемые тегиЗакрепления по digest (урок 14.3)
Базовые образы от внешнего поставщикаОбоснования доверия (урок 12.7)
Слои с удалённым содержимымПонимания, что удалённое остаётся (урок 12.6)

Альтернатива. Фиксированный образ операционной системы и установка из проверенного репозитория.

Чем платят. Теряются воспроизводимость сборки и SBOM — а они как раз облегчают прослеживаемость.

Честная оговорка. Это единственный пункт списка, где вывод неоднозначен. Контейнеризация здесь и добавляет работу, и даёт средства её выполнять: SBOM, digest, подписи. Решение зависит от того, описана ли процедура под фиксированный образ системы — если да, менять её дороже, чем не вводить containers.

Чего в списке нет

Полезно назвать и обратное — случаи, которые в список не попадают, хотя их туда часто помещают:

СлучайПочему не попадает
«У нас всего один сервис»Одинаковость окружения нужна и одному
«У нас нет DevOps-инженера»См. пункт 6: вопрос времени, а не должности
«Приложение с состоянием»Тома решают задачу; вопрос в эксплуатации (урок 19.1)
«Нам нужна максимальная производительность»Расходов на вычислениях практически нет
«Мы на Windows»Механизм иной, но задача решается

Первая и четвёртая строки встречаются чаще прочих, и обе основаны на неточном представлении о том, где Docker берёт своё.


Внутренний механизм

Почему настройки для низких задержек делаются на хосте

Планировщик, прерывания, большие страницы и привязка к ядрам — свойства ядра, а оно одно на всех (урок 19.2). Container не имеет собственного планировщика и не может настроить чужой.

--cpuset-cpus выглядит исключением, но лишь задаёт cgroup-ограничение поверх настройки хоста: если ядра не изолированы на хосте, на них попадут и другие процессы.

Отсюда общее правило: всё, что относится к поведению ядра, настраивается на хосте, независимо от того, есть container или нет. Контейнеризация в такой задаче не мешает, но и не помогает — при этом расходы на вводе-выводе остаются.

Почему порталы решают задачу, которую проброс сокета не решает

Проброс сокета — решение «всё или ничего»: получив доступ к сокету рабочего стола, приложение может всё, что может пользователь.

Портал — посредник: приложение просит «дай файл», посредник показывает диалог, пользователь выбирает, приложение получает дескриптор одного файла. Доступа к каталогу при этом не появляется.

Разница в том, кто принимает решение: при пробросе — никто, при портале — пользователь в момент запроса.

Docker сокеты рабочего стола пробрасывать умеет, а порталов не имеет — потому что предназначен для серверных задач, где рабочего стола нет.


Команды и примеры

Стоимость запуска кратковременной задачи

bash
mkdir -p /tmp/oneshot && cd /tmp/oneshot

cat > script.py <<'PY'
"""Однократная задача: посчитать строки в файлах."""
from __future__ import annotations

import sys
from pathlib import Path


def main(root: str = ".") -> int:
    total = 0
    for p in sorted(Path(root).rglob("*.py")):
        total += len(p.read_text(encoding="utf-8").splitlines())
    print(f"строк Python: {total}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
PY

echo "═══ альтернатива: виртуальное окружение ═══"
command -v uv >/dev/null && uv --version
python3 --version

echo
echo "── стандартное средство ──"
rm -rf venv-std
/usr/bin/time -f "  создание venv: %e с" python3 -m venv venv-std 2>&1 | tail -1

echo "── uv ──"
rm -rf venv-uv
/usr/bin/time -f "  создание venv: %e с" uv venv venv-uv 2>&1 | tail -1

echo
echo "── запуск задачи ──"
/usr/bin/time -f "  выполнение: %e с" python3 script.py . 2>&1 | tail -2

rm -rf venv-std venv-uv

Ожидаемый вывод (числа зависят от машины):

text
═══ альтернатива: виртуальное окружение ═══
uv 0.11.6 (x86_64-unknown-linux-gnu)
Python 3.12.3

── стандартное средство ──
  создание venv: 5.28 с
── uv ──
  создание venv: 0.13 с

── запуск задачи ──
строк Python: 15
  выполнение: 0.02 с

Эти числа получены на машине курса. Сравнение с Docker здесь не приводится — он не установлен, и придумывать время сборки было бы подлогом.

Правильный способ достроить сравнение — измерить у себя:

bash
# НЕ ВЫПОЛНЯЛОСЬ: Docker на машине курса отсутствует
time docker build -t oneshot .
time docker run --rm -v "$PWD:/work" -w /work oneshot python script.py

Смысл не в конкретных числах, а в порядке величин: создание окружения занимает доли секунды, а сборка образа — обычно десятки секунд при первом запуске. Для задачи, работающей две сотых секунды, вторая величина и есть вся стоимость.

Разбор случаев

bash
cd /tmp/oneshot
cat > cases.py <<'PY'
"""Семь случаев, где контейнеризация не оправдана."""
from __future__ import annotations

import json

# (случай, механизм, альтернатива, чем платят, однозначен ли вывод)
CASES = [
    ("Жёсткие требования к задержкам",
     "расходы на вводе-выводе; настройки ядра делаются на хосте",
     "bare metal + пакет дистрибутива",
     "нет одинаковости окружения между машинами", True),
    ("Приложение с графическим интерфейсом",
     "проброс сокетов рабочего стола снимает изоляцию",
     "Flatpak, Snap, AppImage — те же namespaces плюс порталы",
     "упаковка под несколько форматов", True),
    ("Прямой доступ к оборудованию",
     "passthrough привязывает образ к версии драйвера хоста",
     "пакет дистрибутива с модулем и пользовательской частью",
     "возвращаются различия между машинами", False),
    ("Однократный или кратковременный скрипт",
     "стоимость сборки превышает саму работу",
     "uv run --with, venv, pipx",
     "зависимости берутся из системы", True),
    ("Простой статический сайт",
     "задача — отдать файлы; container не решает ни одной из четырёх задач статики",
     "статический хостинг или веб-сервер из пакета",
     "практически ничем", False),
    ("Команда не поддерживает инфраструктуру",
     "стоимость постоянная: базовые образы, кэш, сканирование, очистка",
     "пакеты дистрибутива и systemd",
     "нет одинаковости и единообразия доставки", True),
    ("Жёсткая прослеживаемость состава",
     "появляется второй путь обновления, требующий утверждения",
     "фиксированный образ системы и проверенный репозиторий",
     "теряются воспроизводимость сборки и SBOM", False),
]

# Случаи, которые в список НЕ попадают, хотя их туда часто помещают
NOT_CASES = [
    ("У нас всего один сервис", "одинаковость окружения нужна и одному"),
    ("У нас нет DevOps-инженера", "вопрос времени команды, а не должности"),
    ("Приложение с состоянием", "тома решают задачу; вопрос в эксплуатации"),
    ("Нужна максимальная производительность",
     "расходов на вычислениях практически нет"),
    ("Мы на Windows", "механизм иной, но задача решается"),
]


def main() -> None:
    print(f"  {'случай':<40} {'механизм':<62}")
    print("  " + "─" * 104)
    for case, mech, _, _, _ in CASES:
        print(f"  {case:<40} {mech:<62}")

    print()
    print(f"  {'случай':<40} {'альтернатива':<46} чем платят")
    print("  " + "─" * 132)
    for case, _, alt, cost, _ in CASES:
        print(f"  {case:<40} {alt:<46} {cost}")

    unclear = [c for c in CASES if not c[4]]
    print()
    print(f"  случаев: {len(CASES)}, из них вывод НЕоднозначен: {len(unclear)}")
    for case, _, _, _, _ in unclear:
        print(f"    · {case}")

    print()
    print(f"  {'НЕ попадает в список':<40} почему")
    print("  " + "─" * 96)
    for case, why in NOT_CASES:
        print(f"  {case:<40} {why}")

    print()
    print("  Правило отбора: что перестанет работать, если убрать Docker?")
    print("  Если ответ — «ничего, кроме привычки», случай в списке.")
    print()
    print(json.dumps({"случаев": len(CASES), "неоднозначных": len(unclear),
                      "не_случаев": len(NOT_CASES)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
PY

echo "═══ семь случаев ═══"
python3 cases.py

cd /tmp && rm -rf /tmp/oneshot

Ожидаемый вывод:

text
═══ семь случаев ═══
  случай                                   механизм
  ────────────────────────────────────────────────────────────────────────────────────────────────────────
  Жёсткие требования к задержкам           расходы на вводе-выводе; настройки ядра делаются на хосте
  Приложение с графическим интерфейсом     проброс сокетов рабочего стола снимает изоляцию
  Прямой доступ к оборудованию             passthrough привязывает образ к версии драйвера хоста
  Однократный или кратковременный скрипт   стоимость сборки превышает саму работу
  Простой статический сайт                 задача — отдать файлы; container не решает ни одной из четырёх задач статики
  Команда не поддерживает инфраструктуру   стоимость постоянная: базовые образы, кэш, сканирование, очистка
  Жёсткая прослеживаемость состава         появляется второй путь обновления, требующий утверждения

  случай                                   альтернатива                                   чем платят
  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Жёсткие требования к задержкам           bare metal + пакет дистрибутива                нет одинаковости окружения между машинами
  Приложение с графическим интерфейсом     Flatpak, Snap, AppImage — те же namespaces плюс порталы  упаковка под несколько форматов
  ...

  случаев: 7, из них вывод НЕоднозначен: 3
    · Прямой доступ к оборудованию
    · Простой статический сайт
    · Жёсткая прослеживаемость состава

  НЕ попадает в список                     почему
  ────────────────────────────────────────────────────────────────────────────────────────────────
  У нас всего один сервис                  одинаковость окружения нужна и одному
  У нас нет DevOps-инженера                вопрос времени команды, а не должности
  Приложение с состоянием                  тома решают задачу; вопрос в эксплуатации
  Нужна максимальная производительность    расходов на вычислениях практически нет
  Мы на Windows                            механизм иной, но задача решается

Три случая из семи помечены как неоднозначные — и это существеннее самого списка. Прямой доступ к оборудованию перестаёт быть доводом, если парк машин одинаков; статический сайт стоит собирать в container'е, но не раздавать из него; прослеживаемость и усложняется контейнеризацией, и получает от неё средства.


Практическое упражнение

Задание. Постройте инструмент выбора и примените его к пяти задачам.

Требования:

  1. Описать каждый случай механизмом, а не выводом.
  2. Для каждого назвать конкретную альтернативу и то, чем за неё платят.
  3. Отметить случаи, где вывод неоднозначен, и объяснить, от чего он зависит.
  4. Применить к пяти задачам; хотя бы одна должна получить вывод «Docker оправдан».
  5. Измерить стоимость альтернативы по-настоящему там, где это возможно.
  6. Отдельно перечислить доводы, которые звучат похоже, но в список не попадают.
  7. Отделить измеренное от предполагаемого.

Подсказки

Подсказка 1

Инструмент, отвечающий «не нужен Docker» на любую задачу, бесполезен так же, как инструмент, отвечающий «нужен». Требование 4 — проверка на отрицательном примере.

Подсказка 2

/usr/bin/time -f "%e" даёт время выполнения одной командой, без обвязки.

Подсказка 3

Неоднозначность обычно возникает, когда вывод зависит от условия, которого нет в самой задаче: одинаков ли парк машин, собирается ли артефакт, описана ли процедура.

Решение

Показать решение
bash
mkdir -p /tmp/nodocker && cd /tmp/nodocker

cat > cases.py <<'PY'
"""Выбор между контейнеризацией и альтернативой.

Инструмент отвечает не «да/нет», а «какой случай это, какой механизм
за ним стоит, какая альтернатива и чем за неё платят». Вывод «Docker
оправдан» — полноправный результат, а не отсутствие результата.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path


class Case:
    """Случай, где контейнеризация не оправдана."""

    def __init__(self, key: str, name: str, mechanism: str,
                 alternative: str, price: str, certain: bool,
                 depends_on: str = "") -> None:
        self.key = key
        self.name = name
        self.mechanism = mechanism
        self.alternative = alternative
        self.price = price
        self.certain = certain
        self.depends_on = depends_on

    def as_dict(self) -> dict[str, object]:
        return {
            "ключ": self.key, "случай": self.name, "механизм": self.mechanism,
            "альтернатива": self.alternative, "чем_платят": self.price,
            "однозначен": self.certain, "зависит_от": self.depends_on,
        }


CASES = [
    Case("latency", "Жёсткие требования к задержкам",
         "расходы на вводе-выводе и сети; настройки ядра делаются на хосте",
         "bare metal с настройкой хоста, пакет дистрибутива",
         "нет одинаковости окружения между машинами", True),
    Case("gui", "Приложение с графическим интерфейсом",
         "проброс сокетов рабочего стола снимает изоляцию",
         "Flatpak или Snap: те же namespaces плюс порталы",
         "упаковка под несколько форматов", True),
    Case("hardware", "Прямой доступ к оборудованию",
         "passthrough привязывает образ к версии драйвера хоста",
         "пакет дистрибутива с модулем и пользовательской частью",
         "возвращаются различия между машинами", False,
         "одинаков ли парк машин"),
    Case("oneshot", "Однократный или кратковременный скрипт",
         "стоимость сборки и передачи образа превышает саму работу",
         "uv run --with, venv, pipx",
         "зависимости берутся из системы", True),
    Case("static", "Простой статический сайт",
         "задача — отдать файлы; ни одна из задач статики не решается",
         "статический хостинг или веб-сервер из пакета",
         "практически ничем", False,
         "есть ли нетривиальная сборка артефакта"),
    Case("nocapacity", "Команда не поддерживает инфраструктуру",
         "стоимость постоянная: базовые образы, кэш, сканирование, очистка",
         "пакеты дистрибутива и systemd",
         "нет одинаковости и единообразия доставки", True),
    Case("audit", "Жёсткая прослеживаемость состава",
         "появляется второй путь обновления, требующий утверждения",
         "фиксированный образ системы и проверенный репозиторий",
         "теряются воспроизводимость сборки и SBOM", False,
         "описана ли процедура под фиксированный образ системы"),
]

BY_KEY = {c.key: c for c in CASES}

# Доводы, звучащие похоже, но в список не попадающие
NOT_CASES = [
    ("У нас всего один сервис", "одинаковость окружения нужна и одному"),
    ("У нас нет DevOps-инженера", "вопрос времени команды, а не должности"),
    ("Приложение с состоянием", "тома решают задачу; вопрос в эксплуатации"),
    ("Нужна максимальная производительность",
     "расходов на вычислениях практически нет"),
    ("Мы на Windows", "механизм иной, но задача решается"),
]

# Признаки, при которых контейнеризация ОПРАВДАНА
FOR_DOCKER = [
    ("много_машин", "несколько машин: одинаковость окружения важна"),
    ("много_сервисов", "несколько сервисов: единообразие доставки"),
    ("сложные_зависимости", "зависимости не ставятся из репозитория дистрибутива"),
    ("нужна_воспроизводимость", "требуется воспроизводимая сборка и SBOM"),
    ("есть_поддержка", "есть кому поддерживать инфраструктуру сборки"),
]


def classify(task: dict[str, object]) -> dict[str, object]:
    matched = [BY_KEY[k] for k in task.get("признаки", []) if k in BY_KEY]
    pro = [text for key, text in FOR_DOCKER if task.get(key)]

    # Неоднозначные случаи снимаются, если условие выполнено
    resolved: list[str] = []
    effective: list[Case] = []
    for c in matched:
        if not c.certain and task.get(f"снято_{c.key}"):
            resolved.append(f"{c.name}: снят — {c.depends_on}")
        else:
            effective.append(c)

    if effective and len(effective) >= len(pro):
        verdict = "контейнеризация не оправдана"
        recommend = effective[0].alternative
    elif pro:
        verdict = "Docker оправдан"
        recommend = "образ по правилам разделов 05 и 11"
    else:
        verdict = "недостаточно данных"
        recommend = "уточнить: сколько машин, сколько сервисов, какие зависимости"

    return {
        "задача": task["описание"],
        "случаи": [c.as_dict() for c in effective],
        "снятые": resolved,
        "за_docker": pro,
        "вердикт": verdict,
        "рекомендация": recommend,
    }


def main(path: str) -> int:
    tasks = json.loads(Path(path).read_text())
    result = {
        "справочник": [c.as_dict() for c in CASES],
        "не_случаи": NOT_CASES,
        "неоднозначных": sum(1 for c in CASES if not c.certain),
        "задачи": [classify(t) for t in tasks],
    }
    print(json.dumps(result, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "tasks.json"))
PY

cat > tasks.json <<'EOF'
[
  {
    "описание": "Скрипт выгрузки отчёта, запускается раз в сутки, работает 3 секунды",
    "признаки": ["oneshot"],
    "много_машин": false,
    "сложные_зависимости": false
  },
  {
    "описание": "Настольная утилита с интерфейсом для внутренних пользователей",
    "признаки": ["gui"],
    "много_машин": false
  },
  {
    "описание": "Обработка видео на ускорителе, парк из 40 одинаковых машин",
    "признаки": ["hardware"],
    "снято_hardware": true,
    "много_машин": true,
    "много_сервисов": false,
    "сложные_зависимости": true,
    "есть_поддержка": true
  },
  {
    "описание": "Лендинг из готовых HTML и CSS без сборки",
    "признаки": ["static"],
    "много_машин": false
  },
  {
    "описание": "API на Python, шесть сервисов, четыре машины, есть дежурство",
    "признаки": [],
    "много_машин": true,
    "много_сервисов": true,
    "сложные_зависимости": true,
    "нужна_воспроизводимость": true,
    "есть_поддержка": true
  }
]
EOF

cat > script.py <<'PY'
"""Кратковременная задача для измерения стоимости запуска."""
from __future__ import annotations

import sys
from pathlib import Path


def main(root: str = ".") -> int:
    total = sum(len(p.read_text(encoding="utf-8").splitlines())
                for p in sorted(Path(root).rglob("*.py")))
    print(f"  строк Python: {total}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
PY

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

python3 cases.py tasks.json > result.json

printf '\n═══ Требование 1: механизм, а не вывод ═══\n'
python3 - <<'PY'
import json

d = json.load(open("result.json"))
print(f"    {'случай':<40} механизм")
print("    " + "─" * 108)
for c in d["справочник"]:
    print(f"    {c['случай']:<40} {c['механизм']}")
print()
print(f"    случаев: {len(d['справочник'])}")
print()
print("    В каждой строке названа ПРИЧИНА, по которой контейнеризация")
print("    не даёт выигрыша, а не утверждение, что не даёт.")
PY
n_cases="$(python3 -c "import json;print(len(json.load(open('result.json'))['справочник']))")"
[ "$n_cases" -eq 7 ] \
    && ok "семь случаев, каждый описан механизмом" \
    || bad "случаев: $n_cases"

printf '\n═══ Требование 2: альтернатива и её цена ═══\n'
python3 - <<'PY'
import json

d = json.load(open("result.json"))
print(f"    {'случай':<38} {'альтернатива':<46} чем платят")
print("    " + "─" * 128)
for c in d["справочник"]:
    print(f"    {c['случай']:<38} {c['альтернатива']:<46} {c['чем_платят']}")
print()
free = [c for c in d["справочник"] if "практически ничем" in c["чем_платят"]]
print(f"    случаев, где отказ почти ничего не стоит: {len(free)}")
for c in free:
    print(f"      · {c['случай']}")
print()
print("    Столбец «чем платят» обязателен: без него список превращается")
print("    в агитацию против инструмента, а не в разбор задач.")
PY
with_price="$(python3 -c "
import json
d = json.load(open('result.json'))
print(sum(1 for c in d['справочник'] if c['чем_платят']))")"
[ "$with_price" -eq 7 ] \
    && ok "для каждого случая названа цена отказа" \
    || bad "с ценой: $with_price из 7"

printf '\n═══ Требование 3: неоднозначные случаи ═══\n'
python3 - <<'PY'
import json

d = json.load(open("result.json"))
unclear = [c for c in d["справочник"] if not c["однозначен"]]
print(f"    {'случай':<38} вывод зависит от")
print("    " + "─" * 96)
for c in unclear:
    print(f"    {c['случай']:<38} {c['зависит_от']}")
print()
print(f"    неоднозначных: {len(unclear)} из {len(d['справочник'])}")
print()
print("    Это существеннее самого списка: прямой доступ к оборудованию")
print("    перестаёт быть доводом при одинаковом парке машин; статический")
print("    сайт стоит СОБИРАТЬ в container'е, но не РАЗДАВАТЬ из него;")
print("    прослеживаемость и усложняется контейнеризацией, и получает")
print("    от неё средства — SBOM, digest, подписи.")
PY
unclear="$(python3 -c "
import json
d = json.load(open('result.json'))
print(sum(1 for c in d['справочник'] if not c['однозначен']))")"
[ "$unclear" -ge 3 ] \
    && ok "неоднозначные случаи выделены с указанием условия" \
    || bad "неоднозначных: $unclear"

printf '\n═══ Требование 4: пять задач, включая обратный случай ═══\n'
python3 - <<'PY'
import json

d = json.load(open("result.json"))
for t in d["задачи"]:
    print(f"    ── {t['задача']}")
    for c in t["случаи"]:
        print(f"       случай: {c['случай']}")
        print(f"               механизм: {c['механизм']}")
    for s in t["снятые"]:
        print(f"       СНЯТ: {s}")
    for p in t["за_docker"]:
        print(f"       + {p}")
    print(f"       ВЫВОД: {t['вердикт']}")
    print(f"       → {t['рекомендация']}")
    print()
verdicts = [t["вердикт"] for t in d["задачи"]]
pro = sum(1 for v in verdicts if v == "Docker оправдан")
print(f"    задач: {len(verdicts)}, «Docker оправдан»: {pro}, "
      f"различных выводов: {len(set(verdicts))}")
PY
pro="$(python3 -c "
import json
d = json.load(open('result.json'))
print(sum(1 for t in d['задачи'] if t['вердикт'] == 'Docker оправдан'))")"
printf '\n  задач с выводом «Docker оправдан»: %s\n' "$pro"
[ "$pro" -ge 1 ] \
    && ok "инструмент способен рекомендовать Docker — значит, отказ что-то значит" \
    || bad "«Docker оправдан»: $pro — инструмент отвечает односторонне"

printf '\n═══ Требование 5: измерение стоимости альтернативы ═══\n'
command -v uv >/dev/null && uv --version | sed 's/^/  /'
python3 --version | sed 's/^/  /'
echo
rm -rf venv-std venv-uv
printf '  стандартное средство: '
/usr/bin/time -f "%e с" python3 -m venv venv-std 2>&1 | tail -1
printf '  uv:                   '
/usr/bin/time -f "%e с" uv venv venv-uv 2>&1 | tail -1
printf '  сама задача:          '
python3 script.py . >/dev/null 2>&1
/usr/bin/time -f "%e с" python3 script.py . 2>&1 | tail -1
rm -rf venv-std venv-uv
echo
echo "  Сравнение с docker build здесь НЕ приводится: Docker не установлен,"
echo "  а придумывать время сборки было бы подлогом. Достроить сравнение"
echo "  можно у себя:"
echo "    time docker build -t oneshot .          # НЕ ВЫПОЛНЯЛОСЬ"
echo "    time docker run --rm oneshot script.py  # НЕ ВЫПОЛНЯЛОСЬ"
ok "стоимость альтернативы измерена; сравнение честно оставлено читателю"

printf '\n═══ Требование 6: похожие доводы, не попадающие в список ═══\n'
python3 - <<'PY'
import json

d = json.load(open("result.json"))
print(f"    {'довод':<42} почему НЕ попадает")
print("    " + "─" * 100)
for case, why in d["не_случаи"]:
    print(f"    {case:<42} {why}")
print()
print(f"    доводов: {len(d['не_случаи'])}")
print()
print("    Первый и четвёртый встречаются чаще прочих, и оба основаны")
print("    на неточном представлении о том, где Docker берёт своё:")
print("    одинаковость окружения нужна и одной машине, а расходов")
print("    на вычислениях у container'а практически нет.")
PY
n_not="$(python3 -c "import json;print(len(json.load(open('result.json'))['не_случаи']))")"
[ "$n_not" -ge 4 ] \
    && ok "названы доводы, которые звучат похоже, но неверны" \
    || bad "доводов: $n_not"

printf '\n═══ Требование 7: измеренное и предполагаемое ═══\n'
python3 - <<'PY'
MEASURED = [
    ("время создания venv стандартным средством", "/usr/bin/time"),
    ("время создания venv через uv", "/usr/bin/time"),
    ("время выполнения самой задачи", "/usr/bin/time"),
    ("наличие и версии инструментов", "команды --version"),
]
NOT_MEASURED = [
    ("время сборки образа", "Docker на машине курса не установлен"),
    ("расходы на вводе-выводе в container'е", "то же"),
    ("поведение проброса сокета рабочего стола", "требует сессии и Docker"),
    ("привязка образа к версии драйвера", "требует оборудования"),
    ("стоимость поддержки инфраструктуры", "зависит от команды и проекта"),
]
print(f"    {'ИЗМЕРЕНО':<44} чем")
print("    " + "─" * 74)
for name, how in MEASURED:
    print(f"    {name:<44} {how}")
print()
print(f"    {'НЕ ИЗМЕРЯЛОСЬ':<44} причина")
print("    " + "─" * 84)
for name, why in NOT_MEASURED:
    print(f"    {name:<44} {why}")
print()
print(f"    измерено: {len(MEASURED)}, не измерялось: {len(NOT_MEASURED)}")
print()
print("    Механизмы в справочнике — утверждения о том, как устроены")
print("    OverlayFS, сеть, passthrough и порталы. Они опираются")
print("    на разделы 08, 17 и документацию, а не на замеры здесь.")
PY
ok "измеренное отделено от утверждаемого"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все требования выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"
echo "  примечание: Docker не использовался; измерена только альтернатива"

cd /tmp && rm -rf /tmp/nodocker
exit "$fail"

Ожидаемый вывод (значения времени зависят от машины и от прогрева кэша):

text
═══ Требование 1: механизм, а не вывод ═══
    случай                                   механизм
    ────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Жёсткие требования к задержкам           расходы на вводе-выводе и сети; настройки ядра делаются на хосте
    Приложение с графическим интерфейсом     проброс сокетов рабочего стола снимает изоляцию
    Прямой доступ к оборудованию             passthrough привязывает образ к версии драйвера хоста
    Однократный или кратковременный скрипт   стоимость сборки и передачи образа превышает саму работу
    Простой статический сайт                 задача — отдать файлы; ни одна из задач статики не решается
    Команда не поддерживает инфраструктуру   стоимость постоянная: базовые образы, кэш, сканирование, очистка
    Жёсткая прослеживаемость состава         появляется второй путь обновления, требующий утверждения

    случаев: 7

    В каждой строке названа ПРИЧИНА, по которой контейнеризация
    не даёт выигрыша, а не утверждение, что не даёт.
  ✓ семь случаев, каждый описан механизмом

═══ Требование 3: неоднозначные случаи ═══
    случай                                 вывод зависит от
    ────────────────────────────────────────────────────────────────────────────────────────────────
    Прямой доступ к оборудованию           одинаков ли парк машин
    Простой статический сайт               есть ли нетривиальная сборка артефакта
    Жёсткая прослеживаемость состава       описана ли процедура под фиксированный образ системы

    неоднозначных: 3 из 7
  ✓ неоднозначные случаи выделены с указанием условия

═══ Требование 4: пять задач, включая обратный случай ═══
    ── Скрипт выгрузки отчёта, запускается раз в сутки, работает 3 секунды
       случай: Однократный или кратковременный скрипт
               механизм: стоимость сборки и передачи образа превышает саму работу
       ВЫВОД: контейнеризация не оправдана
       → uv run --with, venv, pipx

    ── Настольная утилита с интерфейсом для внутренних пользователей
       случай: Приложение с графическим интерфейсом
               механизм: проброс сокетов рабочего стола снимает изоляцию
       ВЫВОД: контейнеризация не оправдана
       → Flatpak или Snap: те же namespaces плюс порталы

    ── Обработка видео на ускорителе, парк из 40 одинаковых машин
       СНЯТ: Прямой доступ к оборудованию: снят — одинаков ли парк машин
       + несколько машин: одинаковость окружения важна
       + зависимости не ставятся из репозитория дистрибутива
       + есть кому поддерживать инфраструктуру сборки
       ВЫВОД: Docker оправдан
       → образ по правилам разделов 05 и 11

    ── Лендинг из готовых HTML и CSS без сборки
       случай: Простой статический сайт
               механизм: задача — отдать файлы; ни одна из задач статики не решается
       ВЫВОД: контейнеризация не оправдана
       → статический хостинг или веб-сервер из пакета

    ── API на Python, шесть сервисов, четыре машины, есть дежурство
       + несколько машин: одинаковость окружения важна
       + несколько сервисов: единообразие доставки
       + зависимости не ставятся из репозитория дистрибутива
       + требуется воспроизводимая сборка и SBOM
       + есть кому поддерживать инфраструктуру сборки
       ВЫВОД: Docker оправдан
       → образ по правилам разделов 05 и 11

    задач: 5, «Docker оправдан»: 2, различных выводов: 2

  задач с выводом «Docker оправдан»: 2
  ✓ инструмент способен рекомендовать Docker — значит, отказ что-то значит

═══ Требование 5: измерение стоимости альтернативы ═══
  uv 0.11.6 (x86_64-unknown-linux-gnu)
  Python 3.12.3

  стандартное средство: 5.28 с
  uv:                   0.13 с
  сама задача:          0.02 с

  Сравнение с docker build здесь НЕ приводится: Docker не установлен,
  а придумывать время сборки было бы подлогом.
  ✓ стоимость альтернативы измерена; сравнение честно оставлено читателю

═══ ИТОГ ═══
  все требования выполнены
  примечание: Docker не использовался; измерена только альтернатива

Все требования выполнены. Числа времени получены на машине курса.

Три решения, определяющие качество.

Инструмент умеет отвечать «Docker оправдан». Две задачи из пяти получили именно этот вывод, причём одна — за счёт снятия неоднозначного случая: обработка видео на ускорителе попадает в пункт «прямой доступ к оборудованию», но парк из сорока одинаковых машин снимает привязку к драйверу, и контейнеризация становится оправданной. Без такого исхода список превратился бы в агитацию: инструмент, отвечающий «не нужен» на любой вопрос, ничего не сообщает.

Три случая из семи помечены неоднозначными, с указанием условия. Соблазн был свести всё к однозначным правилам — так список выглядит убедительнее. Но именно неоднозначные случаи полезны на практике: они говорят, какой вопрос задать, а не какой ответ выбрать. «Одинаков ли парк машин» решает больше, чем сам факт наличия ускорителя.

Сравнение с Docker не достроено. Измерены только альтернативы: 5,28 с и 0,13 с на создание окружения, 0,02 с на работу. Время docker build не измерено и не оценено — Docker не установлен. Приписать сюда «а образ собирается 40 секунд» было бы подлогом, и вся ценность урока о честности исчезла бы вместе с этим числом.

Чего решение не делает. Механизмы в справочнике — утверждения, а не результаты замеров: OverlayFS, veth, passthrough и порталы описаны по документации и разделам 08 и 17. Ни один из семи случаев не воспроизведён: проброс сокета рабочего стола, привязка к версии драйвера и расходы на вводе-выводе требуют Docker и оборудования. Правило выбора вердикта — сравнение числа доводов — грубое: доводы разного веса, и на пограничной задаче итог зависит от того, как её описали. Наконец, пункт про прослеживаемость состава остаётся самым спорным: он верен для процедур, определённых под фиксированный образ системы, и неверен там, где процедура допускает образы и требует SBOM.

Проверка результата

bash
command -v uv && uv --version
/usr/bin/time -f "%e с" uv venv /tmp/probe && rm -rf /tmp/probe

Для своей задачи ответьте на два вопроса: что перестанет работать, если убрать Docker и чем придётся заплатить за отказ. Если на первый ответа нет, а на второй — «ничем», решение принято.

Типичные ошибки

ОшибкаПричинаИсправление
Контейнеризовать однократный скриптЕдинообразиеСтоимость запуска превышает работу
Раздавать статику из container'аПривычкаContainer не решает ни одной задачи статики
Пробрасывать сокет рабочего столаЕдинственный способОт изоляции остаётся немного; есть Flatpak
Ждать от container'а низких задержекКажется лёгкимНастройки ядра делаются на хосте
Считать GPU-образ переносимымОбещание «работает везде»Привязка к версии драйвера
Отказываться из-за одного сервисаКажется избыточнымОдинаковость окружения нужна и одному
Отказываться ради производительностиАналогия с ВМРасходов на вычислениях практически нет
Считать список безусловнымТак прощеТри случая из семи зависят от условия
Внедрять без ответа «кто поддерживает»Кажется разовой работойСтоимость постоянная

Контрольные вопросы

На понимание:

  1. Какой вопрос отбирает случаи в этот список?
  2. Почему настройки для низких задержек делаются на хосте?
  3. Чем портал отличается от проброса сокета?
  4. Почему статический сайт не выигрывает от контейнеризации?
  5. Какие три случая неоднозначны и от чего зависит вывод?

На применение:

  1. Какая альтернатива у однократного скрипта и чем за неё платят?
  2. Когда прямой доступ к оборудованию перестаёт быть доводом против?
  3. Что нужно выяснить, прежде чем отказываться из-за прослеживаемости?

На диагностику:

  1. Команда отказалась от Docker «ради производительности». Какой вопрос задать?
  2. Настольное приложение в container'е работает, но видит все файлы пользователя. Что произошло?

Краткое резюме

  1. Правило отбора: что перестанет работать, если убрать Docker.
  2. Семь случаев, и в трёх из них вывод зависит от дополнительного условия.
  3. Низкие задержки: настройки ядра делаются на хосте, container их не приносит.
  4. Графический интерфейс: проброс сокетов снимает изоляцию; Flatpak даёт порталы.
  5. Оборудование: passthrough привязывает образ к версии драйвера на хосте.
  6. Привязка перестаёт быть проблемой, если парк машин одинаков.
  7. Однократные скрипты: стоимость сборки превышает саму работу.
  8. Статический сайт стоит собирать в container'е, но не раздавать из него.
  9. Отсутствие людей на поддержку инфраструктуры — вопрос времени команды, а не должностей.
  10. Прослеживаемость состава контейнеризация и усложняет, и снабжает средствами.
  11. «Один сервис» и «нужна производительность» — доводы, основанные на неточных представлениях.
  12. Отказ от Docker должен называть альтернативу и цену, иначе это не решение.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker: драйверы хранилищаhttps://docs.docker.com/storage/storagedriver/Расходы на файловой системе
Docker: сетьhttps://docs.docker.com/network/Bridge, NAT, --network host
Docker: GPUhttps://docs.docker.com/desktop/features/gpu/Требования к драйверу хоста
Flatpak: песочницаhttps://docs.flatpak.org/en/latest/sandbox-permissions.htmlNamespaces и права
Flatpak: порталыhttps://docs.flatpak.org/en/latest/desktop-integration.htmlДоступ через посредника
uvhttps://docs.astral.sh/uv/uv run --with, скорость создания окружений
pipxhttps://pipx.pypa.io/stable/Изолированная установка приложений
Kernel: cpusetshttps://docs.kernel.org/admin-guide/cgroup-v1/cpusets.htmlПривязка к ядрам
Kernel: hugetlbpagehttps://docs.kernel.org/admin-guide/mm/hugetlbpage.htmlБольшие страницы памяти

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Каталог антипаттернов
Главное оглавление

Markdown на GitHub ↗