Эксплуатация без K8s
Расскажу о нашем опыте эксплуатации production-инфраструктуры без Kubernetes в Kinescope. Мы отказались от K8s около трёх лет назад и построили простую, надёжную и эффективную систему на базе стандартных инструментов Linux: systemd, пакетных менеджеров и Ansible.
В статье описаны причины отказа от Kubernetes, требования к системе, выбор решений и практический опыт эксплуатации более 50 серверов в 4 дата-центрах силами одного администратора.
TL;DR
Основные выводы из нашего опыта эксплуатации без Kubernetes:
- Простота важнее сложности — стандартные инструменты Linux (systemd, пакетные менеджеры) решают большинство задач без дополнительных слоёв абстракции
- Один бинарник — один сервис — все сервисы собираются в один бинарный файл, что упрощает доставку и эксплуатацию
- Автоматизация через Ansible — вся инфраструктура описывается кодом, разработчики не зависят от админов
- Масштабирование через группы серверов — простое горизонтальное масштабирование без оркестраторов
- Отказоустойчивость через DNS и BGP — маршрутизация трафика на уровне сети, а не приложений
- Меньше компонентов — меньше проблем — за три года ничего не упало по вине systemd или пакетных менеджеров
Контекст: что было и что стало
Сейчас у нас нет Kubernetes и Docker в продакшене. Раньше они были, но мы не занимались их выпиливанием специально — переход произошёл естественным образом. Сейчас у нас один админ, один DevOps-инженер, мы находимся в 4 дата-центрах (два в России, один в Европе, один в США, и в Азии тоже планируем), и чуть больше 50 серверов.
Когда-то давно у нас был Kubernetes, и мы без него живём уже около трёх лет (может быть чуть меньше). Он стоял, что-то делал, ел ресурсы, иногда ломался, и мы не понимали, что там сломалось. Вроде всё работало, но починить это было зачастую очень сложно — админ занимался этим. У нас было тогда даже два админа.
Мне очень не нравилось то, что происходило, потому что не было никакого контроля. Всё усложнялось тем, что было два дата-центра — соответственно, несколько кластеров Kubernetes, и это создавало дополнительные сложности.
Мы не хотели ни от чего отказываться, но хотели больше стабильности в системе.
Что мы хотели от системы
Прежде чем решать проблемы с Kubernetes как таковым, мы хотели задаться вопросом: а что мы на самом деле хотим от наших систем?
Требования эксплуатации
Инженер, который занимается эксплуатацией, — это не разработчик. Он не знает, что находится в сервисе, и ему не нужно знать, что сервис делает и как он работает. У нас около 40 сервисов, может больше.
Эксплуатация хотела, чтобы сервис:
- Представлял какие-то порты наружу (если он работает по HTTP — это не так важно)
- Обязательно имел порт для метрик
- Хранил параметры через переменные окружения (все конфиги должны быть через переменные окружения, чтобы не было конфигурационных файлов по возможности)
- Не сильно отличался от других сервисов (зачем использовать множество разных сервисов, которые по-разному настраиваются и эксплуатируются — это будет сложно, дорого и неэффективно)
Требования разработки
Разработка, естественно, хотела:
- Не зависеть от эксплуатации — не находиться в состоянии заложников
- Не просить добавить какой-то сервис или выкатить куда-то
- Не выпрашивать логи, мониторинг и всё остальное — обычные бытовые проблемы, которые у некоторых остаются проблемами
Общие требования
Что мы хотели от системы в целом:
- Автоматизация — она была, но хотелось больше
- Масштабирование — система должна масштабироваться
- Отказоустойчивость — система должна быть отказоустойчивой
- Единый формат пакетов и доставки — это важная часть: мы деплоим каждый день, бывает по 10–20 релизов
- Персистентность сервиса — мы хотим собрать сервис ровно в таком же состоянии, протестировать и потом выкатить на продакшн
- Изоляция ресурсов — на одной машине может быть более чем один сервис
- Безопасность — это важно
- Производительность — у нас были проблемы с производительностью Kubernetes, он ломался, это выходило дорого по железу
Решение: один бинарник — один сервис
Мы пошли немного другим путём: мы стали не пытаться сделать платформу для сервисов, которая могла бы всё что угодно, а решили дотащить сами сервисы до нужного состояния.
Требования к сервисам
Один сервис должен быть одним бинарником со стандартными метриками и всем остальным. У нас были сервисы, написанные на Ruby — «один сервис — один бинарник» им, соответственно, не подходил. Сервисы, которые оставались на Ruby, мы тоже переписали на Go1, чтобы можно было их собирать.
Сейчас мы собираем не только сервисы, которые просто сервисы. У нас, например, есть плеер (JS-файлы), есть сайт, есть админка — всё это мы тоже пакуем в один бинарник вместе со статикой. То есть все сервисы одинаковые с точки зрения эксплуатации — неважно, что это: player, API или DNS-сервер. Это всё одинаково с точки зрения эксплуатации и доставки.
Единый формат метрик
Метрики должны быть стандартизированы, но всегда должна быть как минимум одна, которая описывает приложение: версия, время сборки и название. Так мы его всегда видим.
В итоге мы пришли к тому, что все сервисы одинаковые — они никак не отличаются с точки зрения эксплуатации. На выходе у нас получился один бинарник, и жизнь стала с этим легче.
Почему не Docker?
Плюсы не только в том, что бинарник можно запустить вручную без проблем. Нужно доставить и запустить сервис. Для этого многие говорят, что нужен Docker: потому что Docker удобен, Docker «пакует», у Docker есть изоляция ресурсов и другие возможности.
Но помимо всего прочего у Docker есть накладные расходы. Плюс усложняется сеть и диагностика. Нам нужно проверенное и надёжное решение, а Docker в нашем кейсе таким не показался: не то чтобы были конкретные проблемы, скорее он слишком большой и не нужен, и даёт лишние потери производительности.
Решение: пакетный менеджер и systemd
Чтобы что-то доставить надёжно и запустить, всё необходимое есть в операционной системе. Мы работаем на Linux, у нас везде Ubuntu, и как в любой операционной системе есть пакетный менеджер (у нас это самый распространённый пакетный менеджер, который существует уже достаточно давно) и systemd, который запускает любое приложение, которое есть в системе.
Эти инструменты старые, надёжные и проверенные. Никаких дополнительных систем ставить не нужно — мы не тащим лишние сущности, у нас меньше частей, которые могут выйти из строя. Это существенное преимущество.
Практическая заметка. systemd и пакетные менеджеры — это стандартные инструменты Linux, которые проверены временем и используются повсеместно. Их преимущество не только в надёжности, но и в том, что любой Linux-администратор знает, как с ними работать. Это снижает порог входа для новых членов команды и упрощает диагностику проблем. Источник: опыт эксплуатации более 50 серверов без Kubernetes.
При этом пакетный менеджер — это необязательно apt: может быть yum, может быть любой, в зависимости от вашей операционной системы. Если у нас Ubuntu, то apt. В целом разницы нет.
Процесс доставки: от сборки до запуска
Полный конвейер доставки выглядит так:
[1] Разработка
|
v
[2] Сборка (go build)
|
v
[3] Создание пакета (kinectl deb)
| |
| +---> Чтение spec.yml
| +---> Генерация deb/rpm через nfpm
|
v
[4] Репозиторий пакетов (apt repo)
| |
| +---> Версионирование
| +---> Хранение старых версий (для rollback)
|
v
[5] Ansible playbook (kinescope-service.yml)
| |
| +---> Prepare: загрузка spec.yml
| +---> Deploy: установка пакета на группу хостов
| | |
| | +---> apt update && apt install
| | +---> Создание systemd unit
| | +---> Настройка переменных окружения
| | +---> Настройка секретов
| | +---> systemctl start service
| | +---> Health check (проверка /metrics)
| |
| +---> Monitoring: добавление в Prometheus
| +---> Notify: аннотация в Grafana
|
v
[6] Сервис работает
|
+---> Метрики доступны
+---> Логи собираются
+---> Алерты настроены
Детали процесса развёртывания
Этап 1: Подготовка (Prepare)
- Playbook читает
spec.ymlиз репозитория сервиса - Парсит спецификацию и подготавливает переменные
- Загружает зашифрованные данные из Ansible Vault
- Объединяет переменные из разных источников (общие, окружение, специфичные для сервиса)
- Генерирует шаблон для переменных окружения (включая зашифрованные секреты)
Этап 2: Развёртывание (Deploy)
- Обновляет кэш пакетов на целевых хостах (
apt update) - Устанавливает пакет указанной версии (
apt install package=version) - Фиксирует версию пакета (
dpkg --set-selections hold), чтобы предотвратить автоматическое обновление - Создаёт директорию конфигурации (
/etc/${service_name}/) - Записывает переменные окружения (включая зашифрованные секреты) в
/etc/${service_name}/environment - Генерирует systemd unit файл из шаблона с учётом всех лимитов и настроек
- Перезагружает systemd daemon (
systemctl daemon-reload) - Запускает сервис (
systemctl start ${service_name}) - Включает автозапуск (
systemctl enable ${service_name}) - Проверяет health check: делает HTTP запрос к
/metricsдо 12 раз с интервалом 5 секунд, пока не получит ответ 200
Этап 3: Мониторинг (Monitoring)
- Добавляет сервис в конфигурацию Prometheus (file_sd)
- Перезагружает конфигурацию Prometheus
- Создаёт аннотацию в Grafana о развёртывании (версия, хост, пользователь, время)
Этап 4: Очистка (Cleanup)
- Удаляет временные файлы, созданные во время развёртывания
Откат (Rollback)
Откат к предыдущей версии выполняется так же просто, как и деплой:
ansible-playbook playbooks/kinescope-service.yml \
-i inventories/production \
-e app_version=v1.2.3 # Предыдущая версия
Playbook установит указанную версию пакета, перезапустит сервис и проверит health check. Старые версии пакетов хранятся в репозитории, поэтому откат всегда возможен.
Canary развёртывание
Для постепенного развёртывания можно использовать параметр serial в spec.yml:
service:
deploy:
serial: 1 # Развёртывание по одному хосту
Или ограничить развёртывание конкретным хостом:
ansible-playbook playbooks/kinescope-service.yml \
-i inventories/production \
-e app_version=v2.0.0 \
--limit host-01 # Только на одном хосте
После проверки на одном хосте можно развернуть на остальных.
Health check и проверка работоспособности
После запуска сервиса playbook проверяет его доступность через health check endpoint. Это важно, потому что:
- Раннее обнаружение проблем: если сервис не запустился или упал сразу после старта, мы узнаем об этом до того, как трафик пойдёт на него
- Автоматическая проверка: не нужно вручную проверять, что сервис работает
- Интеграция с балансировщиками: health check endpoint используется не только Ansible, но и балансировщиками для маршрутизации трафика
Если health check не проходит после 12 попыток (60 секунд), playbook завершается с ошибкой, и можно разобраться, что не так.
Всё стандартными средствами ОС: пакетный менеджер для доставки, systemd для запуска и управления, Ansible для автоматизации.
Сборка пакетов: NFPM
Единственное, что для сборки мы используем NFPM (Not FPM). Нам лень каждый раз писать файлик или править его руками для сборки пакетов, поэтому есть прекрасная штука NFPM от GoReleaser. Это простой инструмент для создания пакетов (deb, rpm, apk, ipk, arch linux), написанный на Go, без зависимостей. Он позволяет в YAML описать, что вы хотите видеть в пакете, и собирать под нужную операционку — а запускать уже через systemd.
Преимущества systemd
Видимость в системе
Во-первых, сервис виден системе, и мы это легко мониторим. Мы на машину закидываем экспортеры, и даже если разработчик вдруг не добавил мониторинг (например, не добавлен endpoint или мы его не добавили в Prometheus), мы всё равно увидим сервис, потому что он отдаёт стандартные метрики. Мы подхватываемся по названию — все сервисы видны в системе; упал или не упал — тоже видно.
Изоляция ресурсов
Если кто-то говорит, что Docker нужен для изоляции ресурсов, это не совсем так. В systemd точно так же прописываются ограничения — там CPU, память и всё, что вы хотите. То есть там достаточно гибко сделано.
Безопасность
Безопасность, естественно, есть: если сервис, например, не работает с какими-то устройствами, то он не сможет до них достучаться. Вы это всё прописываете там же, и это всё видно.
Задачи по расписанию
Мы используем systemd для задач по расписанию. Если кто-то помнит cron — та же идея. У нас есть несколько задач, которые мы запускаем редко, поэтому нет смысла держать сервис запущенным всё оставшееся время. Через systemd мы просто ставим таймер/задачу — он запускает, отрабатывает и завершает.
Производительность и надёжность
Если у нас ничего лишнего нет, то ломаться нечему, и ресурсы тратить некому. systemd работает стабильно. Да, я слышал жалобы на systemd, что он якобы потребляет много ресурсов, но в нашей практике его накладные расходы практически незаметны. Это надёжно: если systemd не работает — у нас ничего не работает, так что это базовая система, за которой следят все.
Автоматизация через Ansible
Обычно все рассказывают про то, что что-то внедряют. Мы рассказываем про то, что мы, наоборот, «выпилили» и получили результат почти ничего не делая — даже никакого дополнительного софта не поставили. У нас получился единый формат пакетов, с доставкой всё ок, есть версионирование, изоляция — то есть почти серебряная пуля.
Однако у нас ещё были закрыты вопросы по автоматизации.
Автоматизировать нужно всё
Обычно сейчас расскажу, наверно, такой капитан очевидность: автоматизировать нужно всё, даже если у вас есть один сервер. Это можно всё автоматизировать, потому что если он сгорит, потом вы руками ещё не сделаете — вы не восстановите то, что там было, окружение какое, там неважно почему, чем занимается.
Почему Ansible?
Для автоматизации, естественно, мы выбрали Ansible. Он у нас уже был, поэтому выбор выглядел логичным. «Почему Ansible?» — а что ещё выбирать? Puppet мы использовали очень давно, много лет. Его можно расширять плагинами и в целом поддерживать, но про «легко поддерживать» я бы поставил звёздочку: на практике это не всегда так.
Если вы пользуетесь Ansible, у вас неизбежно появляется «мусор» и технический долг. Поэтому раз в какое-то время мы возвращаемся и чистим: переписываем playbook’и, удаляем ненужные роли/сервисы и всё, что накопилось.
Как работает Ansible у нас
У нас всё очень просто. У нас есть две команды по большому счёту — разработчики и инженеры, которые поддерживают систему (админы, проще говоря).
Эксплуатация занимается базой: когда появляется сервер и к нему подведена сеть, на него накатывается базовая роль. Дальше сервер добавляется в inventory, отключается root-доступ, настраиваются доступы, и ставится мониторинг/логирование: например, node exporter и агент для сбора логов. Мы собираем все логи в системе — не только наши сервисы, а всё, что есть. Нужно это редко, но метко: можно одномоментно посмотреть, что происходило на хосте.
Для разработчиков Ansible тоже важен: playbook’и лежат в репозитории, и в него могут коммитить обе стороны — и разработчики, и админы. Разработчики полностью отвечают за свои сервисы. Поскольку сервисы у нас унифицированы, чаще всего достаточно скопировать «скелет», проставить нужные настройки — и всё. Правила роутинга в nginx/proxy тоже на стороне разработки: эксплуатация в это почти не лезет.
Соответственно разработчик может «с коммита» добавить свой сервис, собрать его и выкатить — админ для этого не нужен. Поэтому админ стал спать спокойнее и меньше дёргаться.
“Manifest style” без Kubernetes: единый spec-файл на сервис
Человеческий фактор всё равно остаётся: можно забыть лимиты памяти, порт для мониторинга, какой-нибудь обязательный параметр или зависимость. Чтобы снизить вероятность ошибок, мы используем единый файл спецификации — spec.yml — который описывает весь жизненный цикл сервиса: от сборки пакета до настройки systemd unit и мониторинга.
Идея простая: один файл в репозитории сервиса описывает всё, что нужно для его эксплуатации. Это похоже на манифест Kubernetes, но без самого Kubernetes. Из этого файла автоматически генерируется:
- deb/rpm пакет (через утилиту
kinectl, которая используетnfpmкак библиотеку) - Ansible playbook для развёртывания
- systemd unit файл с правильными лимитами и настройками
- конфигурация для мониторинга
Структура spec.yml
Файл spec.yml описывает сервис в формате YAML. Вот минимальный пример:
service:
name: my-service
group: applications
description: "Мой сервис"
deploy:
probe:
type: "http"
port: 9090
path: /metrics
limits:
mem: 500M
no_file: 5_000
environments: |
HTTP_ADDRESS=:8080
LOG_LEVEL=info
Более полный пример с зависимостями и секретами:
service:
name: my-api-service
group: applications
description: "API сервис"
deploy:
serial: 1 # Развёртывание по одному хосту
probe:
type: "http"
port: 9090
path: /metrics
limits:
mem: 1G
no_file: 10_000
nproc: 1000
resources:
- postgres
- sentinel
- memcached
- nats
environments: |
HTTP_ADDRESS=:{{ api_port }}
LOG_LEVEL={{ log_level }}
SECRET_KEY={{ vault_secret_key_encrypted }}
API_TOKEN={{ vault_api_token_encrypted }}
exec:
start: "/usr/bin/my-api-service"
stop: "/bin/kill -s SIGINT $MAINPID"
security:
owner: "kinescope"
group: "kinescope"
Что описывает spec.yml
Базовые параметры:
name— имя сервиса (должно совпадать с именем пакета)group— группа хостов для развёртывания (applications, encoders, storage и т. п.)description— описание сервиса
Развёртывание:
deploy.serial— сколько хостов развёртывать одновременно (для canary или постепенного деплоя)deploy.probe— настройки health check (тип, порт, путь)deploy.numa_netdevs— список сетевых интерфейсов для NUMA привязки (если нужно)
Лимиты ресурсов:
limits.mem— лимит памяти (например,500M,1G,unlimited)limits.no_file— максимальное количество открытых файловlimits.nproc— максимальное количество процессовlimits.nice— nice value для процессаlimits.allowed_cpus— разрешённые CPU (cpuset, например,"0-3")
Зависимости:
resources— список зависимостей от других сервисов (postgres, redis, nats и т. п.). Автоматически добавляются переменные окружения для подключения к этим сервисам
Конфигурация:
environments— переменные окружения (включая зашифрованные секреты). Чувствительные данные шифруются через Ansible Vault и кодируются в base64. При сборке бинарника в него вшивается пароль для расшифровки (через библиотекуgitlab.kinescope.dev/go/vault), и при запуске приложение автоматически расшифровывает переменные окруженияrequired_packages— системные пакеты, которые нужно установить
Команды управления:
exec.start— команда запуска (по умолчанию/usr/bin/${service.name})exec.stop— команда остановки (по умолчаниюSIGINT)exec.reload— команда перезагрузки (если поддерживается)
Безопасность:
security.ownerиsecurity.group— пользователь и группа для запуска сервисаcapabilities— Linux capabilities (например,CAP_NET_BIND_SERVICE)
Типовые ошибки и как их избежать
Ошибка 1: Забыли указать лимит памяти
# Плохо: нет лимита
service:
name: my-service
# limits отсутствует
# Хорошо: лимит указан
service:
name: my-service
limits:
mem: 500M
Ошибка 2: Неправильный порт для health check
# Плохо: порт не совпадает с реальным
deploy:
probe:
port: 8080 # А сервис слушает на 9090
# Хорошо: порт совпадает
deploy:
probe:
port: 9090 # Сервис действительно слушает на 9090
Ошибка 3: Забыли указать зависимости
# Плохо: сервис использует PostgreSQL, но это не указано
service:
name: my-service
# resources отсутствует, но в коде используется POSTGRES_DSN
# Хорошо: зависимости указаны
service:
name: my-service
resources:
- postgres # Автоматически добавит POSTGRES_DSN
Ошибка 4: Секреты в открытом виде
# Плохо: секреты в открытом виде
environments: |
SECRET_KEY=super-secret-key # Небезопасно!
# Хорошо: секреты зашифрованы через Ansible Vault и закодированы в base64
environments: |
LOG_LEVEL=info
SECRET_KEY={{ vault_secret_key_encrypted_base64 }} # Зашифровано через Ansible Vault, затем base64
# При сборке бинарника вшивается пароль для расшифровки через gitlab.kinescope.dev/go/vault
# При запуске приложение автоматически расшифровывает переменные окружения
Как это работает на практике:
Шифрование секрета (при подготовке конфигурации через Ansible Vault):
# Секрет шифруется через Ansible Vault и кодируется в base64 ansible-vault encrypt_string "my-secret-key" --vault-password-file vault-pass | base64В spec.yml секрет хранится в зашифрованном виде:
environments: | SECRET_KEY={{ vault_secret_key_encrypted_base64 }}В коде приложения используется библиотека
gitlab.kinescope.dev/go/vault:package action import "gitlab.kinescope.dev/go/vault" var ( vaultPassword = "" // Вшивается при сборке через -ldflags skipDecrypt, _ = strconv.ParseBool(os.Getenv("SKIP_DECRYPT")) ) var Flags = []cli.Flag{ vault.StringFlag{ Name: "secret-key", EnvVar: "SECRET_KEY", Value: "", // Значение из переменной окружения (зашифрованное) Password: vaultPassword, // Вшит при сборке через Makefile SkipDecrypt: skipDecrypt, // Для локальной разработки }, }При сборке пароль для расшифровки вшивается через Makefile:
VAULT_PASSWORD ?="" LDFLAGS := -X '$(GO_PACKAGE)/cmd/service/action.vaultPassword=$(VAULT_PASSWORD)' build: go build -ldflags "$(LDFLAGS)" -o service cmd/service/main.goПри запуске приложение автоматически расшифровывает переменные окружения. Если переменная
SKIP_DECRYPT=true, используется значение как есть (для локальной разработки).
Таким образом, зашифрованные секреты можно безопасно хранить в spec.yml и переменных окружения, не раскрывая их в открытом виде на серверах. Пароль для расшифровки вшивается в бинарник при сборке, что позволяет приложению автоматически расшифровывать секреты при запуске.
Преимущества подхода
- Единая точка истины: вся информация о сервисе в одном месте
- Меньше ошибок: утилита может проверить обязательные поля и подставить дефолты
- Автоматизация: из одного файла генерируется всё необходимое для деплоя
- Версионирование: spec.yml хранится в Git вместе с кодом, можно отслеживать изменения
- Простота: разработчик не должен знать детали Ansible или systemd — достаточно описать сервис в spec.yml
Это не полная замена Kubernetes манифестов, но для нашего случая этого достаточно. Мы получаем многие преимущества «manifest style» подхода без сложности оркестратора.
Масштабирование и отказоустойчивость
Что такое масштабирование?
Масштабирование — это была одна из задач: чтобы система масштабировалась и была отказоустойчивой. Что такое масштабирование? Когда в Kubernetes есть autoscaling: пришла нагрузка — «нарисовался» новый экземпляр сервиса. Создание экземпляров сервиса на лету нам казалось сомнительной идеей: это добавляет сложности и магии, а нам хотелось предсказуемости.
Если нужно, чтобы сервис обрабатывал больше трафика, мы хотели, чтобы он просто держал этот трафик при нормальном горизонтальном масштабировании по хостам/группам. Не хотелось городить отдельную «магическую» сущность, которая решает, когда и сколько экземпляров поднять. И отдельный «единственный» балансировщик — тоже точка отказа: где-то появляется условный master, значит появляется и риск отказа — такого допускать не хотелось.
Простое решение: группы серверов
Вопрос масштабирования решился достаточно просто — «как в старые времена». Все серверы разделены на группы: application-сервера (простые дешёвые машины), CDN-сервера (мы занимаемся видео — там много трафика и диска), сервера транскодирования (другая конфигурация: сеть/CPU/GPU — под обработку видео) и т. п.
Серверы разной конфигурации, потому что решают разные задачи. Всё просто: в каждой группе больше одного сервера. Если одного не хватает — добавляем ещё. Обычно это актуально для CDN (чем больше трафика — тем больше серверов нужно) и для транскодирования (докидываем машины в группу, они забирают задачи из очереди).
Отказоустойчивость через DNS и BGP
Отказоустойчивость — за счёт простых механизмов. Например, для фоновых задач: если один из серверов выпадает, задачи остаются в очереди и забираются другими — в целом всё продолжает работать.
Для HTTP-трафика у нас «двойная» схема. Первое — DNS: мы можем направить пользователя в нужный регион. Например, пользователи из России идут в Россию. Дальше уже внутри региона — балансировка по группам серверов: пользователь попадает либо в один, либо в другой дата-центр, а там — на конкретный сервер в группе.
Если сервер «сгорает» или с ним что-то случается, BGP отключается, и он выпадает из раздачи примерно за полторы секунды — нас это устраивает. Обратно включается так же: поднялся — добавился в роутинг. Работает это хорошо.
Собственный DNS-сервер
Пойдём дальше: у нас свои DNS-сервера, и мы можем достаточно тонко настраивать роутинг. Да, здесь мы «завелосипедили» и написали свой DNS-сервер — зато можем очень точно задавать правила обработки трафика.
Это сильно помогает, потому что, например, одна из задач — масштабирование CDN. Есть крупные клиенты, под которых мы можем оптимизировать инфраструктуру, и DNS в этом сильно помогает. Из коробки такого решения нам никто не давал — пришлось запилить самим.
Плюсы решения
Плюсы из того, что у нас получилось:
- Почти ничего не пришлось делать — обычно все рассказывают, что пришлось много «внедрять», а мы, наоборот, многое выкинули
- Надёжно — потому что мы вместо того, чтобы привносить что-то в систему, наоборот выкинули. Мы работаем на том, что даёт из коробки операционная система, больше ничего не ставим
- Достаточно просто — нет никаких накладных расходов: софт работает в окружении, просто поставил, запустил — и готово
- Отлично мониторится — мониторится не потому что мы такие умные что-то там написали, а потому что стандартизировали образ и подход к метрикам, логам и остальному. Все логи в одном месте — спасибо Loki: мы на него перешли в какой-то момент. Сейчас для эксплуатации много чего есть «из коробки» и бесплатно
Минусы
Минусов для нас, по большому счёту, нет. Они могут быть для кого-то другого, но нас результат устраивает. И «улучшать» тут особо нечего: компонентов стало меньше, мы многое, наоборот, выкинули.
Если придумать, ну конечно они есть:
- Нет autoscaling — о котором я говорил в контексте Kubernetes. У нас своё железо, то есть мы не можем «хлопнуть в ладоши» и получить дополнительные ресурсы. В облаке это проще: большинство провайдеров предоставляют autoscaling — пришло больше трафика, запустилась виртуальная машина; трафик упал — машину можно удалить и сэкономить деньги
- Может сломаться — например, репозиторий: был, а потом перестал существовать, или нужно руками разбираться с пакетами. Поэтому почти весь софт, который мы используем дополнительно (Grafana, Loki, экспортеры, VictoriaMetrics, Prometheus и т. п.), мы стараемся держать у себя: пакуем и складываем в свой репозиторий. Внешний репозиторий используем по минимуму (например, nginx)
- Нельзя локально поднять “всю систему одной командой” — новичкам это иногда было бы удобно. Но по большому счёту и не нужно: сервисов много, а чтобы работать над конкретным сервисом, не надо поднимать всё. Обычно достаточно базы данных, nginx и самого сервиса
- Нельзя загуглить готовый Helm chart и закрыть задачу — иногда это удобно, но у этого есть обратная сторона: можно «поставить по умолчанию» и не понимать, как оно работает. У меня был реальный кейс: Kafka подняли с дефолтными путями в
/tmp, и это вскрылось только когда начались проблемы. Поэтому мы предпочитаем, чтобы люди понимали, что именно они ставят и как это будет жить в проде
Наблюдаемость: метрики, логи, алерты
Наблюдаемость не зависит от Kubernetes. Мы используем стандартный стек: Prometheus для метрик, Loki для логов, Grafana для визуализации, AlertManager для алертов.
Минимальный набор метрик для сервиса
Каждый сервис должен отдавать как минимум:
- Версия и время сборки — чтобы всегда знать, какая версия работает
- Health check endpoint —
/metricsили/healthдля проверки работоспособности - Runtime метрики Go (если сервис на Go) — работа GC, использование памяти, количество горутин
Дополнительно полезны:
- Метрики бизнес-логики (количество запросов, ошибок, latency)
- Метрики зависимостей (время ответа БД, кэша, внешних API)
- Метрики ресурсов (CPU, память, диск, сеть)
Централизованные логи
Все сервисы пишут логи в stdout/stderr, systemd собирает их в journald, а Promtail отправляет в Loki. Это позволяет:
- Видеть логи всех сервисов в одном месте
- Искать по времени, хосту, сервису
- Настраивать алерты на паттерны в логах
- Анализировать инциденты после их завершения
Алерты и реагирование
Алерты настраиваются в Prometheus и отправляются в AlertManager, который группирует их и отправляет в Telegram. Основные типы алертов:
- Сервис недоступен — health check не отвечает
- Высокое использование памяти — превышение лимита или порога
- Нехватка места на диске — свободное место < 25%
- Высокая температура — для физических серверов
- Проблемы с зависимостями — БД, кэш, очереди недоступны
Чеклист: что должно быть у каждого сервиса
Чтобы эксплуатация была предсказуемой, каждый сервис должен иметь:
- ✅ Health check endpoint —
/metricsили/healthна отдельном порту - ✅ Метрики версии — версия, время сборки, название сервиса
- ✅ Логирование — структурированные логи в stdout/stderr
- ✅ Лимиты ресурсов — память, файловые дескрипторы, процессы (в
spec.yml) - ✅ Graceful shutdown — корректная обработка SIGTERM/SIGINT
- ✅ Переменные окружения — вся конфигурация через env, не через файлы
- ✅ Зависимости — указаны в
spec.ymlчерезresources - ✅ Документация — описание в
spec.ymlчерезdescription
Если сервис соответствует этому чеклисту, его эксплуатация будет простой и предсказуемой.
Типовые проблемы и их решение
Сервис не запускается
Симптомы: systemctl status показывает failed, логи показывают ошибку.
Диагностика:
# Проверить статус
systemctl status my-service
# Посмотреть логи
journalctl -u my-service -n 50
# Проверить конфигурацию
cat /etc/my-service/environment
# Проверить права доступа
ls -la /usr/bin/my-service
Типовые причины:
- Неправильные переменные окружения (проверить
/etc/my-service/environment) - Отсутствие зависимостей (БД, кэш недоступны)
- Неправильные права доступа к файлам
- Порт уже занят другим процессом
Решение: Исправить конфигурацию и перезапустить сервис.
Высокое использование памяти
Симптомы: Алерт MemoryUsed, сервис работает медленно, возможен OOM kill.
Диагностика:
# Проверить использование памяти
free -h
htop
# Найти процессы с высоким использованием
ps aux --sort=-%mem | head
# Проверить лимиты systemd
systemctl show my-service | grep Memory
# Проверить метрики Go (если сервис на Go)
curl http://localhost:9090/metrics | grep go_memstats
Типовые причины:
- Утечка памяти в коде
- Лимит слишком маленький для нагрузки
- Много горутин/процессов (проверить
go_goroutines)
Решение: Увеличить лимит в spec.yml (краткосрочно) или исправить утечку (долгосрочно).
Нехватка места на диске
Симптомы: Алерт RootFS или DataFS, ошибки записи.
Диагностика:
# Проверить использование диска
df -h
# Найти большие файлы
du -sh /* | sort -h
# Проверить логи systemd
journalctl --disk-usage
# Очистить старые логи
journalctl --vacuum-time=7d
Типовые причины:
- Накопились логи (journald, приложения)
- Временные файлы не удаляются
- Рост данных (БД, кэш)
Решение: Очистить логи, удалить временные файлы, увеличить диск или добавить новый.
Проблемы с сетью
Симптомы: Высокий latency, потеря пакетов, недоступность сервисов.
Диагностика:
# Проверить сетевые интерфейсы
ip addr
ip link
# Проверить маршрутизацию
ip route
birdc show protocols # Если используется BGP
# Проверить доступность
ping host
traceroute host
# Проверить порты
ss -tulpn | grep port
Типовые причины:
- Проблемы с физическим подключением
- Неправильная маршрутизация
- Firewall блокирует трафик
- Проблемы с BGP сессиями
Решение: Проверить физическое подключение, маршрутизацию, firewall правила, BGP сессии.
Проблемы с зависимостями
Симптомы: Сервис не может подключиться к БД, кэшу, очередям.
Диагностика:
# Проверить переменные окружения
cat /etc/my-service/environment | grep POSTGRES
cat /etc/my-service/environment | grep REDIS
# Проверить доступность зависимостей
nc -zv db-host 5432
redis-cli -h cache-host ping
# Проверить логи сервиса
journalctl -u my-service | grep -i "connection\|timeout\|error"
Типовые причины:
- Зависимость недоступна (упала, сетевые проблемы)
- Неправильные credentials (проверить зашифрованные переменные окружения)
- Превышен лимит соединений
Решение: Проверить доступность зависимости, исправить credentials, увеличить лимит соединений.
Неочевидные плюсы
Плюс и неочевидные на самом деле ещё есть вот эти штуки:
- Стала простой и надёжной — за три года по вине условного systemd или пакетного менеджера у нас ни разу ничего не упало. Если что-то ломалось, то это были наши косяки (обычно на стороне сервиса). При этом всегда есть возможность откатиться назад. Самое страшное, как обычно, — это деплой: там действительно можно что-то сломать
- Расслабились админы — раньше админ жил в режиме 24/7: много алертов, постоянные разборы. Сейчас алертов меньше, и иногда это приводит к «расслабленности» — человек может ехать в электричке на дачу и отвечать “я через полчаса буду на связи”. Это, с одной стороны, хорошо (меньше выгорания), с другой — требует дисциплины
Заключение
Мы показали, что можно успешно эксплуатировать production-инфраструктуру без Kubernetes и Docker, используя стандартные инструменты Linux: systemd, пакетные менеджеры и Ansible. Это решение оказалось проще, надёжнее и эффективнее для нашего случая.
Ключевые моменты:
- Простота важнее сложности — стандартные инструменты решают большинство задач без дополнительных слоёв абстракции
- Один бинарник — один сервис — упрощает доставку, версионирование и эксплуатацию
- Единый spec.yml — один файл описывает весь жизненный цикл сервиса, снижая вероятность ошибок
- Автоматизация через Ansible — вся инфраструктура как код, разработчики не зависят от админов
- Масштабирование через группы серверов — простое горизонтальное масштабирование без оркестраторов
- Отказоустойчивость через DNS и BGP — маршрутизация трафика на уровне сети
- Наблюдаемость не зависит от оркестратора — метрики, логи и алерты работают независимо от Kubernetes
За три года эксплуатации ничего не упало по вине systemd или пакетных менеджеров. Это говорит о том, что простое решение может быть более надёжным, чем сложное.
Важно понимать: это не значит, что Kubernetes плох или что его не нужно использовать. Это значит, что для нашего случая (своё железо, специфические требования, небольшая команда) простое решение оказалось более эффективным.
Похожая тема с другим фокусом (требования, выбор инструментов, экономика) — в статье «Как мы не выбрали K8s». О том, почему Go стал основным языком в нашей инфраструктуре, см. «Профит для компании от Go».
Полезные материалы
- systemd: документация и примеры — официальная документация по systemd unit-файлам, ограничениям ресурсов и безопасности
- Ansible: документация и best practices — официальная документация Ansible с примерами playbook’ов и ролей
- NFPM: создание пакетов для Linux — инструмент для создания deb/rpm пакетов из YAML-описания
- systemd: изоляция ресурсов и безопасность — как настроить лимиты CPU, памяти и другие ограничения через systemd
- Debian Packaging Guide — руководство по созданию deb-пакетов (полезно для понимания структуры пакетов)
- systemd timers вместо cron — как использовать systemd для задач по расписанию вместо cron
- Ansible Vault: хранение секретов — безопасное хранение паролей и секретов в Ansible playbook’ах
- BGP и anycast для отказоустойчивости — как использовать BGP/anycast для маршрутизации трафика и обеспечения отказоустойчивости
- DNS для маршрутизации трафика — основы DNS и как использовать его для гео-маршрутизации
- Простота vs сложность в инфраструктуре — примеры того, как излишняя сложность может привести к проблемам (на примере Cloudflare)
Практическая заметка. При выборе инструментов для инфраструктуры важно понимать: чем больше компонентов, тем больше точек отказа. Стандартные инструменты Linux (systemd, пакетные менеджеры) проверены временем и имеют меньше «магии», что упрощает диагностику проблем. Источник: опыт эксплуатации более 50 серверов без Kubernetes.
Сноски
О том, почему Go стал основным языком в нашей инфраструктуре и какие преимущества это даёт бизнесу, см. «Профит для компании от Go». Похожая тема с другим фокусом — в «Как мы не выбрали K8s». ↩︎