Как мы не выбрали K8s, остались эффективными и повысили производительность
Расскажу про требования, которые мы сформулировали, почему в итоге не «поставили кубер», и как построили эксплуатацию на стандартных инструментах Linux.
Похожая тема, но с другим фокусом и для другой аудитории (DevOpsConf) — в материале «Эксплуатация без K8s».
TL;DR
- Сначала требования, потом инструменты: релизы и откаты, наблюдаемость, безопасность, отказоустойчивость, стоимость владения.
- Унификация сервисов: один сервис — один бинарник, единые правила для метрик, логов и конфигурации.
- Доставка как у обычного софта: пакеты (deb/rpm) + репозиторий + systemd unit’ы.
- Наблюдаемость не зависит от Kubernetes: метрики, централизованные логи, алерты.
- Масштабирование — это планирование и группы серверов, а не «автоскейлинг ради автоскейлинга».
- Отказоустойчивость можно делать на уровне сети (DNS/BGP/anycast) и простыми схемами внутри сервисов.
Вступление: почему вообще нужно говорить про Kubernetes
Про Kubernetes часто говорят: «Kubernetes — это новый Linux. Поставьте его, и дальше всё будет хорошо». Частично это правда: он действительно даёт много важных вещей «из коробки» — оркестрацию, самовосстановление, автоскейлинг, сетевые абстракции.
Но есть нюанс: сложность системы вы платите каждый день — расследованиями, инцидентами, онбордингом людей и стоимостью владения. Поэтому мы сначала честно ответили на вопрос: что нам на самом деле нужно от инфраструктуры, а уже потом выбирали инструменты.
Контекст: откуда мы стартовали
Мы делаем видео‑платформу: большой трафик, много запросов, много данных. В таких системах надёжность и предсказуемость — не «приятный бонус», а часть продукта.
Мы стартовали не «с нуля»: до Kinescope были другие проекты, наработки и легаси‑код. Одна из типичных проблем легаси — зоопарк языков и способов доставки.
Исторически у нас было два основных стека: Go и Ruby (вместо Ruby мог быть любой скриптовый язык с кучей зависимостей). Go удобно: собрал один бинарник — и поехали1. Скриптовые языки часто тянут системные зависимости, и естественная упаковка для них — Docker.
Дальше цепочка выглядит очевидной: если уже есть Docker, почему бы не «закатить в Kubernetes»? Так у многих и происходит. В какой-то момент Kubernetes у нас действительно был — но он потреблял ресурсы, иногда ломался, и диагностика превращалась в очень сложный процесс.
На фоне роста системы нам нужно было не «ещё один слой», а наоборот — больше предсказуемости и контроля.
Какие требования мы сформулировали
У любого проекта есть функциональные требования (что делает продукт) и нефункциональные (как он живёт в продакшене). Если свести нефункциональные требования к практическому списку, обычно получается так:
- Простота и предсказуемость: минимум скрытой логики, прозрачная диагностика.
- Релизы и откаты: быстро, воспроизводимо, с понятным версионированием.
- Масштабирование и стоимость: уметь расти по нагрузке и не «съедать» маржинальность.
- Доступность и отказоустойчивость: система должна переживать деградации и отказы узлов.
- Безопасность: ограничения на уровне ОС и сетевых политик, минимальные права.
- Наблюдаемость (observability): метрики, логи, алерты — чтобы понимать, что происходит.
Отдельно полезно разделить требования двух сторон:
- Эксплуатации нужна унификация: одинаковые порты и метрики, конфигурация через переменные окружения, одинаковый формат сервиса.
- Разработке нужна автономность: не ждать админа, не запрашивать логи и мониторинг вручную, уметь выкатываться самостоятельно.
Почему мы не выбрали Kubernetes
Kubernetes действительно закрывает часть требований «из коробки»: оркестрацию, автоскейлинг, self-healing, сетевые абстракции. Но для нашего кейса стоимость сложности и дополнительного слоя оказалась выше пользы:
- Сложность и диагностика: чем больше компонентов — тем больше точек отказа и непредсказуемости при расследованиях.
- Накладные расходы: дополнительные демоны и контуры, сеть, контрольная плоскость, оверхед по железу.
- Экспертиза: большой инструмент требует устойчивой экспертизы и поддержки.
Если сильно упростить мысль: у нас уже есть «старый Linux», и мы не увидели необходимости добавлять «новый Linux поверх старого Linux», если базовые требования можно закрыть средствами ОС.
Практическая заметка. При выборе инструментов для инфраструктуры важно понимать: каждый дополнительный слой абстракции добавляет сложность и точки отказа. Kubernetes решает много задач «из коробки», но для многих случаев достаточно стандартных инструментов Linux. Важно сначала сформулировать требования, а потом выбирать решения, а не наоборот. Источник: опыт эксплуатации production-инфраструктуры без Kubernetes.
Базовый принцип: чтобы сделать надёжно — убери ненадёжное
Формулировка звучит грубо, но смысл простой: чем больше компонентов в системе, тем больше точек отказа, больше экспертизы нужно для поддержки и выше стоимость владения.
Если упростить до минимума, который нам точно нужен, останется примерно это:
- Linux (операционная система всё равно нужна, без неё никак)
- Go (как основной стек: удобно собирать, доставлять и поддерживать)
Дальше задача — построить вокруг этого понятный жизненный цикл софта: собрать → доставить → запустить → мониторить → обновить/откатить.
Решение: один сервис — один бинарник
Мы пошли немного другим путём: перестали строить «платформу для сервисов, которая может всё что угодно», и вместо этого привели сами сервисы к нужному состоянию.
Один бинарник
Один сервис должен быть одним бинарником со стандартными метриками и всем остальным. Если где-то в легаси это не укладывалось (например, Ruby) — мы постепенно выравнивали стек (переписывали на Go).
Единые правила эксплуатации
Мы стараемся, чтобы у всех сервисов была одинаковая «оболочка»:
- конфигурация через переменные окружения;
- метрики и health‑эндпоинты в едином формате;
- стандартные логи.
Отдельно приятно, что «один бинарник» работает не только для API. В эту же модель хорошо укладываются «не‑сервисы»: сайт, админка, плеер. Мы пакуем статику внутрь бинарника, и для эксплуатации всё выглядит одинаково: неважно, это player, API или условный DNS‑сервер.
Почему не Docker
Docker часто предлагают как «универсальную упаковку»: удобно, изоляция ресурсов, переносимость. Но у этого есть цена — накладные расходы и усложнение диагностики (особенно по сети).
В нашем кейсе хотелось проверенного и минимального решения. Docker не был «абсолютным злом», но оказался лишним слоем: слишком большой, необязательный, и местами добавляющий потери производительности.
Доставка и запуск: пакеты + systemd
У любого «обычного» софта есть жизненный цикл: собрать → протестировать → доставить → установить → обновить/откатить. В Linux это давно решено:
Жизненный цикл сервиса
development
|
v
build (go build)
|
v
package (deb/rpm)
|
v
repository
|
v
server group
| |
| +---> apt/yum install
| |
| +---> systemd unit
| |
| +---> systemctl start/restart
| |
| +---> monitoring/metrics
| |
| +---> rollback (if needed)
Стандартные инструменты ОС покрывают весь цикл: сборка, доставка, запуск, мониторинг, откат.
- пакетный менеджер (deb/rpm и репозиторий) — для доставки и версионирования;
- systemd — для запуска, рестартов, ограничений ресурсов и задач по расписанию.
Сборка пакетов
Пакеты собираем через nfpm: описываем содержимое и метаданные в YAML и получаем deb/rpm под нужные дистрибутивы.
Отдельный принцип: мы не хотим собирать deb‑пакеты «ради того, чтобы упаковать их в Docker, а потом в Kubernetes». Это выглядело бы странно — и технически, и по смыслу.
Что даёт systemd
- Видимость: сервисы видны системе, можно мониторить статусы и ресурсы.
- Изоляция ресурсов: CPU/memory limits, пермишены, sandboxing.
- Таймеры: задачи по расписанию (альтернатива cron) без отдельного «зоопарка».
Почему это похоже на Kubernetes (и почему нам этого достаточно)
Если вы посмотрите на systemd и на Kubernetes без религиозной составляющей, то заметите: многие идеи похожи.
- Есть описание сервиса (юнит или манифест)
- Есть политика перезапуска
- Есть лимиты по ресурсам
- Есть обвязка вокруг наблюдаемости
Разница в уровне абстракции. Kubernetes делает это «поверх», добавляя свой control plane, сеть, API и экосистему. Это может быть оправдано. В нашем случае оказалось, что базового уровня ОС достаточно, а вот лишний слой усложняет эксплуатацию.
Наблюдаемость: метрики, логи, алерты
Наблюдаемость не привязана к Kubernetes.
- Метрики: сервисы отдают стандартные метрики, которые подхватываются мониторингом.
- Логи: сервисы пишут в stdout/файлы, дальше — централизованный сбор и просмотр в Grafana (например, через Loki).
- Алерты: основная реакция — по алертам, а не «пялиться в графики».
Помимо логов приложений есть логи и метрики «внешнего софта» (БД, nginx и т. п.). Для него мы используем экспортеры и сбор логов так же, как и все.
Отдельный пример из жизни: если нам критично анализировать запросы к базе данных, это тоже можно сделать без Kubernetes. В одном из кейсов мы поднимали свой UDP‑сервер с простым протоколом, слали туда информацию о запросах из драйвера, складывали в ClickHouse и смотрели из Grafana.
Подробнее о практической настройке мониторинга, типовых проблемах и их решении см. в статье «Эксплуатация без K8s».
Автоматизация: Ansible и снижение человеческого фактора
Автоматизация нужна всегда — даже если у вас один сервер. Если хост сгорит, руками вы не восстановите «точно как было».
Для автоматизации мы используем Ansible. Классическая проблема Ansible — со временем в нём копится мусор и технический долг, поэтому playbook’и периодически нужно чистить.
Схема работы у нас примерно такая:
- Эксплуатация накатывает базовую роль на новый сервер (сеть, базовые настройки, доступы, мониторинг и логирование).
- Разработка отвечает за свои сервисы: роли, юниты и роутинг. Разработчик может добавить сервис «с коммита» и выкатить его без участия админа.
“Manifest style” без Kubernetes: один spec‑файл на сервис
Человеческий фактор всё равно остаётся: можно забыть лимиты, мониторинг, какой-нибудь обязательный параметр. Чтобы снизить это, мы используем единый файл спецификации — spec.yml — который описывает весь жизненный цикл сервиса.
Идея: один файл в репозитории сервиса описывает всё, что нужно для его эксплуатации. Это похоже на манифест Kubernetes, но без самого Kubernetes. Из этого файла автоматически генерируется deb‑пакет (через утилиту kinectl, которая использует nfpm как библиотеку), Ansible playbook для развёртывания, systemd unit файл и конфигурация для мониторинга.
Пример spec.yml
Вот как выглядит типичный spec.yml для сервиса:
service:
name: my-api-service
group: applications
description: "API сервис"
deploy:
serial: 1 # Развёртывание по одному хосту (canary)
probe:
type: "http"
port: 9090
path: /metrics
limits:
mem: 1G
no_file: 10_000
resources:
- postgres
- sentinel
- nats
environments: |
HTTP_ADDRESS=:8080
LOG_LEVEL=info
SECRET_KEY={{ vault_secret_key_encrypted }}
Что это даёт
- Единая точка истины: вся информация о сервисе в одном месте, версионируется вместе с кодом
- Меньше ошибок: утилита может проверить обязательные поля и подставить дефолты (например, если забыли указать лимиты)
- Автоматизация: из одного файла генерируется всё необходимое для деплоя
- Простота: разработчик не должен знать детали Ansible или systemd — достаточно описать сервис в spec.yml
Если разработчик забыл указать лимиты — утилита может подставить дефолты. Идея простая: снизить вероятность случайных ошибок и сделать процесс развёртывания более предсказуемым.
Подробнее о формате spec.yml и практических примерах см. в статье «Эксплуатация без K8s».
Масштабирование и отказоустойчивость
Что такое масштабирование
Мы различаем:
- вертикальное: увеличить ресурсы одного хоста (в облаке часто делается «ручкой»);
- горизонтальное: добавить хосты в группу.
Мы не считаем «виртуальное масштабирование» (много сущностей на одном железе ради отчёта) полезным: железо всё равно куплено, место и энергия заняты, значит экономии нет — только усложнение.
Группы серверов
Серверы разделены на группы по задачам (application/CDN/transcoding и т. п.). В каждой группе — больше одного хоста. Не хватает — добавляем серверы в группу (планово).
Отказоустойчивость: DNS/BGP/anycast
Для внешнего HTTP‑трафика схема двухуровневая:
- DNS направляет в регион или точку присутствия.
- Внутри точки — балансировка по группе.
На уровне сети используем BGP/anycast: если хост «падает» или его выводят из эксплуатации, он быстро выпадает из раздачи; когда поднимается — возвращается в роутинг.
Балансировка «на клиенте»
Для внутреннего трафика (межсервисное общение) мы избегаем выделенной сущности, которая становится точкой отказа. Часто проще и надёжнее, когда клиенты знают несколько адресов и балансируют на своей стороне.
Вопросы из зала
Релизы с downtime или без?
Релизов у нас много: в день может быть 20–30 выкаток. Поэтому «предупреждать клиентов о релизах» мы не можем просто организационно.
При этом релизы стараемся делать без даунтайма:
- Для внешних сервисов, которые должны быть доступны всегда, трафик уводится на уровне сети: BGP‑сессия опускается, и нода почти мгновенно выпадает из раздачи (текущие сессии обрываются и клиенты переподключаются на другие ноды).
- Если это сервис за прокси или балансировщиком, переключение происходит на уровне прокси и health‑политик: бинарник остановился — трафик ушёл на другие инстансы, поднялся — вернулся.
Даунтайм у нас бывает не из-за схемы выкладки, а из-за ошибок релиза — «накосячили, выкатили фигню, что-то поехало».
Как вы мониторите «зоопарк» сервисов?
Метрики и централизованные логи (Loki/Grafana). Экспортеры для стороннего софта. В графики постоянно никто не смотрит — живём по алертам.
Одинаковая ли версия ОС на всех серверах?
Не обязательно. Для Go‑сервисов это обычно не проблема (минимум зависимостей). Для софта с системными зависимостями могут быть разные сборки или пакеты под разные версии ОС.
Где храните секреты?
Секреты мы не хотим «размазывать» по серверам и файлам конфигурации. Если есть что-то действительно секретное, оно хранится централизованно (например, в базе данных с сегрегацией доступа). Доступ на прод ограничен, и в идеале разработчику он вообще не нужен: диагностика выполняется через метрики и логи.
Как это работает технически: В конфигах и переменных окружения все чувствительные данные зашифрованы через Ansible Vault и закодированы в base64. При сборке бинарника в него вшивается пароль для расшифровки (через библиотеку gitlab.kinescope.dev/go/vault). При запуске приложение автоматически расшифровывает переменные окружения, используя вшитый пароль. Это позволяет хранить зашифрованные секреты в spec.yml и переменных окружения, не раскрывая их в открытом виде на серверах.
Вы считали стоимость vs Kubernetes?
То, чего нет, обычно дешевле в поддержке. В облаке и при других вводных баланс может быть иным: можно «платить деньгами», снижая требования к людям и железу. На своём железе и при большом трафике экономика часто другая.
Заключение
Главная мысль не в том, что «Kubernetes плохой». Главная мысль в том, что:
- нельзя выбирать инструмент раньше требований;
- для части компаний Kubernetes — отличный ответ (особенно в облаке);
- для части компаний (как у нас) проще и дешевле закрыть требования средствами ОС: пакеты, systemd, Ansible и минимальное количество дополнительных компонентов.
Полезные материалы
- systemd: документация и примеры — официальная документация по systemd unit-файлам, ограничениям ресурсов и безопасности
- Ansible: документация и best practices — официальная документация Ansible с примерами playbook’ов и ролей
- NFPM: создание пакетов для Linux — инструмент для создания deb/rpm пакетов из YAML-описания
- systemd: изоляция ресурсов и безопасность — как настроить лимиты CPU, памяти и другие ограничения через systemd
- Наблюдаемость без Kubernetes — Loki для централизованного сбора логов и Grafana для визуализации метрик
- Prometheus: мониторинг и метрики — система мониторинга, которая работает независимо от оркестраторов
- BGP и anycast для отказоустойчивости — как использовать BGP/anycast для маршрутизации трафика
- Простота vs сложность в инфраструктуре — примеры того, как излишняя сложность может привести к проблемам
- Kubernetes: когда он оправдан — официальная документация Kubernetes (полезно для понимания, когда он действительно нужен)
- Go: простота эксплуатации — как Go упрощает доставку и эксплуатацию приложений
Практическая заметка. При выборе инструментов для инфраструктуры важно сначала сформулировать требования, а потом выбирать решения. Kubernetes решает много задач «из коробки», но добавляет сложность и накладные расходы. Для многих случаев достаточно стандартных инструментов Linux. Источник: опыт эксплуатации production-инфраструктуры без Kubernetes.
Сноски
О преимуществах Go для бизнеса и эксплуатации см. «Профит для компании от Go». ↩︎