Привет, друзья!
Сегодня поговорим о кластерном режиме в 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 становится надёжнее, но и сложнее. Возрастает количество серверов и сервисов, требующих наблюдения, усложняется диагностика проблем, появляется необходимость в дополнительных инструментах для балансировки, распределённого хранения и оркестрации. Взамен мы получаем систему, которая продолжает работать при отказе отдельных серверов и распределяет нагрузку между несколькими узлами при её росте. Переход на кластерную архитектуру оправдан, если вашей системе нельзя допускать простоев, количество пользователей растёт и требует масштабирования и где есть специалисты для поддержания распределённой инфраструктуры.
