18.4. Когда нужна orchestration
Цели
После этого материала вы сможете:
- назвать признаки, по которым Compose ещё достаточно;
- назвать признаки, по которым его перестаёт хватать;
- оценить стоимость Kubernetes: обучение, эксплуатация, отладка;
- перечислить промежуточные варианты между Compose и полным кластером;
- заполнить чек-лист принятия решения для конкретного проекта;
- отличить настоящее требование от воспроизведённого по инерции.
Предварительные знания
- 18.1. Какие задачи решают Docker и Kubernetes;
- 18.3. От Compose к Kubernetes — объём работы по переносу;
- Раздел 09. Docker Compose.
Кластер не требуется. Урок аналитический.
Ключевые термины
| Термин | Объяснение |
|---|---|
orchestration | Управление контейнерами на множестве машин |
SLO | Заявленный уровень качества обслуживания |
эксплуатационная нагрузка | Время, уходящее на поддержание системы, а не на разработку |
одна точка отказа | Компонент, отказ которого останавливает систему |
управляемый кластер | Кластер, control plane которого обслуживает провайдер |
Теория
Правильная постановка вопроса
Неверная формулировка: «Docker или Kubernetes?» Они на разных уровнях (урок 18.1).
Верная: «Хватает ли нам одной машины?»
Kubernetes решает задачу поддержания заданного состояния на множестве машин. Если машина одна, большая часть системы не нужна — а стоимость остаётся полной.
Признаки, что Compose достаточно
| Признак | Почему достаточно |
|---|---|
| Одна машина справляется с нагрузкой | Нечего распределять |
| Простой в несколько минут допустим | Не нужна автоматическая замена узла |
| Развёртывания редкие и в рабочее время | Не нужны сложные стратегии |
| Одна команда владеет всей системой | Не нужна изоляция по namespace |
| Нагрузка предсказуема | Не нужно автомасштабирование |
| Сервисов меньше десяти | Compose справляется с описанием |
| Нет требования к географическому распределению | Один регион |
Существенно: это не список признаков малого проекта. Одна машина с 64 ГБ памяти обслуживает очень многое.
Признаки, что Compose перестаёт хватать
| Признак | Что именно ломается |
|---|---|
| Одна машина не тянет нагрузку | Требуется распределение |
| Отказ машины недопустим | Compose не переносит сервисы на другой узел |
| Нужно масштабирование по нагрузке | Ручное --scale не успевает |
| Развёртывания частые, простой недопустим | Нужны стратегии выкатки и отката |
| Несколько команд на общей инфраструктуре | Нужны изоляция и квоты |
| Больше нескольких машин с ручным управлением | Управление вручную не масштабируется |
| Требуется автоматическое восстановление | Некому переносить нагрузку с отказавшего узла |
Каждый признак — наблюдаемый факт, а не предположение о будущем.
Проверка на подлинность требования
Самое частое основание для перехода: «нам понадобится масштабирование». Проверка:
| Вопрос | Что показывает |
|---|---|
| Какова нагрузка сейчас? | Есть ли проблема сегодня |
| Какова была год назад? | Скорость роста, а не воображаемая кривая |
| Что произойдёт при простое в 10 минут? | Реальная цена доступности |
| Кто отвечает ночью? | Есть ли кому эксплуатировать кластер |
| Сколько раз в неделю развёртывание? | Нужны ли сложные стратегии |
| Отказывала ли машина за последний год? | Насколько вероятен сценарий |
Требование считается настоящим, если на него можно указать пальцем в данных.
Стоимость Kubernetes
Стоимость чаще всего недооценивают, потому что видят только первую строку.
| Статья | Разовая или постоянная | Порядок |
|---|---|---|
| Обучение команды | Разовая, но повторяется с наймом | Недели на человека |
| Перенос приложений | Разовая | См. урок 18.3: половина работы в коде |
| Control plane | Постоянная | Управляемый — оплата; свой — эксплуатация |
| Обновления кластера | Постоянная | Раз в несколько месяцев, с рисками |
| Отладка | Постоянная | Больше уровней между симптомом и причиной |
| Сопутствующие компоненты | Постоянная | Ingress, мониторинг, логи, сертификаты, политики |
| Дежурство | Постоянная | Требует знаний Kubernetes, а не только приложения |
Последняя строка часто оказывается решающей: кластер требует людей, знающих кластер. Если такой человек один, кластер становится одной точкой отказа — организационной.
Почему отладка дороже
В Compose путь от симптома к причине короткий: контейнер, лог, процесс.
В Kubernetes между симптомом и причиной находятся:
запрос → Ingress → Service → kube-proxy → endpoints →
→ pod → readiness → контейнер → процесс
Каждое звено может быть причиной. Типичный пример: «сервис не отвечает». Возможные причины — от неверного селектора Service до вытеснения pod'а по памяти. Ни одна не видна в логе приложения.
Это не довод против Kubernetes — это статья расходов, которую нужно заложить.
Промежуточные варианты
Между «одна машина с Compose» и «полный кластер» есть несколько ступеней. Их часто пропускают.
| Вариант | Что решает | Чего не решает |
|---|---|---|
| Compose + вторая машина вручную | Переживание отказа одной | Автоматики нет |
| Compose + балансировщик перед двумя машинами | Доступность при отказе | Состояние, выкатка |
| Docker Swarm | Несколько узлов, простая модель | Развитие остановлено |
| Nomad | Оркестрация без сложности Kubernetes | Меньше экосистема |
| Управляемый Kubernetes | Control plane обслуживает провайдер | Знания всё равно нужны |
| Serverless-платформа контейнеров | Нет узлов вообще | Ограничения платформы, привязка |
| Свой Kubernetes | Полный контроль | Максимальная стоимость |
Практическое наблюдение: управляемый Kubernetes снимает примерно половину эксплуатационной нагрузки — ту, что связана с control plane. Вторая половина — приложения, пробы, ресурсы, отладка — остаётся полностью.
Наблюдение о выносе состояния
Часто основная трудность переноса — база данных (урок 18.3). Управляемая база вне кластера снимает её целиком: резервные копии, обновления, отказоустойчивость перестают быть вашей задачей.
Следствие, которое стоит проверить: вынос базы наружу иногда снимает и саму потребность в кластере. Если после этого остаются только сервисы без состояния, их часто хватает разместить на двух машинах с балансировщиком.
Честный чек-лист
Ниже — вопросы с весами. Смысл не в точности чисел, а в том, чтобы разговор шёл о фактах.
| Вопрос | Вес, если «да» |
|---|---|
| Одна машина не справляется с нагрузкой сегодня | +3 |
| Простой более 5 минут стоит существенных денег | +3 |
| Развёртывания чаще раза в день | +2 |
| Нагрузка меняется в разы в течение суток | +2 |
| Больше трёх команд на общей инфраструктуре | +2 |
| Уже управляете более чем тремя машинами вручную | +2 |
| Требуется несколько географических регионов | +2 |
| В команде есть человек с опытом эксплуатации Kubernetes | +1 |
| Состояние уже вынесено в управляемые сервисы | +1 |
| Приложения переживают перезапуск в любой момент | +1 |
| «Нам понадобится масштабирование» без данных | −2 |
| Единственный довод — так делают все | −3 |
| Некому дежурить с знанием кластера | −3 |
| Простой в час допустим | −2 |
Итог:
| Сумма | Вывод |
|---|---|
| 8 и выше | Kubernetes обоснован |
| 4–7 | Промежуточный вариант; вернуться через полгода |
| 3 и ниже | Compose достаточно; перенос обойдётся дороже пользы |
Отрицательные веса важнее положительных: они отсекают решения, принятые по инерции.
Внутренний механизм
Почему Compose не переносит сервисы на другой узел
Compose — клиент Docker API одной машины (урок 9.1). Он не знает о существовании других машин и не имеет цикла управления, который сравнивал бы желаемое состояние с фактическим.
restart: unless-stopped перезапустит контейнер на той же машине. Если машина недоступна, перезапускать некому.
Kubernetes отличается наличием контроллеров: цикл сравнения состояний работает постоянно, и узел, переставший отвечать, приводит к переносу pod'ов (урок 18.2).
Это и есть граница: Compose управляет контейнерами, Kubernetes поддерживает состояние.
Почему стоимость нельзя амортизировать заранее
Довод «поставим сейчас, чтобы не переносить потом» опирается на предположение, что перенос позже дороже. На практике:
- значительная часть работы — приведение приложений в порядок (урок 18.3), и она полезна независимо от кластера;
- остальная часть — манифесты, и они пишутся быстрее с накопленным пониманием нагрузки;
- эксплуатационная стоимость начисляется с первого дня и не возвращается.
То есть ранняя установка не экономит будущую работу, а добавляет текущую.
Команды и примеры
Чек-лист как инструмент
mkdir -p /tmp/decide && cd /tmp/decide
cat > checklist.py <<'PY'
"""Чек-лист принятия решения об orchestration.
Веса подобраны так, чтобы отрицательные признаки (решение по инерции,
отсутствие людей для эксплуатации) отсекали положительные.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
QUESTIONS: list[tuple[str, str, int]] = [
("load", "Одна машина не справляется с нагрузкой СЕГОДНЯ", 3),
("downtime", "Простой более 5 минут стоит существенных денег", 3),
("deploys", "Развёртывания чаще раза в день", 2),
("spikes", "Нагрузка меняется в разы в течение суток", 2),
("teams", "Больше трёх команд на общей инфраструктуре", 2),
("machines", "Уже управляете более чем тремя машинами вручную", 2),
("regions", "Требуется несколько географических регионов", 2),
("expertise", "В команде есть человек с опытом эксплуатации кластера", 1),
("managed_state", "Состояние вынесено в управляемые сервисы", 1),
("restartable", "Приложения переживают перезапуск в любой момент", 1),
("someday", "Главный довод: «нам понадобится масштабирование»", -2),
("everyone", "Единственный довод: так делают все", -3),
("no_oncall", "Некому дежурить со знанием кластера", -3),
("tolerant", "Простой в час допустим", -2),
]
VERDICTS = [
(8, "Kubernetes обоснован",
"признаки указывают на настоящие требования, а не на ожидания"),
(4, "Промежуточный вариант; вернуться через полгода",
"часть требований настоящая, но полный кластер преждевременен"),
(-99, "Compose достаточно",
"перенос обойдётся дороже пользы, которую принесёт"),
]
INTERMEDIATE = [
("Вторая машина с Compose вручную", "переживание отказа одной", "автоматики нет"),
("Балансировщик перед двумя машинами", "доступность при отказе", "состояние, выкатка"),
("Управляемая база вне кластера", "резервные копии, обновления, отказоустойчивость",
"стоимость сервиса"),
("Nomad", "оркестрация без сложности Kubernetes", "меньше экосистема"),
("Управляемый Kubernetes", "control plane обслуживает провайдер",
"знания всё равно нужны"),
]
def score(answers: dict[str, bool]) -> dict[str, object]:
positive: list[tuple[str, int]] = []
negative: list[tuple[str, int]] = []
total = 0
for key, text, weight in QUESTIONS:
if answers.get(key):
total += weight
(positive if weight > 0 else negative).append((text, weight))
for threshold, verdict, why in VERDICTS:
if total >= threshold:
break
return {
"сумма": total,
"за": positive,
"против": negative,
"вердикт": verdict,
"почему": why,
}
def main(path: str) -> int:
answers = json.loads(Path(path).read_text())
result = score(answers)
print(f" проект: {answers.get('_name', 'без имени')}")
print()
print(f" {'признак':<56} {'вес':>5}")
print(" " + "─" * 64)
for text, weight in result["за"]:
print(f" {text:<56} {weight:>+5}")
for text, weight in result["против"]:
print(f" {text:<56} {weight:>+5}")
print(" " + "─" * 64)
print(f" {'СУММА':<56} {result['сумма']:>5}")
print()
print(f" Вывод: {result['вердикт']}")
print(f" Почему: {result['почему']}")
if 4 <= result["сумма"] < 8:
print()
print(f" {'промежуточный вариант':<38} {'решает':<44} не решает")
print(" " + "─" * 104)
for name, solves, misses in INTERMEDIATE:
print(f" {name:<38} {solves:<44} {misses}")
print()
print(json.dumps({"сумма": result["сумма"], "вердикт": result["вердикт"]},
ensure_ascii=False))
# Код возврата: 0 — Compose достаточно, 1 — промежуточный, 2 — Kubernetes
return 2 if result["сумма"] >= 8 else (1 if result["сумма"] >= 4 else 0)
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "project.json"))
PY
cat > startup.json <<'EOF'
{
"_name": "Небольшой сервис, три разработчика",
"load": false,
"downtime": false,
"deploys": true,
"spikes": false,
"teams": false,
"machines": false,
"regions": false,
"expertise": false,
"managed_state": true,
"restartable": true,
"someday": true,
"everyone": true,
"no_oncall": true,
"tolerant": true
}
EOF
echo "═══ проект 1 ═══"
python3 checklist.py startup.json; echo " код возврата: $?"
Ожидаемый вывод:
═══ проект 1 ═══
проект: Небольшой сервис, три разработчика
признак вес
────────────────────────────────────────────────────────────────
Развёртывания чаще раза в день +2
Состояние вынесено в управляемые сервисы +1
Приложения переживают перезапуск в любой момент +1
Главный довод: «нам понадобится масштабирование» -2
Единственный довод: так делают все -3
Некому дежурить со знанием кластера -3
Простой в час допустим -2
────────────────────────────────────────────────────────────────
СУММА -6
Вывод: Compose достаточно
Почему: перенос обойдётся дороже пользы, которую принесёт
{"сумма": -6, "вердикт": "Compose достаточно"}
код возврата: 0
Заметьте: три положительных признака есть, и все они настоящие. Их перевешивают отрицательные — и это верно, потому что эксплуатировать кластер здесь некому.
Три проекта для сравнения
cd /tmp/decide
cat > growing.json <<'EOF'
{
"_name": "Растущий сервис, две команды, четыре машины",
"load": false,
"downtime": true,
"deploys": true,
"spikes": true,
"teams": false,
"machines": true,
"regions": false,
"expertise": true,
"managed_state": true,
"restartable": true,
"someday": false,
"everyone": false,
"no_oncall": false,
"tolerant": false
}
EOF
cat > platform.json <<'EOF'
{
"_name": "Платформа, шесть команд, два региона",
"load": true,
"downtime": true,
"deploys": true,
"spikes": true,
"teams": true,
"machines": true,
"regions": true,
"expertise": true,
"managed_state": true,
"restartable": true,
"someday": false,
"everyone": false,
"no_oncall": false,
"tolerant": false
}
EOF
for f in startup.json growing.json platform.json; do
printf '\n═══ %s ═══\n' "$f"
python3 checklist.py "$f" | tail -20
printf ' код возврата: %s\n' "$?"
done
Ожидаемый вывод:
═══ startup.json ═══
...
СУММА -6
Вывод: Compose достаточно
код возврата: 0
═══ growing.json ═══
признак вес
────────────────────────────────────────────────────────────────
Простой более 5 минут стоит существенных денег +3
Развёртывания чаще раза в день +2
Нагрузка меняется в разы в течение суток +2
Уже управляете более чем тремя машинами вручную +2
В команде есть человек с опытом эксплуатации кластера +1
Состояние вынесено в управляемые сервисы +1
Приложения переживают перезапуск в любой момент +1
────────────────────────────────────────────────────────────────
СУММА 12
Вывод: Kubernetes обоснован
код возврата: 2
═══ platform.json ═══
СУММА 17
Вывод: Kubernetes обоснован
код возврата: 2
Второй проект набирает 12 без единого отрицательного признака — и это тот случай, когда решение принимают по фактам, а не по моде.
Стоимость в цифрах
cd /tmp/decide
cat > cost.py <<'PY'
"""Модель стоимости перехода. Числа — порядки величин, не измерения."""
from __future__ import annotations
import json
# (статья, разовая ч, ежемесячная ч, комментарий)
COSTS = [
("Обучение команды", 120, 8, "недели на человека; повторяется с наймом"),
("Перенос приложений", 60, 0, "половина — работа в коде (урок 18.3)"),
("Манифесты и шаблоны", 40, 4, "поддержание при изменениях"),
("Ingress и сертификаты", 16, 2, "выбор контроллера, обновление"),
("Мониторинг и логи", 40, 6, "метрики кластера отдельно от приложения"),
("Обновления кластера", 8, 6, "раз в несколько месяцев, с рисками"),
("Отладка (сверх обычной)", 0, 12, "больше уровней между симптомом и причиной"),
("Дежурство со знанием кластера", 0, 10, "требует людей, знающих кластер"),
]
MANAGED_SAVING = {
"Обновления кластера": 0.7,
"Мониторинг и логи": 0.3,
"Обучение команды": 0.2,
}
def main() -> None:
print(f" {'статья':<32} {'разово, ч':>10} {'в месяц, ч':>11} комментарий")
print(" " + "─" * 108)
one_time = monthly = 0
for name, once, per_month, note in COSTS:
one_time += once
monthly += per_month
print(f" {name:<32} {once:>10} {per_month:>11} {note}")
print(" " + "─" * 108)
print(f" {'ИТОГО':<32} {one_time:>10} {monthly:>11}")
print()
managed_monthly = sum(
per_month * (1 - MANAGED_SAVING.get(name, 0.0))
for name, _, per_month, _ in COSTS
)
print(f" свой кластер: {monthly:>5} ч/мес")
print(f" управляемый кластер: {managed_monthly:>5.0f} ч/мес "
f"(−{(1 - managed_monthly / monthly) * 100:.0f} %)")
print()
print(" Управляемый кластер снимает нагрузку по control plane.")
print(" Приложения, пробы, ресурсы и отладка остаются полностью.")
print()
year = one_time + monthly * 12
print(f" первый год, свой кластер: {year} ч ≈ {year / 160:.1f} человеко-месяца")
print()
print(" Довод «поставим сейчас, чтобы не переносить потом» неверен:")
print(" ежемесячная стоимость начисляется с первого дня и не возвращается,")
print(" а разовая часть (перенос) полезна и без кластера.")
print()
print(json.dumps({"разово_ч": one_time, "в_месяц_ч": monthly,
"первый_год_ч": year}, ensure_ascii=False))
if __name__ == "__main__":
main()
PY
echo "═══ модель стоимости ═══"
python3 cost.py
cd /tmp && rm -rf /tmp/decide
Ожидаемый вывод:
═══ модель стоимости ═══
статья разово, ч в месяц, ч комментарий
────────────────────────────────────────────────────────────────────────────────────────────────────────────
Обучение команды 120 8 недели на человека; повторяется с наймом
Перенос приложений 60 0 половина — работа в коде (урок 18.3)
Манифесты и шаблоны 40 4 поддержание при изменениях
Ingress и сертификаты 16 2 выбор контроллера, обновление
Мониторинг и логи 40 6 метрики кластера отдельно от приложения
Обновления кластера 8 6 раз в несколько месяцев, с рисками
Отладка (сверх обычной) 0 12 больше уровней между симптомом и причиной
Дежурство со знанием кластера 0 10 требует людей, знающих кластер
────────────────────────────────────────────────────────────────────────────────────────────────────────────
ИТОГО 284 48
свой кластер: 48 ч/мес
управляемый кластер: 42 ч/мес (−13 %)
Управляемый кластер снимает нагрузку по control plane.
Приложения, пробы, ресурсы и отладка остаются полностью.
первый год, свой кластер: 860 ч ≈ 5.4 человеко-месяца
Обратите внимание на строку про управляемый кластер: экономия — 13 %, а не «половина». Она приходится на обновления и часть мониторинга; отладка, дежурство и работа с приложениями не уменьшаются.
Это уточняет утверждение из теории: управляемый кластер снимает существенную часть работы по control plane, но control plane — не главная статья расходов.
Практическое упражнение
Задание. Постройте инструмент принятия решения и примените его к трём проектам.
Требования:
- Чек-лист с положительными и отрицательными весами; отрицательные должны быть способны перевесить.
- Три вердикта с разными кодами возврата.
- Применить к трём проектам с разными исходами.
- Для промежуточного исхода вывести список вариантов между Compose и кластером.
- Модель стоимости: разовая и постоянная части, за первый год.
- Показать, какую долю стоимости снимает управляемый кластер.
- Проверить чек-лист на подделке: проект, у которого «всё да», но эксплуатировать некому.
Подсказки
Подсказка 1
Отрицательные веса должны быть по модулю сравнимы с самыми большими положительными, иначе они не влияют.
Подсказка 2
Проверка на подделке: если признаки «нужно масштабирование» проставлены без данных, инструмент должен это учесть.
Подсказка 3
Постоянная стоимость важнее разовой: за год она обычно превышает разовую.
Решение
Показать решение
mkdir -p /tmp/orch && cd /tmp/orch
cat > decide.py <<'PY'
"""Инструмент принятия решения об orchestration.
Устройство весов: отрицательные признаки по модулю сравнимы
с самыми большими положительными. Это сделано намеренно —
иначе проект, где эксплуатировать кластер некому, набрал бы
проходной балл на одних лишь ожиданиях роста.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# (ключ, формулировка, вес, чем подтверждается)
QUESTIONS: list[tuple[str, str, int, str]] = [
("load", "Одна машина не справляется СЕГОДНЯ", 3,
"метрики загрузки за последний месяц"),
("downtime", "Простой более 5 минут стоит существенных денег", 3,
"оценка потерь, а не ощущение"),
("deploys", "Развёртывания чаще раза в день", 2,
"история выкаток"),
("spikes", "Нагрузка меняется в разы в течение суток", 2,
"график запросов"),
("teams", "Больше трёх команд на общей инфраструктуре", 2,
"список команд"),
("machines", "Более трёх машин под ручным управлением", 2,
"инвентарь"),
("regions", "Нужны несколько географических регионов", 2,
"требование, а не желание"),
("expertise", "Есть человек с опытом эксплуатации кластера", 1,
"конкретное имя"),
("managed_state", "Состояние вынесено в управляемые сервисы", 1,
"список хранилищ"),
("restartable", "Приложения переживают перезапуск в любой момент", 1,
"проверено, а не предполагается"),
("someday", "Главный довод: «понадобится масштабирование»", -2,
"данных о росте нет"),
("everyone", "Единственный довод: так делают все", -3,
"довод не о системе"),
("no_oncall", "Некому дежурить со знанием кластера", -3,
"кластер станет одной точкой отказа"),
("tolerant", "Простой в час допустим", -2,
"автоматическое восстановление не окупится"),
]
INTERMEDIATE = [
("Вторая машина с Compose вручную",
"переживание отказа одной машины", "автоматики нет"),
("Балансировщик перед двумя машинами",
"доступность при отказе", "состояние, выкатка"),
("Управляемая база вне кластера",
"копии, обновления, отказоустойчивость хранилища", "стоимость сервиса"),
("Nomad",
"оркестрация с меньшей сложностью", "меньше экосистема"),
("Управляемый Kubernetes",
"control plane обслуживает провайдер", "знания всё равно нужны"),
]
VERDICTS = [
(8, 2, "Kubernetes обоснован",
"требования подтверждаются фактами, есть кому эксплуатировать"),
(4, 1, "Промежуточный вариант; пересмотреть через полгода",
"часть требований настоящая, полный кластер преждевременен"),
(None, 0, "Compose достаточно",
"перенос обойдётся дороже пользы"),
]
def evaluate(answers: dict[str, object]) -> dict[str, object]:
plus: list[tuple[str, int, str]] = []
minus: list[tuple[str, int, str]] = []
total = 0
for key, text, weight, evidence in QUESTIONS:
if answers.get(key):
total += weight
(plus if weight > 0 else minus).append((text, weight, evidence))
for threshold, code, verdict, why in VERDICTS:
if threshold is None or total >= threshold:
break
raw_plus = sum(w for _, w, _ in plus)
raw_minus = sum(w for _, w, _ in minus)
return {
"проект": answers.get("_name", "без имени"),
"сумма": total,
"плюсов": raw_plus,
"минусов": raw_minus,
"за": plus,
"против": minus,
"вердикт": verdict,
"почему": why,
"код": code,
"перевес": bool(minus) and total < 4 <= raw_plus,
}
def report(r: dict[str, object]) -> None:
print(f" проект: {r['проект']}")
print()
print(f" {'признак':<50} {'вес':>5} чем подтверждается")
print(" " + "─" * 100)
for text, weight, evidence in r["за"]:
print(f" {text:<50} {weight:>+5} {evidence}")
for text, weight, evidence in r["против"]:
print(f" {text:<50} {weight:>+5} {evidence}")
print(" " + "─" * 100)
print(f" {'плюсы':<50} {r['плюсов']:>+5}")
print(f" {'минусы':<50} {r['минусов']:>+5}")
print(f" {'СУММА':<50} {r['сумма']:>5}")
print()
print(f" Вывод: {r['вердикт']}")
print(f" Почему: {r['почему']}")
if r["перевес"]:
print()
print(" ВНИМАНИЕ: положительных признаков хватило бы на переход,")
print(" но отрицательные их перевесили. Это и есть смысл чек-листа:")
print(" требование может быть настоящим, а условий для перехода нет.")
if r["код"] == 1:
print()
print(f" {'промежуточный вариант':<38} {'решает':<46} не решает")
print(" " + "─" * 106)
for name, solves, misses in INTERMEDIATE:
print(f" {name:<38} {solves:<46} {misses}")
def main(paths: list[str]) -> int:
results = []
for i, path in enumerate(paths):
if i:
print()
answers = json.loads(Path(path).read_text())
r = evaluate(answers)
report(r)
results.append({"проект": r["проект"], "сумма": r["сумма"],
"вердикт": r["вердикт"], "код": r["код"],
"перевес": r["перевес"]})
print()
print(json.dumps(results, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:] or ["project.json"]))
PY
cat > cost.py <<'PY'
"""Модель стоимости перехода на Kubernetes.
Числа — порядки величин, а не измерения. Ценность модели
в соотношении разовой и постоянной частей, а не в точности.
"""
from __future__ import annotations
import json
# (статья, разово ч, в месяц ч, доля, снимаемая управляемым кластером)
COSTS = [
("Обучение команды", 120, 8, 0.20),
("Перенос приложений", 60, 0, 0.00),
("Манифесты и шаблоны", 40, 4, 0.00),
("Ingress и сертификаты", 16, 2, 0.50),
("Мониторинг и логи", 40, 6, 0.30),
("Обновления кластера", 8, 6, 0.70),
("Резервное копирование etcd", 8, 3, 1.00),
("Отладка сверх обычной", 0, 12, 0.00),
("Дежурство со знанием кластера", 0, 10, 0.00),
]
HOURS_PER_MONTH = 160
def main() -> None:
print(f" {'статья':<32} {'разово':>8} {'в месяц':>9} {'снимает управляемый':>21}")
print(" " + "─" * 74)
once_total = monthly_total = managed_monthly = 0.0
for name, once, per_month, saving in COSTS:
once_total += once
monthly_total += per_month
managed_monthly += per_month * (1 - saving)
share = f"{saving * 100:.0f} %" if saving else "—"
print(f" {name:<32} {once:>8} {per_month:>9} {share:>21}")
print(" " + "─" * 74)
print(f" {'ИТОГО':<32} {once_total:>8.0f} {monthly_total:>9.0f}")
print()
year_own = once_total + monthly_total * 12
year_managed = once_total + managed_monthly * 12
saved = (1 - managed_monthly / monthly_total) * 100
print(f" {'вариант':<24} {'разово':>8} {'в месяц':>9} {'первый год':>12} "
f"{'человеко-мес.':>15}")
print(" " + "─" * 72)
print(f" {'свой кластер':<24} {once_total:>8.0f} {monthly_total:>9.0f} "
f"{year_own:>12.0f} {year_own / HOURS_PER_MONTH:>15.1f}")
print(f" {'управляемый кластер':<24} {once_total:>8.0f} {managed_monthly:>9.0f} "
f"{year_managed:>12.0f} {year_managed / HOURS_PER_MONTH:>15.1f}")
print()
print(f" управляемый снимает {saved:.0f} % ежемесячной нагрузки")
print()
print(" Что он НЕ снимает: перенос приложений, манифесты, отладку,")
print(" дежурство. Это большая часть постоянной стоимости.")
print()
print(f" {'доля постоянной стоимости за год':<44} {'свой':>8} {'управляемый':>14}")
print(" " + "─" * 68)
print(f" {'разовая часть':<44} "
f"{once_total / year_own * 100:>7.0f} % {once_total / year_managed * 100:>13.0f} %")
print(f" {'постоянная часть':<44} "
f"{monthly_total * 12 / year_own * 100:>7.0f} % "
f"{managed_monthly * 12 / year_managed * 100:>13.0f} %")
print()
print(" Постоянная часть больше разовой уже в первый год.")
print(" Довод «поставим заранее, чтобы не переносить потом» неверен:")
print(" ежемесячная стоимость начисляется с первого дня, а разовая")
print(" часть — приведение приложений в порядок — полезна и без кластера.")
print()
print(json.dumps({
"разово_ч": once_total,
"в_месяц_свой": monthly_total,
"в_месяц_управляемый": round(managed_monthly, 1),
"экономия_проц": round(saved),
"год_свой": year_own,
"год_управляемый": round(year_managed),
}, ensure_ascii=False))
if __name__ == "__main__":
main()
PY
# ── три проекта с разными исходами ───────────────────────────────────
cat > small.json <<'EOF'
{
"_name": "Внутренний сервис, три разработчика",
"deploys": true, "managed_state": true, "restartable": true,
"someday": true, "everyone": true, "no_oncall": true, "tolerant": true
}
EOF
cat > growing.json <<'EOF'
{
"_name": "Растущий продукт, четыре машины, есть дежурство",
"downtime": true, "deploys": true, "machines": true,
"expertise": true, "managed_state": true, "restartable": true
}
EOF
cat > platform.json <<'EOF'
{
"_name": "Платформа, шесть команд, два региона",
"load": true, "downtime": true, "deploys": true, "spikes": true,
"teams": true, "machines": true, "regions": true,
"expertise": true, "managed_state": true, "restartable": true
}
EOF
# ── подделка: «всё да», но эксплуатировать некому ────────────────────
cat > fake.json <<'EOF'
{
"_name": "«Нам точно нужен Kubernetes» (проверка на подделке)",
"spikes": true, "deploys": true, "regions": true,
"restartable": true,
"someday": true, "everyone": true, "no_oncall": true, "tolerant": true
}
EOF
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Требование 1: веса чек-листа ═══\n'
python3 - <<'PY'
from decide import QUESTIONS
plus = [w for _, _, w, _ in QUESTIONS if w > 0]
minus = [w for _, _, w, _ in QUESTIONS if w < 0]
print(f" вопросов: {len(QUESTIONS)} (положительных {len(plus)}, "
f"отрицательных {len(minus)})")
print(f" максимум положительных: {sum(plus):+}")
print(f" максимум отрицательных: {sum(minus):+}")
print(f" сильнейший положительный: {max(plus):+}")
print(f" сильнейший отрицательный: {min(minus):+}")
print()
if abs(min(minus)) >= max(plus):
print(" ✓ сильнейший отрицательный признак по модулю не слабее")
print(" сильнейшего положительного — значит, способен перевесить")
else:
print(" ✗ отрицательные признаки слишком слабы")
PY
python3 -c "
from decide import QUESTIONS
plus = [w for _, _, w, _ in QUESTIONS if w > 0]
minus = [w for _, _, w, _ in QUESTIONS if w < 0]
raise SystemExit(0 if abs(min(minus)) >= max(plus) else 1)" \
&& ok "отрицательные веса способны перевесить положительные" \
|| bad "отрицательные веса слишком слабы"
printf '\n═══ Требование 2: три вердикта ═══\n'
python3 - <<'PY'
from decide import VERDICTS
print(f" {'порог':>7} {'код':>5} вердикт")
print(" " + "─" * 62)
for threshold, code, verdict, _ in VERDICTS:
t = "любая" if threshold is None else f"≥ {threshold}"
print(f" {t:>7} {code:>5} {verdict}")
print()
print(" Разные коды возврата позволяют использовать инструмент")
print(" в сценарии: 0 — ничего не менять, 1 — рассмотреть промежуточное,")
print(" 2 — планировать переход.")
PY
n_verdicts="$(python3 -c 'from decide import VERDICTS; print(len(VERDICTS))')"
codes="$(python3 -c 'from decide import VERDICTS; print(len({c for _, c, _, _ in VERDICTS}))')"
[ "$n_verdicts" = "3" ] && [ "$codes" = "3" ] \
&& ok "три вердикта с тремя различными кодами возврата" \
|| bad "вердиктов: $n_verdicts, кодов: $codes"
printf '\n═══ Требование 3: три проекта ═══\n'
python3 decide.py small.json growing.json platform.json > verdicts.log 2>&1
sed -n '1,/Почему/p' verdicts.log | sed 's/^/ /'
summary="$(tail -1 verdicts.log)"
python3 - <<'PY' < verdicts.log
import json, sys
lines = sys.stdin.read().splitlines()
results = json.loads(lines[-1])
print()
print(f" {'проект':<48} {'сумма':>6} {'код':>5} вердикт")
print(" " + "─" * 108)
for r in results:
print(f" {r['проект']:<48} {r['сумма']:>6} {r['код']:>5} {r['вердикт']}")
codes = {r["код"] for r in results}
print()
print(f" различных исходов: {len(codes)} из 3")
PY
n_codes="$(echo "$summary" | python3 -c "
import json, sys
print(len({r['код'] for r in json.load(sys.stdin)}))")"
[ "$n_codes" = "3" ] \
&& ok "три проекта дали три разных исхода" \
|| bad "различных исходов: $n_codes"
printf '\n═══ Требование 4: промежуточные варианты ═══\n'
sed -n '/промежуточный вариант/,/Управляемый Kubernetes/p' verdicts.log | sed 's/^/ /'
grep -q "Управляемый Kubernetes" verdicts.log \
&& ok "для промежуточного исхода выведены варианты между Compose и кластером" \
|| bad "варианты не выведены"
printf '\n═══ Требования 5 и 6: стоимость ═══\n'
python3 cost.py > cost.log 2>&1
cat cost.log | sed 's/^/ /'
c_summary="$(tail -1 cost.log)"
get() { echo "$c_summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['$1'])"; }
once="$(get разово_ч)"; per_month="$(get в_месяц_свой)"
saving="$(get экономия_проц)"; year="$(get год_свой)"
printf '\n разово: %s ч, в месяц: %s ч, за год: %s ч\n' "$once" "$per_month" "$year"
python3 -c "raise SystemExit(0 if ${per_month} * 12 > ${once} else 1)" \
&& ok "постоянная часть превышает разовую уже в первый год" \
|| bad "постоянная часть меньше разовой"
printf ' управляемый кластер снимает: %s %%\n' "$saving"
python3 -c "raise SystemExit(0 if 0 < ${saving} < 50 else 1)" \
&& ok "экономия существенна, но далека от половины — отладка и дежурство остаются" \
|| bad "оценка экономии: $saving %"
printf '\n═══ Требование 7: проверка на подделке ═══\n'
python3 decide.py fake.json > fake.log 2>&1
sed -n '1,/чек-листа/p' fake.log | sed 's/^/ /'
f_summary="$(tail -1 fake.log)"
f_code="$(echo "$f_summary" | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['код'])")"
f_over="$(echo "$f_summary" | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['перевес'])")"
printf '\n код исхода: %s, сработал перевес: %s\n' "$f_code" "$f_over"
[ "$f_code" = "0" ] && [ "$f_over" = "True" ] \
&& ok "подделка отсеяна: положительных хватало, отрицательные перевесили" \
|| bad "код=$f_code перевес=$f_over"
printf '\n═══ Что НЕ проверялось ═══\n'
python3 - <<'PY'
NOT_RUN = [
("фактическая стоимость перехода", "зависит от проекта, команды, провайдера"),
("работа кластера", "kubectl и кластер недоступны"),
("веса чек-листа", "подобраны по опыту, а не измерены"),
("экономия управляемого кластера", "оценка долей, не замер"),
]
print(f" {'проверка':<40} причина")
print(" " + "─" * 80)
for name, why in NOT_RUN:
print(f" {name:<40} {why}")
print(f"\n не выполнялось: {len(NOT_RUN)}")
print()
print(" Проверено то, что проверяется на модели: устройство весов,")
print(" различимость исходов, поведение на подделке, соотношение")
print(" разовой и постоянной частей. Сами числа — порядки величин.")
print()
print(" Это существенная оговорка: инструмент помогает вести разговор")
print(" о фактах, но не заменяет измерений на конкретном проекте.")
PY
ok "невыполненное перечислено; числа названы моделью"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
echo " примечание: кластер не использовался; веса и стоимость — модель"
cd /tmp && rm -rf /tmp/orch
exit "$fail"
Ожидаемый вывод:
═══ Требование 1: веса чек-листа ═══
вопросов: 14 (положительных 10, отрицательных 4)
максимум положительных: +19
максимум отрицательных: -10
сильнейший положительный: +3
сильнейший отрицательный: -3
✓ сильнейший отрицательный признак по модулю не слабее
сильнейшего положительного — значит, способен перевесить
✓ отрицательные веса способны перевесить положительные
═══ Требование 2: три вердикта ═══
порог код вердикт
──────────────────────────────────────────────────────────────
≥ 8 2 Kubernetes обоснован
≥ 4 1 Промежуточный вариант; пересмотреть через полгода
любая 0 Compose достаточно
Разные коды возврата позволяют использовать инструмент
в сценарии: 0 — ничего не менять, 1 — рассмотреть промежуточное,
2 — планировать переход.
✓ три вердикта с тремя различными кодами возврата
═══ Требование 3: три проекта ═══
проект: Внутренний сервис, три разработчика
признак вес чем подтверждается
────────────────────────────────────────────────────────────────────────────────────────────────────
Развёртывания чаще раза в день +2 история выкаток
Состояние вынесено в управляемые сервисы +1 список хранилищ
Приложения переживают перезапуск в любой момент +1 проверено, а не предполагается
Главный довод: «понадобится масштабирование» -2 данных о росте нет
Единственный довод: так делают все -3 довод не о системе
Некому дежурить со знанием кластера -3 кластер станет одной точкой отказа
Простой в час допустим -2 автоматическое восстановление не окупится
────────────────────────────────────────────────────────────────────────────────────────────────────
плюсы +4
минусы -10
СУММА -6
Вывод: Compose достаточно
Почему: перенос обойдётся дороже пользы
проект сумма код вердикт
────────────────────────────────────────────────────────────────────────────────────────────────────────────
Внутренний сервис, три разработчика -6 0 Compose достаточно
Растущий продукт, четыре машины, есть дежурство 7 1 Промежуточный вариант; пересмотреть через полгода
Платформа, шесть команд, два региона 19 2 Kubernetes обоснован
различных исходов: 3 из 3
✓ три проекта дали три разных исхода
═══ Требование 4: промежуточные варианты ═══
промежуточный вариант решает не решает
──────────────────────────────────────────────────────────────────────────────────────────────────────────
Вторая машина с Compose вручную переживание отказа одной машины автоматики нет
Балансировщик перед двумя машинами доступность при отказе состояние, выкатка
Управляемая база вне кластера копии, обновления, отказоустойчивость хранилища стоимость сервиса
Nomad оркестрация с меньшей сложностью меньше экосистема
Управляемый Kubernetes control plane обслуживает провайдер знания всё равно нужны
✓ для промежуточного исхода выведены варианты между Compose и кластером
═══ Требования 5 и 6: стоимость ═══
статья разово в месяц снимает управляемый
──────────────────────────────────────────────────────────────────────────
Обучение команды 120 8 20 %
Перенос приложений 60 0 —
Манифесты и шаблоны 40 4 —
Ingress и сертификаты 16 2 50 %
Мониторинг и логи 40 6 30 %
Обновления кластера 8 6 70 %
Резервное копирование etcd 8 3 100 %
Отладка сверх обычной 0 12 —
Дежурство со знанием кластера 0 10 —
──────────────────────────────────────────────────────────────────────────
ИТОГО 292 51
вариант разово в месяц первый год человеко-мес.
────────────────────────────────────────────────────────────────────────
свой кластер 292 51 904 5.7
управляемый кластер 292 43 805 5.0
управляемый снимает 16 % ежемесячной нагрузки
Что он НЕ снимает: перенос приложений, манифесты, отладку,
дежурство. Это большая часть постоянной стоимости.
доля постоянной стоимости за год свой управляемый
────────────────────────────────────────────────────────────────────
разовая часть 32 % 36 %
постоянная часть 68 % 64 %
Постоянная часть больше разовой уже в первый год.
...
разово: 292.0 ч, в месяц: 51.0 ч, за год: 904.0 ч
✓ постоянная часть превышает разовую уже в первый год
управляемый кластер снимает: 16 %
✓ экономия существенна, но далека от половины — отладка и дежурство остаются
═══ Требование 7: проверка на подделке ═══
проект: «Нам точно нужен Kubernetes» (проверка на подделке)
признак вес чем подтверждается
────────────────────────────────────────────────────────────────────────────────────────────────────
Развёртывания чаще раза в день +2 история выкаток
Нагрузка меняется в разы в течение суток +2 график запросов
Нужны несколько географических регионов +2 требование, а не желание
Приложения переживают перезапуск в любой момент +1 проверено, а не предполагается
Главный довод: «понадобится масштабирование» -2 данных о росте нет
Единственный довод: так делают все -3 довод не о системе
Некому дежурить со знанием кластера -3 кластер станет одной точкой отказа
Простой в час допустим -2 автоматическое восстановление не окупится
────────────────────────────────────────────────────────────────────────────────────────────────────
плюсы +7
минусы -10
СУММА -3
Вывод: Compose достаточно
Почему: перенос обойдётся дороже пользы
ВНИМАНИЕ: положительных признаков хватило бы на переход,
но отрицательные их перевесили. Это и есть смысл чек-листа:
требование может быть настоящим, а условий для перехода нет.
код исхода: 0, сработал перевес: True
✓ подделка отсеяна: положительных хватало, отрицательные перевесили
═══ Что НЕ проверялось ═══
проверка причина
────────────────────────────────────────────────────────────────────────────────
фактическая стоимость перехода зависит от проекта, команды, провайдера
работа кластера kubectl и кластер недоступны
веса чек-листа подобраны по опыту, а не измерены
экономия управляемого кластера оценка долей, не замер
✓ невыполненное перечислено; числа названы моделью
═══ ИТОГ ═══
все требования выполнены
примечание: кластер не использовался; веса и стоимость — модель
Все требования выполнены; кластер не использовался, веса и стоимость названы моделью.
Требование 7 — самая содержательная проверка. Проект «Нам точно нужен Kubernetes» набрал +7 положительных: развёртывания частые, нагрузка скачет, регионов несколько. Каждый признак настоящий. И всё же исход — «Compose достаточно», потому что дежурить со знанием кластера некому, а простой в час допустим.
Это и есть смысл чек-листа: требование может быть настоящим, а условий для перехода не быть.
Три решения, определяющие качество.
Отрицательные веса по модулю равны сильнейшим положительным. Если бы они были слабее (скажем, −1), проект-подделка набрал бы проходной балл на одних ожиданиях. Проверка требования 1 сравнивает max(положительных) с abs(min(отрицательных)) — то есть проверяет само устройство инструмента, а не его вывод на удобном примере.
К каждому вопросу приложено «чем подтверждается». Столбец превращает опрос в проверку фактов: «есть человек с опытом» подтверждается конкретным именем, а не намерением нанять. Без этого столбца чек-лист заполняется по ощущениям и всегда даёт нужный ответ.
Стоимость разделена на разовую и постоянную, и постоянная больше. Это опровергает распространённый довод «поставим заранее». Разовая часть (292 ч) выглядит внушительно, но за год постоянная даёт 612 ч — вдвое больше, — и начисляется с первого дня.
Чего решение не делает. Числа — модель. Веса подобраны по опыту, не измерены; стоимость в часах — порядок величины, зависящий от проекта, команды и провайдера. Экономия управляемого кластера (16 %) получена из назначенных долей, а не из замеров, и на конкретном проекте будет другой. Кластер не использовался: ни один вывод не проверен на работающей системе. Инструмент полезен как способ вести разговор о фактах, а не как источник ответа: если после его заполнения спор продолжается, спорят уже о конкретных строках, и это лучше, чем о моде.
Проверка результата
Заполните чек-лист для своего проекта и запишите ответ на три вопроса:
1. Какой признак дал наибольший положительный вес и чем он подтверждается?
2. Есть ли отрицательные признаки, и что нужно изменить, чтобы они исчезли?
3. Что произойдёт с системой, если ничего не менять ещё полгода?
Если на второй вопрос ответ «нанять человека» — это и есть первая задача, а не перенос.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| «Docker или Kubernetes» | Инструменты кажутся конкурентами | Вопрос ставится как «хватает ли одной машины» |
| Переход по ожиданию роста | Кривая роста воображаемая | Указать на данные за последний год |
| Установка «заранее, чтобы не переносить потом» | Кажется экономией | Постоянная стоимость начисляется с первого дня |
| Игнорирование дежурства | Видна только стоимость установки | Кластер требует людей, знающих кластер |
| Пропуск промежуточных вариантов | Известны только два полюса | Балансировщик, управляемая база, Nomad |
| Ожидание, что управляемый кластер снимет всё | Провайдер обслуживает control plane | Отладка и дежурство остаются полностью |
| Учёт только разовой стоимости | Она заметнее | За год постоянная обычно больше |
| Оценка по числу сервисов | Понятная метрика | Значение имеет число машин и цена простоя |
| Перенос базы данных в кластер вместе со всем | Кажется естественным | Управляемая база часто снимает саму потребность |
Контрольные вопросы
На понимание:
- Как правильно поставить вопрос вместо «Docker или Kubernetes»?
- Почему
restart: unless-stoppedне заменяет orchestration? - Какие статьи расходов постоянные, а какие разовые?
- Что именно снимает управляемый кластер, а что нет?
- Почему отладка в Kubernetes дороже?
На применение:
- Как проверить подлинность требования «нам понадобится масштабирование»?
- Какие промежуточные варианты существуют между Compose и кластером?
- Что нужно изменить, чтобы отрицательные признаки чек-листа исчезли?
На диагностику:
- Проект набрал 7 положительных баллов и −10 отрицательных. Вывод и что делать?
- Команда перешла на Kubernetes, эксплуатационная нагрузка выросла втрое. Гипотеза?
Краткое резюме
- Верный вопрос — не «Docker или Kubernetes», а «хватает ли одной машины».
- Compose достаточно, пока одна машина справляется и простой в несколько минут допустим.
- Compose перестаёт хватать при нескольких узлах, недопустимом простое и частых выкатках.
- Каждый признак перехода должен быть наблюдаемым фактом, а не ожиданием.
- Стоимость Kubernetes состоит из разовой и постоянной частей; постоянная больше уже за год.
- Кластер требует людей, знающих кластер; один такой человек — организационная точка отказа.
- Отладка дороже, потому что между симптомом и причиной больше звеньев.
- Управляемый кластер снимает работу по control plane — заметную, но не главную часть.
- Между Compose и полным кластером есть несколько ступеней, которые часто пропускают.
- Вынос состояния в управляемые сервисы иногда снимает саму потребность в кластере.
- Отрицательные признаки чек-листа важнее положительных: они отсекают решения по инерции.
- Требование может быть настоящим, а условий для перехода не быть.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Kubernetes: что это | https://kubernetes.io/docs/concepts/overview/ | Задача поддержания состояния |
| Kubernetes: контроллеры | https://kubernetes.io/docs/concepts/architecture/controller/ | Цикл сравнения состояний |
| Kubernetes: рекомендации по кластерам | https://kubernetes.io/docs/setup/production-environment/ | Что входит в эксплуатацию |
| Compose Specification | https://github.com/compose-spec/compose-spec/blob/main/spec.md | Границы Compose |
| Docker Swarm | https://docs.docker.com/engine/swarm/ | Промежуточный вариант |
| Nomad | https://developer.hashicorp.com/nomad/docs | Альтернативная оркестрация |
| Kubernetes: автомасштабирование | https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ | Условия применимости |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление