Привет, друзья!

Сегодня поговорим о кластерном режиме в Pilot – линейке продуктов, на базе которых организуют совместную работу над строительными проектами, сборку и проверку BIM-моделей.

Кластерный режим Pilot обеспечивает отказоустойчивость и горизонтальное масштабирование её центрального компонента — Pilot-Server. Для хранения данных в кластере используется PostgreSQL, а для взаимодействия между узлами — Redis.

Примечание: На данный момент кластеризация доступна только для Pilot-Server. Pilot-BIM-Server и Pilot-Web-Server работают как отдельные сервисы без возможности горизонтального масштабирования.

Какие преимущества мы получаем по сравнению с подходом, где используется один компонент Pilot-Server?

1. Аппаратный сбой на сервере Pilot-Server. При падении машины, на которой расположен единственный экземпляр Pilot-Server, работа пользователей парализуется, никакие действия с системой в режиме онлайн невозможно совершить. При нескольких компонентах на разных серверах, в случае падения одного из них, мгновенно назначается новый активный узел из оставшихся рабочих. Для пользователей это будет кратковременный разрыв соединения, после которого они переподключатся к новому узлу.

2. Обновление ПО. При обновлении Pilot-Server с единственным экземпляром, пользователи не могут подключаться в этот момент. В кластерном режиме обновление можно произвести в режиме “Последовательного обновления”.

3. Рост нагрузки на Pilot-Server. Большое количество запросов к одному компоненту Pilot-Server может превысить пропускную способность одного сервера. В кластерном режиме за счёт нескольких узлов с Pilot-Server  нагрузка будет распределяться между ними через балансировщик.

4. Аварийное завершение Pilot-Server. При аварийном завершении Pilot-Server, расположенного на одном узле, работа пользователей парализуется, никакие действия с системой в режиме онлайн невозможно совершить. В кластерном режиме, механизмы оркестратора поднимают еще один экземпляр этого компонента на этом сервере.

Что мы теряем при отказе от монолитной архитектуры Pilot-Server?

1. Простоту развертывания и эксплуатации на одном сервере. Исчезает возможность развернуть систему "одной командой".

2. Понятные причины сбоев. В монолите все ошибки видны в одном логе. В кластере проблема может быть в сети, в базе, в кэше – найти причину становится сложнее.

3.  Гарантированную согласованность данных. Данные теперь обновляются на нескольких узлах, и нужно следить, чтобы они не конфликтовали.

4. Локальный доступ к данным. Раньше Pilot-Server работал со своей встроенной базой данных напрямую, без сетевых задержек. Теперь каждый узел кластера вынужден обращаться к внешним службам (PostgreSQL, Redis) через сеть, что добавляет задержку и создаёт узкое место на сетевом взаимодействии.

5. Простую точку входа. Вместо одного адреса сервера теперь необходим балансировщик нагрузки, который нужно настраивать и обслуживать.

Краткое введение про оркестрацию

Развертывание кластерного режима Pilot-Server означает, что мы переходим от управления одним сервисом к управлению набором взаимосвязанных сервисов, образующих единую систему. Вместо наблюдения за одним процессом на известном сервере мы должны будем управлять пулом динамических экземпляров, состояние и доступность которых постоянно меняются. Теперь перед нами встаёт ряд вопросов: Где сейчас работают экземпляры? Все ли они доступны? Где возникла ошибка? и т.д. Вручную управлять такой системой трудоёмко и чревато ошибками. Поможет нам в этом оркестратор.

Оркестратор следит за здоровьем экземпляров, распределяет нагрузку, обновляет систему без простоя и управляет настройками и секретами для всех экземпляров из одного места, исключая ошибки.

Наши сервисы являются контейнезированными и для наших целей подходят такие оркестраторы, как Kubernetes, Nomad, Docker Swarm.

  • Kubernetes является стандартом для продакшн-среды. Самый мощный и гибкий инструмент, но и самый сложный в освоении и администрировании.

  • Nomad достаточно легковесный и гибкий оркестратор от HashiCorp, но многие функции вынесены в другие продукты компании HashiCorp. Чаще всего выбирают, если экосистема построена на стеке HashiCorp.

  • Docker Swarm - встроенная в Docker Engine система оркестрации. В ней нет такого большого набора функций как в Kubernetes, но главный её плюс – это простота и низкий порог вхождения.

