Часть 2: Балансировка нагрузки и сервис-дискавери с Consul и Traefik для Marimo
В первой части мы развернули приложение Marimo через Nomad, используя драйвер `raw_exec` и динамические порты. Приложение работало, но у него был существенный недостаток: каждая реплика была доступна по отдельному порту, а пользователю приходилось вручную искать актуальный адрес. Кроме того, отсутствовала отказоустойчивость — при падении одной реплики трафик не перераспределялся.
Первая часть тут HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS
В этой статье мы решим эти проблемы с помощью Consul (сервис-дискавери) и Traefik (балансировщик нагрузки). Мы также настроим липкие сессии (sticky sessions), которые критически важны для stateful-приложений, таких как Marimo.
Зачем нужны Consul и Traefik?
- Consul — распределённый каталог сервисов. Он хранит информацию о работающих репликах, проверяет их здоровье и предоставляет DNS-интерфейс. Без него балансировщик не знал бы, куда направлять трафик.
- Traefik — современный обратный прокси и балансировщик нагрузки. Он автоматически обнаруживает сервисы через Consul Catalog Provider и распределяет запросы между здоровыми репликами.
- Nomad — оркестратор, который управляет всеми компонентами.
Итоговая архитектура:
Пользователь
│
▼
Traefik ──► Marimo 1 / 2 / 3
(балансировщик) │
│ ▼
└──────────► Consul
▲
│
Nomad ClusterПредварительные требования
Убедитесь, что у вас есть:
- Работающий Nomad в режиме разработки (`nomad agent -dev -bind=0.0.0.0`).
- Проект `~/marimo-app` с файлами `app.py`, `pyproject.toml`, `uv.lock` и виртуальным окружением.
- Установленный Consul.
- Установленный Traefik (мы будем запускать его через Nomad, но бинарник должен быть доступен).
Установка Consul
brew tap hashicorp/tap
brew install hashicorp/tap/consulУстановка Traefik
brew install traefikПроверьте пути:
which consul # /usr/local/bin/consul
which traefik # /usr/local/bin/traefikШаг 1: Запуск Consul в режиме разработки
Откройте отдельный терминал и запустите Consul:
consul agent -dev -bind=0.0.0.0 -client=0.0.0.0Consul будет доступен по адресу `http://127.0.0.1:8500`. Оставьте терминал открытым.
Важно: Consul должен быть запущен до того, как вы запустите Nomad с интеграцией с Consul, иначе сервисы не зарегистрируются.
Шаг 2: Обновление задания Marimo
Мы добавим в задание блок `service`, который зарегистрирует приложение в Consul и снабдит его тегами для Traefik. Также включим липкие сессии, чтобы запросы одного пользователя всегда попадали на одну и ту же реплику.
Создайте файл `marimo-job.nomad`:
job "marimo-app" {
datacenters = ["dc1"]
type = "service"
group "app" {
count = 3
network {
port "http" {}
}
service {
name = "marimo-app"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.marimo.rule=Host(`marimo.localhost`)",
"traefik.http.routers.marimo.entrypoints=web",
"traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true",
]
check {
type = "http"
path = "/"
interval = "2s"
timeout = "2s"
}
}
task "marimo" {
driver = "raw_exec"
config {
command = "/Users/yuriygavrilov/marimo-app/.venv/bin/marimo"
args = [
"run", "app.py",
"--headless",
"--port", "${NOMAD_PORT_http}",
"--host", "0.0.0.0"
]
work_dir = "/Users/yuriygavrilov/marimo-app"
}
resources {
cpu = 500
memory = 256
}
}
}
}Что изменилось:
- `count = 3` — запускаем три реплики.
- Блок `service` — регистрирует сервис в Consul.
- Теги Traefik:
- `traefik.enable=true` — разрешает Traefik обслуживать сервис.
- `traefik.http.routers.marimo.rule=Host(\`marimo.localhost\`)` — правило маршрутизации по домену.
- `traefik.http.routers.marimo.entrypoints=web` — точка входа (должна совпадать с именем в конфиге Traefik).
- `traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true` — включает липкие сессии на основе cookie.
Примечание про домен `.localhost`
В примере используется домен `marimo.localhost`. Добавлять его в `/etc/hosts` не нужно — имена, заканчивающиеся на `.localhost`, автоматически резолвятся в `127.0.0.1` согласно стандарту RFC 6761 (раздел 6.3) и RFC 6762. Это поведение поддерживается:
- системным резолвером macOS (`mDNSResponder`),
- всеми современными браузерами (Chrome, Safari, Firefox),
- а также большинством инструментов командной строки (`curl`, `ping` и т.д.).
Именно поэтому `http://marimo.localhost:8080` будет работать «из коробки» без правки системных файлов.
Когда `/etc/hosts` всё-таки понадобится:
Если вы хотите использовать другое имя — например, `marimo.dev`, `marimo.internal` или реальный домен вроде `marimo.example.com`, — тогда запись нужна:
echo "127.0.0.1 marimo.dev" | sudo tee -a /etc/hostsДля локальной разработки и тестирования домен `.localhost` — самый удобный вариант: он не требует никаких дополнительных настроек и работает одинаково на любой машине.
Шаг 3: Создание задания для Traefik
Traefik будет запущен через `raw_exec`. Конфигурационный файл создаётся динамически с помощью шаблона Nomad.
Создайте файл `traefik.nomad`:
job "traefik" {
region = "global"
datacenters = ["dc1"]
type = "service"
group "traefik" {
count = 1
network {
port "http" {
static = 8080
}
port "api" {
static = 8081
}
}
service {
name = "traefik"
check {
name = "alive"
type = "tcp"
port = "http"
interval = "10s"
timeout = "2s"
}
}
task "traefik" {
driver = "raw_exec"
# Создаём конфигурационный файл Traefik через шаблон
template {
data = <<EOF
[entryPoints]
[entryPoints.web]
address = ":8080"
[entryPoints.traefik]
address = ":8081"
[api]
dashboard = true
insecure = true
# Включаем Consul Catalog Provider
[providers.consulCatalog]
prefix = "traefik"
exposedByDefault = false
[providers.consulCatalog.endpoint]
address = "127.0.0.1:8500"
scheme = "http"
EOF
destination = "local/traefik.toml"
}
config {
command = "/usr/local/bin/traefik"
args = [
"--configfile",
"${NOMAD_TASK_DIR}/traefik.toml"
]
}
resources {
cpu = 100
memory = 128
}
}
}
}Ключевые моменты:
- `driver = “raw_exec”` — запускаем бинарник напрямую.
- `template` — создаёт файл `traefik.toml` в директории задачи.
- `command` — полный путь к Traefik.
- `args` — указываем путь к созданному конфигу через переменную `${NOMAD_TASK_DIR}`.
- В конфиге Traefik точка входа называется `web` (порт 8080). Это имя должно совпадать с тегом `traefik.http.routers.marimo.entrypoints=web`.
Шаг 4: Запуск всех компонентов
Убедитесь, что Consul и Nomad запущены в отдельных терминалах.
Затем в терминале, где вы работаете с заданиями:
cd ~/marimo-app
# Запускаем приложение
nomad job run marimo-job.nomad
# Запускаем Traefik
nomad job run traefik.nomadПроверьте статус:
nomad job status marimo-app
nomad job status traefikШаг 5: Проверка работы
- Consul: откройте `http://127.0.0.1:8500` и убедитесь, что сервис `marimo-app` зарегистрирован.
- Traefik Dashboard: откройте `http://127.0.0.1:8081/dashboard/`. В разделе HTTP Routers должен быть маршрут `marimo` со статусом Enabled. В HTTP Services — сервис `marimo-app` с тремя здоровыми репликами.
- Приложение: откройте `http://marimo.localhost:8080`. Вы должны увидеть интерфейс Marimo с полем ввода имени и диаграммой. Проверьте, что при нажатии кнопки данные обновляются, а ошибка `Invalid session id` не появляется.
Возможные ошибки и их решения
В процессе настройки мы столкнулись с несколькими типичными проблемами. Вот они и способы их устранения.
Ошибка 1: `EntryPoint doesn’t exist entryPointName=web`
Причина: в тегах сервиса указана точка входа `web`, а в конфигурации Traefik она называется иначе (например, `http`).
Решение: приведите имена к единому виду. В нашем случае мы переименовали точку входа в конфиге Traefik в `web`:
[entryPoints]
[entryPoints.web]
address = ":8080"И в тегах указали `traefik.http.routers.marimo.entrypoints=web`.
Ошибка 2: `Invalid session id: s_bpy8u7`
Причина: отсутствие липких сессий. Marimo — stateful-приложение, оно хранит сессию на конкретной реплике. Если запросы распределяются случайно, браузер попадает на разные реплики, и сессия теряется.
Решение: добавьте тег `traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true`. Traefik установит cookie и будет направлять все запросы от одного браузера на одну и ту же реплику.
Ошибка 3: `Missing required argument: “command”` при использовании `raw_exec`
Причина: конфигурация написана для драйвера `docker` (с полями `image`, `volumes`, `network_mode`), но применён `raw_exec`.
Решение: для `raw_exec` используйте `command` и `args`. Файлы конфигурации создавайте через `template`. Пример для Traefik приведён выше.
Ошибка 4: `raw_exec` отключён
Причина: в некоторых сборках Nomad драйвер `raw_exec` отключён по умолчанию.
Решение: добавьте в конфигурацию клиента Nomad (например, `/etc/nomad.d/nomad.hcl`):
plugin "raw_exec" {
config {
enabled = true
}
}и перезапустите агент Nomad, но в моем случае это не понадобилось.
Итог
Мы добавили к нашему приложению Marimo два важных компонента:
- Consul — для сервис-дискавери и проверки здоровья.
- Traefik — для балансировки нагрузки и единой точки входа.
Теперь приложение доступно по адресу `http://marimo.localhost:8080`, а трафик автоматически распределяется между тремя репликами. Благодаря липким сессиям пользователи не теряют состояние при работе с интерактивным интерфейсом.
Этот подход является основой для построения отказоустойчивых и масштабируемых систем. В следующих статьях мы рассмотрим настройку TLS/HTTPS, использование Vault для управления секретами и автоматическое масштабирование.
Поздравляю! Теперь ваше приложение готово к более серьёзным нагрузкам. 🚀