Welcome to my personal place for love, peace and happiness 🤖

Часть 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

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

Убедитесь, что у вас есть:

  1. Работающий Nomad в режиме разработки (`nomad agent -dev -bind=0.0.0.0`).
  2. Проект `~/marimo-app` с файлами `app.py`, `pyproject.toml`, `uv.lock` и виртуальным окружением.
  3. Установленный Consul.
  4. Установленный 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.0

Consul будет доступен по адресу `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: Проверка работы

  1. Consul: откройте `http://127.0.0.1:8500` и убедитесь, что сервис `marimo-app` зарегистрирован.
  1. Traefik Dashboard: откройте `http://127.0.0.1:8081/dashboard/`. В разделе HTTP Routers должен быть маршрут `marimo` со статусом Enabled. В HTTP Services — сервис `marimo-app` с тремя здоровыми репликами.
  1. Приложение: откройте `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 для управления секретами и автоматическое масштабирование.

Поздравляю! Теперь ваше приложение готово к более серьёзным нагрузкам. 🚀

Follow this blog
Send
Share
Tweet
Pin