В данной статье мы рассмотрим развёртывание кластера Pilot-Server именно с помощью Docker Swarm. Он отлично подходит для демонстрации основных принципов оркестрации, обеспечивая всю необходимую базовую функциональность: распределённый запуск контейнеров по узлам кластера, автоматический перезапуск упавших экземпляров и балансировку нагрузки между репликами сервиса. Это позволяет наглядно показать преимущества кластерного режима.

Для начала разберемся с основными понятиями Docker Swarm:

  • Node (Узел) - серверы с установленным docker. Бывают Manager Node и Worker Node. Manager node обеспечивает управление кластером, Worker используется в качестве исполнителя контейнеров.

  • Service (Сервис) - абстракция в Swarm, описывающая, как запускать и поддерживать контейнеры.

  • Stack (Стек) - группа взаимосвязанных сервисов, развертываемая из одного файла docker-compose.yml.

  • Task (Задача) - конкретный контейнер, который является частью сервиса.

Подготовка Pilot-Server для кластерного режима

Перед миграцией должен быть развёрнут Pilot-Server на Linux с присоединёнными базами данных, предназначенными для переноса в PostgreSQL.

1. Загружаем Pilot-Server под Linux нужной версии.

wget https://pilot.ascon.ru/release/pilot-server_25.22.0.59473.zip
unzip pilot-server_25.22.0.59473.zip
cd pilot-server_25.22.0.59473

2. Добавляем администратора сервера:

./Ascon.Pilot.Daemon --admin ./settings.xml admin admin

3. Запускаем Pilot-Server в консольном режиме:

./Ascon.Pilot.Daemon ./settings.xml

Примечание: Сервер должен оставаться запущенным на время присоединения базы.

4. Присоединяем базу для миграции:

Если у вас нет подготовленной базы, можно использовать демонстрационную (но учитывайте, что версия базы должна соответствовать версии сервера). Присоединить базу можно через приложение Pilot-myAdmin либо консольной командой (выполняется при работающем Pilot-Server):

./Ascon.Pilot.Daemon --attach pilot-ice_ru /opt/Databases/pilot-ice_ru/base.dbp /opt/Databases/pilot-ice_ru/FileArchive/44191f9c-c8a4-4689-b38b-474f2aba4465.pilotfa

где:

pilot-ice_ru - имя базы

/opt/Databases/pilot-ice_ru/base.dbp - путь до файла базы

/opt/Databases/pilot-ice_ru/FileArchive/44191f9c-c8a4-4689-b38b-474f2aba4465.pilotfa - путь до файлового архива

5. Останавливаем Pilot-Server и проверяем, что в файле settings.xml появилась информация о подключённой базе:

<Databases>
     <DatabaseParameters Name="pilot-ice_ru" Filename="/opt/Databases/pilot-ice_ru/base.dbp" State="Launched" Version="20251029" Id="e65d1c20-a480-446c-ac2c-1b9ff1843b0a" SessionId="029310f1-582d-4385-a506-d4f641a6945d" WarmUp="true" IsLaunched="false">
       <FileArchiveFolders>
         <FileArchiveParameters Folder="/opt/Databases/pilot-ice_ru/FileArchive" Id="44191f9c-c8a4-4689-b38b-474f2aba4465" IsWritable="true" />
       </FileArchiveFolders>
     </DatabaseParameters>
   </Databases>

Примечание: Более подробные инструкции о настройке и запуске Pilot-Server на Linux см. в официальной документации

Подготовка PostgreSQL для кластерного режима

Перед развёртыванием кластера необходимо мигрировать существующую базу Pilot (файл .dbp) в PostgreSQL.

1. Создаём том postgres_data. В нём будут храниться данные из .dbp-файла и конфигурационная БД для настроек Pilot-Server.

docker volume create postgres_data

2.      Поднимаем временный контейнер PostgreSQL

version: '3.8'
services:
  postgresql:
    image: postgres:16.2
    container_name: postgresql
    restart: unless-stopped
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgresuser
      POSTGRES_PASSWORD: postgresuserpass
    stdin_open: true
    tty: true
    
volumes:
  postgres_data:
    external: true

Примечание: В Volumes обязательно указываем параметр external: true, это позволит использовать том, созданный на первом шаге.

3. Создаём пользователя pilotuser в PostgreSQL, от имени которого будет осуществляться миграция и подключение к базе данных.

CREATE ROLE pilotuser WITH
NOSUPERUSER
CREATEDB
NOCREATEROLE
NOINHERIT
LOGIN
NOREPLICATION
NOBYPASSRLS
CONNECTION LIMIT -1;
ALTER USER pilotuser PASSWORD 'pilotuserpass';

4. Выполняем миграцию с помощью утилиты PostgresMigration (входит в состав дистрибутива Pilot-Server для Linux):

/opt/pilot-server/PostgresMigration /opt/pilot-server/settings.xml "Host=192.168.0.4:5432; Username=pilotuser; Include Error Detail=true; Password=pilotuserpass; DataBase=postgres" "myredis:6379, password=redispass" configurationdb admin admin

5. Проверяем созданные базы данных в PostgreSQL:

docker exec -it postgresql psql -U postgresuser -c "\l"

В списке должны отобразиться:

  • База configurationdb — для хранения настроек кластера Pilot-Server

  • База с данными, перенесёнными из .dbp-файла (например, pilot-ice_ru)

6. Останавливаем и удаляем временный контейнер — том с данными останется для использования в кластере:

docker stop postgresql
docker rm postgresql

Примечание: Более подробные инструкции о миграции базы данных для кластерного режима работы Pilot-Server см. в официальной документации

Порядок развёртывания в Docker Swarm

Исходные данные:  3 сервера на Linux Ubuntu с установленным docker, расположенных в одной подсети.

Шаг 1. Подключаемся к серверу, который будет выступать у нас в роли Manager node, и выполняем следующую команду:

docker swarm init --advertise-addr "{IP-NODE}"

При успешном выполнении в ответ мы получим схожую команду:

docker swarm join --token SWMTKN-1-51k2k418tw2j0juwm3inq6crp4ow6xogswihcc5azg7oq5qo7e-a3rfeyfwo7d93heq0y5vhyzod 172.31.245.104:2377

Шаг 2. Полученную команду из предыдущего шага мы должны выполнить на остальных узлах, выступающих в качестве Worker Node. Это позволит серверу стать частью Swarm-кластера.

docker swarm join --token SWMTKN-1-51k2k418tw2j0juwm3inq6crp4ow6xogswihcc5azg7oq5qo7e-a3rfeyfwo7d93heq0y5vhyzod 172.31.245.104:2377

Для подтверждения, что узлы добавились в кластер, на сервере, который выступает в роли Manager Node, выполним команду docker node ls

Сервер с hostname "Pilot-Server-1" является ManagerNode, а "vm1" и "vm-base" WorkerNode, и у всех у них статус Ready - они готовы к работе.

Шаг 3. Создаём overlay-сеть для сервисов:

docker network create --driver overlay pilot-net

Шаг 4. Создаём файл стека на Manager Node.

Пример docker-compose.yml:

version: '3.8'

services:
  postgresql:
    image: postgres:16.2
    container_name: postgresql
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgresuser
      POSTGRES_PASSWORD: postgresuserpass
    networks:
      - pilot-net
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      labels:
      - "traefik.enable=false"

  myredis:
    image: redis/redis-stack-server:latest
    container_name: myredis
    restart: unless-stopped
    environment:
      REDIS_ARGS: --notify-keyspace-events KEA
      REDIS_PASSWORD: redispass
    command: >
       sh -c 'redis-server --requirepass "$$REDIS_PASSWORD" $$REDIS_ARGS' 
    networks:
      - pilot-net
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      restart_policy:
        condition: on-failure
        delay: 5s
      labels:
      - "traefik.enable=false"

  pilot-server:
    image: registry.ascon.ru/project/pilotdev/pilot/pilot-server:25.22.0
    restart: unless-stopped
    volumes:
      - type: bind
        source: /mnt
        target: /mnt/vol1
    entrypoint: ./Ascon.Pilot.Daemon
    command: >
      --pgConnectionString="Host=postgresql:5432;Username=pilotuser;Include Error Detail=True;Pooling=True;Connection Lifetime=0;Keepalive=3;Database=configurationdb;Password=pilotuserpass"
      --indexPath=/mnt/vol1/indexes
      --httpPort=5545
    networks:
      - pilot-net
    deploy:
      replicas: 2
      placement:
        constraints:
          - node.role == worker
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.pilot.rule=PathPrefix(`/`)"
        - "traefik.http.services.pilot.loadbalancer.server.port=5545"
        - "traefik.http.services.pilot.loadbalancer.sticky=true"
        - "traefik.http.services.pilot.loadbalancer.sticky.cookie=true"
        - "traefik.http.services.pilot.loadbalancer.sticky.cookie.name=pilot_session"

  traefik:
    image: traefik:v3.0
    ports:
      - "80:80"      # HTTP
      - "8080:8080"  # Dashboard
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.swarm=true"
      - "--providers.docker.exposedByDefault=false"
      - "--providers.docker.network=pilot-net"
    networks:
      - pilot-net
    deploy:
      placement:
        constraints:
          - node.role == manager
      labels:
      - "traefik.enable=true"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"

  portainer:
    image: portainer/portainer-ce:lts
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "9000:9000"
      - "9443:9443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - portainer_data:/data
    networks:
      - pilot-net
    deploy:
      placement:
        constraints:
          - node.role == manager
      labels:
      - "traefik.enable=true"
      - "traefik.http.services.traefik.loadbalancer.server.port=9000"

  portainer-agent:
    image: portainer/agent:lts
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - pilot-net
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

volumes:
  postgres_data:
    external: true
  pilot_data:
  portainer_data:

networks:
  pilot-net:
    driver: overlay
    external: true

Примечание: В данной конфигурации все пароли указаны в открытом виде исключительно для наглядности и простоты воспроизведения. В production-среде обязательно используйте механизмы безопасного хранения секретов.

Подробнее о сервисах:

Pilot-Server. Центральный компонент системы Pilot. Запускается в 2 репликах (replicas: 2) с политикой распределения по worker-нодам (node.role == worker).

Для корректной работы нескольких реплик pilot-server необходим общий доступ к файловому архиву. В монолитной архитектуре файлы хранились локально на одном сервере. В кластере же, если каждая реплика будет использовать свой локальный диск, возникнет рассогласование данных. Для обеспечения единого доступа к файловому архиву Pilot нужно обеспечить единую точку монтирования, доступную с любой ноды кластера. Это может быть сетевая файловая система (NFS) или распределённая файловая система (GlusterFS, Ceph). В нашем примере это путь /mnt — единая точка монтирования к общему хранилищу.

    volumes:
       - type: bind
         source: /mnt
         target: /mnt/vol1

Выбор конкретной реализации зависит от вашей инфраструктуры. Для тестового стенда подойдёт NFS, для production с требованиями к отказоустойчивости - GlusterFS или Ceph. Главное, чтобы на всех нодах кластера путь /mnt вёл к одному и тому же хранилищу.

Примечание: Вместо сетевой файловой системы можно использовать S3-совместимое объектное хранилище.

PostgreSQL. СУБД PostgreSQL для хранения данных системы. В данной демонстрации мы запускаем одну реплику на Manager-ноде с постоянным томом, привязанным к этому узлу. Обязательно используем том, который создавался в разделе "Подготовка PostgreSQL для кластерного режима". В нём присутствует конфигурационная база данных и мигрированные базы данных.

В томах указываем существующий том:

    volumes:
       - postgres_data:/var/lib/postgresql/data

Не забываем указать, что он уже существует:

volumes:
   postgres_data:
     external: true

В конфигурационной базе PostgreSQL (configurationdb) в таблице settings должны быть верно указаны путь файлового архива базы, параметры подключения к базе postgresql, параметры подключения к redis.

В производственной среде Docker Swarm больше подходит для сервисов без сохранения состояния - stateless-решений, в то время как PostgreSQL - это классическое stateful-решение.

Рекомендуемый подход: Ра��вёртывание выделенного отказоустойчивого кластера PostgreSQL (например, на основе Patroni) на отдельных серверах или виртуальных машинах, внешних по отношению к Swarm-кластеру. В качестве промежуточного решения для некритичных нагрузок можно рассмотреть запуск PostgreSQL на выделенной ноде в Swarm с использованием высокопроизводительного внешнего хранилища (SAN/NAS) и регулярными снапшотами.

REDIS. Брокер сообщений для взаимодействия между узлами Pilot-Server. Запускается в одной реплике. Рекомендуется настроить кластер.

Traefik. Балансировщик нагрузки и обратный прокси. Работает на Manager-ноде, автоматически обнаруживает сервисы Pilot-Server и распределяет между ними входящие HTTP-запросы.

Основные параметры, которые позволяют автоматически обнаруживать все сервисы в Swarm, помеченные traefik.enable=true.

command:
   - "--providers.docker=true"
   - "--providers.swarm=true"
   - "--providers.docker.exposedByDefault=false"

Необходимые labels (правила для Traefik) для Pilot-Server:

      labels:
         - "traefik.enable=true"
         - "traefik.http.routers.pilot.rule=PathPrefix(/)"
         - "traefik.http.services.pilot.loadbalancer.server.port=5545"
         - "traefik.http.services.pilot.loadbalancer.sticky=true"
         - "traefik.http.services.pilot.loadbalancer.sticky.cookie=true"
         - "traefik.http.services.pilot.loadbalancer.sticky.cookie.name=pilot_session"

Этими правилами мы включаем traefik для Pilot-Server, направляем все запросы на порт 5545 и закрепляем пользователя за конкретной репликой, чтобы сохранить состояние сессии (sticky-sessions).

Примечание: В качестве альтернативы могут быть использованы другие балансировщики нагрузок, работающие с контейнезированными приложениями.

Portainer. Сервис для управления контейнерами из графического веб-интерфейса. Не является обязательным компонентом, устанавливается опционально. Обеспечивает визуальный мониторинг кластера, упрощенное управление сервисами,  быстрый доступ к оболочке контейнеров и многое другое.

Шаг 5. Переходим в директорию, где расположен наш файл docker-compose.yml, и выполняем команду развёртывания стека:

docker stack deploy -c docker-compose.yml pilot

 Проверить работоспособность сервисов можно по команде:

docker service ls

Как видим, все реплики работают (REPLICAS). Также состояние сервисов можно отслеживать в Portainer.

 Тестирование работоспособности кластера Pilot

Тест отказоустойчивости и самовосстановления

1. Используя команду watch на manager-ноде, выполним команду watch -n 1 'docker service ps pilot_pilot-server'. Эта команда позволит наблюдать в реальном времени за состоянием сервиса Pilot-Server. На экране отображаются две активные задачи со статусом Running и несколько завершённых (Shutdown) — это нормально для динамичной среды Swarm, где контейнеры перезапускались при обновлениях или сбоях.

2. Принудительно на одной worker-ноде останавливаем контейнер сервиса Pilot-Server: docker stop pilot_pilot-server.1...

3. Наблюдаем, как Swarm обнаруживает потерю реплики, планирует новую задачу на доступной ноде и запускает новый контейнер.

4. Тестируем отключение ноды. Выключаем worker-ноду, к которой был подключен пользователь через клиентское приложение. Пользователь теряет соединение, Swarm запускает задачи упавшей ноды на оставшуюся (вторая worker-нода). Подключение пользователя восстанавливается.

Тест балансировки нагрузки

1. Выполняем эмуляцию подключения 6 тестовых пользователей через клиентское приложение.

2. Просматриваем логи контейнеров сервиса Pilot-Server и видим, что три пользователя подключились к одной ноде, а три других – к другой.

Примечание: При повторном подключении пользователи автоматически направляются на те же самые реплики Pilot-Server, к которым были подключены ранее. Это обеспечивается механизмом “липких сессий”.

Заключение

В кластерном режиме система Pilot становится надёжнее, но и сложнее. Возрастает количество серверов и сервисов, требующих наблюдения, усложняется диагностика проблем, появляется необходимость в дополнительных инструментах для балансировки, распределённого хранения и оркестрации. Взамен мы получаем систему, которая продолжает работать при отказе отдельных серверов и распределяет нагрузку между несколькими узлами при её росте. Переход на кластерную архитектуру оправдан, если вашей системе нельзя допускать простоев, количество пользователей растёт и требует масштабирования и где есть специалисты для поддержания распределённой инфраструктуры.