Переход на раздельные кластеры Docker Swarm для режима отказоустойчивости

В версии 6.9.0 изменено устройство отказоустойчивости: основной и резервный серверы больше не объединяются в общий кластер Docker Swarm. Для обновления серверов потребуется отключение текущей отказоустойчивости.

Предупреждение

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

Во время работы скрипта update.py приложение может быть недоступно для пользователей в течение нескольких минут.

Перед началом убедитесь, что основной сервер является активным - VIP находится на нём. Если VIP находится на резервном сервере, сначала переключите его на основной в соответствии с инструкцией по ручному переключению.

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

Этап 1. Отключение отказоустойчивости

  1. На обоих серверах перейдите в директорию установщика и задайте переменные:

    INSTALLER=$(pwd); \
    BACKUP_DIR=/opt/compass_db_backup_690; \
    VIP=$(sudo grep -A3 virtual_ipaddress /etc/keepalived/keepalived.conf | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1); \
    SELF_IP=$(grep -E '^host_ip:' $INSTALLER/configs/global.yaml | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1); \
    PEER_IP=$(sudo grep -E '^\s*target_host:' /etc/rsync-replication/sync_files.yaml | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1); \
    ROOT_MOUNT=$(grep -E '^root_mount_path:' $INSTALLER/configs/global.yaml | sed -E "s/^root_mount_path:[[:space:]]*//; s/[\"']//g"); \
    echo "установщик:     $INSTALLER"; \
    echo "этот сервер:    $SELF_IP"; \
    echo "второй сервер:  $PEER_IP"; \
    echo "виртуальный IP: $VIP"; \
    echo "данные:         $ROOT_MOUNT"; \
    echo "копия БД:       $BACKUP_DIR"
    

    Убедитесь, что все значения заполнены, а значение «второй сервер» на основном сервере совпадает со значением «этот сервер» на резервном и наоборот.

    Примечание

    В BACKUP_DIR указан каталог для копии баз данных, которая понадобится на этапе повторной настройки. Каталог можно изменить, но путь должен быть одинаковым на обоих серверах.

  2. Проверьте, что основной сервер является активным. На основном сервере выполните:

    sudo python3 script/replication/check_current_master_server.py; \
    grep "service_label:" src/values.compass.yaml; \
    ip a | grep $VIP
    

    Ожидаемый результат: сообщение «Данный сервер является активным мастер сервером», service_label: primary, master_service_label: primary и строка с VIP.

    Выполните ту же команду на резервном сервере. Ожидаемый результат: сообщение «Данный сервер НЕ является активным мастер сервером», service_label: reserve, master_service_label: primary, строки с VIP нет.

  3. На резервном сервере остановите keepalived:

    sudo systemctl stop keepalived; sudo systemctl disable keepalived
    

    Предупреждение

    На основном сервере keepalived не останавливайте: он удерживает VIP, и приложение остаётся доступным по домену на протяжении всего обновления.

  4. На основном сервере очистите метку и идентификатор сервера в конфигурационном файле configs/replication.yaml:

    sudo sed -i -E 's/^service_label:.*/service_label: ""/; s/^mysql_server_id:.*/mysql_server_id: 0/' $INSTALLER/configs/replication.yaml
    

    Проверьте результат:

    grep -E '^(service_label|mysql_server_id):' $INSTALLER/configs/replication.yaml
    

    Ожидаемый результат: service_label: "" и mysql_server_id: 0.

  5. На основном сервере сохраните идентификатор ноды резервного сервера:

    NODE_ID=$(sudo docker node ls -q --filter node.label=role=reserve); \
    echo "нода резервного сервера: $NODE_ID"; \
    sudo docker node ls
    

    Ожидаемый результат: один идентификатор — ноды резервного сервера.

  6. На основном сервере понизьте уровень ноды резервного сервера и выведите её из работы:

    sudo docker node demote $NODE_ID
    
    sudo docker node update --availability drain $NODE_ID
    

    Проверьте, что у ноды резервного сервера статус Drain:

    sudo docker node ls
    
  7. На резервном сервере выведите сервер из кластера Docker Swarm:

    sudo docker swarm leave --force
    

    На основном сервере проверьте, что у ноды резервного сервера статус Down:

    sudo docker node ls
    

    Если статус ещё Ready, подождите несколько секунд и повторите проверку. Затем удалите ноду из кластера:

    sudo docker node rm $NODE_ID
    
  8. На основном сервере разверните приложение без меток:

    Примечание

    При запуске скрипта появится предупреждение, что при смене service_label приложение будет недоступно для пользователей. Необходимо подтвердить дальнейшее выполнение.

    sudo python3 script/update.py
    

    Дождитесь завершения обновления.

  9. На основном сервере выполните обновление manticore для корректной работы поиска:

    MANTICORE=$(sudo docker ps -q --filter name=_manticore-d1 | head -1); \
    echo "контейнер manticore: $MANTICORE"; \
    sudo docker exec $MANTICORE rm -f /var/lib/manticore/manticore.json; \
    sudo docker service update --force $(sudo docker inspect $MANTICORE --format '{{index .Config.Labels "com.docker.swarm.service.name"}}')
    

    Ожидаемый результат: непустой идентификатор контейнера и сообщение Service ... converged.

  10. Проверьте, что отказоустойчивость отключена. На основном сервере выполните:

    sudo docker node ls; \
    grep service_label $INSTALLER/src/values.compass.yaml; \
    ip a | grep $VIP
    

    Ожидаемый результат: в списке одна нода, значения service_label и master_service_label пустые, строка с VIP на месте.

    На резервном сервере выполните:

    systemctl is-active keepalived; \
    sudo docker info --format '{{.Swarm.LocalNodeState}}'
    

    Ожидаемый результат: inactive и inactive.

Этап 2. Обновление основного сервера

  1. На основном сервере обновите установщик:

    git pull
    

    Запустите обновление:

    sudo python3 script/update.py
    

    Дождитесь завершения обновления.

Этап 3. Повторная настройка отказоустойчивости

  1. На резервном сервере создайте собственный кластер Docker Swarm:

    sudo docker swarm init --advertise-addr $SELF_IP
    

    Команда выведет подсказку, как подключить к кластеру другие ноды, - выполнять её не нужно.

  2. На основном сервере заполните конфигурационный файл configs/replication.yaml:

    sudo sed -i -E 's/^service_label:.*/service_label: "primary"/; s/^mysql_server_id:.*/mysql_server_id: 1/' $INSTALLER/configs/replication.yaml; \
    grep -q '^peer_host:' $INSTALLER/configs/replication.yaml \
      && sudo sed -i -E "s|^peer_host:.*|peer_host: \"$PEER_IP\"|" $INSTALLER/configs/replication.yaml \
      || echo "peer_host: \"$PEER_IP\"" | sudo tee -a $INSTALLER/configs/replication.yaml > /dev/null; \
    grep -q '^master_state_changed_disable:' $INSTALLER/configs/replication.yaml \
      && sudo sed -i -E 's/^master_state_changed_disable:.*/master_state_changed_disable: true/' $INSTALLER/configs/replication.yaml \
      || echo "master_state_changed_disable: true" | sudo tee -a $INSTALLER/configs/replication.yaml > /dev/null
    

    Проверьте результат:

    grep -E '^(service_label|mysql_server_id|peer_host|master_state_changed_disable):' $INSTALLER/configs/replication.yaml
    

    Ожидаемый результат: service_label: "primary", mysql_server_id: 1, в peer_host — внутренний IP резервного сервера, master_state_changed_disable: true.

    Примечание

    Параметр peer_host появился в версии 6.9.0 — это внутренний IP-адрес второго сервера. Параметр обязателен, если заполнен service_label.

  3. На основном сервере примените изменения:

    Примечание

    При запуске скрипта снова появится предупреждение о смене service_label. Необходимо подтвердить дальнейшее выполнение.

    sudo python3 script/update.py
    

    Дождитесь завершения обновления.

  4. На основном сервере создайте mysql-пользователя для репликации баз данных:

    sudo python3 script/replication/create_mysql_user.py --type monolith
    
    sudo python3 script/replication/create_mysql_user.py --type team
    

    Если у вас больше одного пространства — выберите пункт «Все».

  5. На основном сервере запустите репликацию данных Manticore:

    sudo python3 script/replication/start_manticore_replication.py --type master --master-mysql-server-id 1 --need-update-company 1
    
  6. На основном сервере проверьте доступ к резервному серверу от имени пользователя rider:

    sudo -u rider ssh -o BatchMode=yes rider@$PEER_IP hostname
    

    Ожидаемый результат: имя резервного сервера без запроса пароля. От имени пользователя rider работает синхронизация файлов, через него же на резервный сервер будет передана копия баз данных.

  7. Проверьте, что для копии баз данных достаточно места. На основном сервере выполните:

    sudo du -sch $ROOT_MOUNT/monolith/database $ROOT_MOUNT/db_company $ROOT_MOUNT/manticore; \
    df -h $(dirname $BACKUP_DIR)
    

    На резервном сервере выполните:

    df -h $(dirname $BACKUP_DIR)
    

    Свободного места на каждом сервере должно быть больше двух значений total с основного сервера. Пример вывода на основном сервере:

    3.1G    /home/compass/monolith/database
    8.4G    /home/compass/db_company
    512M    /home/compass/manticore
    12G     total
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/sda1       100G   30G   70G  30% /
    

    В этом примере total равен 12G, значит в колонке Avail на каждом сервере должно быть больше 24G. Если места не хватает, укажите в BACKUP_DIR на обоих серверах каталог на другом разделе диска и повторите проверку.

  8. На основном сервере создайте копию баз данных:

    sudo python3 script/backup_db.py --backups-folder $BACKUP_DIR
    

    Если скрипт завершился сообщением «Свободное место на диске превышает указанный порог для создания бэкапа», сверьтесь с проверкой места на предыдущем шаге и снизьте порог:

    sudo python3 script/backup_db.py --backups-folder $BACKUP_DIR --free-threshold-percent 30
    
  9. На основном сервере передайте копию на резервный сервер:

    sudo rsync -a --rsync-path="sudo rsync" -e "ssh -l rider -i /home/rider/.ssh/id_rsa" $BACKUP_DIR/ $PEER_IP:$BACKUP_DIR/
    

    При первом подключении может появиться запрос на подтверждение ключа сервера — ответьте yes.

    На резервном сервере проверьте, что копия получена:

    sudo ls -la $BACKUP_DIR/*
    

    Ожидаемый результат: одна папка с копией, в которой находятся файлы control.json, configs.tgz, mysql.tgz и mysql_company_*.tgz.

  10. На резервном сервере обновите установщик:

    git pull
    

    Примените миграции конфигурационных файлов и проверьте версию установщика:

    sudo python3 script/installer_migrations_up.py; \
    cat $INSTALLER/.version
    

    Ожидаемый результат: 6.9.0.

  11. На резервном сервере заполните конфигурационный файл configs/replication.yaml:

    sudo sed -i -E 's/^service_label:.*/service_label: "reserve"/; s/^mysql_server_id:.*/mysql_server_id: 2/' $INSTALLER/configs/replication.yaml; \
    grep -q '^peer_host:' $INSTALLER/configs/replication.yaml \
      && sudo sed -i -E "s|^peer_host:.*|peer_host: \"$PEER_IP\"|" $INSTALLER/configs/replication.yaml \
      || echo "peer_host: \"$PEER_IP\"" | sudo tee -a $INSTALLER/configs/replication.yaml > /dev/null
    

    Проверьте результат:

    grep -E '^(service_label|mysql_server_id|peer_host):' $INSTALLER/configs/replication.yaml; \
    grep -E '^host_ip:' $INSTALLER/configs/global.yaml
    

    Ожидаемый результат: service_label: "reserve", mysql_server_id: 2, в peer_host — внутренний IP основного сервера, в host_ip — внутренний IP резервного сервера.

  12. Перенесите ключ-секрет шифрования базы данных на резервный сервер.

    Примечание

    Если на серверах не включено шифрование сообщений, этот шаг нужно пропустить.

    Перенос ключа-секрета шифрования базы данных

    Ключ-секрет хранится в кластере Docker Swarm основного сервера и на резервный автоматически не переносится.

    Проверьте, включено ли шифрование. На основном сервере выполните:

    grep -A4 "^database_encryption:" $INSTALLER/configs/database.yaml | grep "mode:"
    

    Если команда вернула mode: "none", шифрование выключено — пропустите этот шаг.

    На основном сервере сохраните ключ-секрет в файл:

    sudo docker exec $(sudo docker ps -q --filter name=php-monolith | head -1) cat /run/secrets/compass_database_encryption_secret_key | sudo tee /root/compass_db_secret.b64 > /dev/null; \
    sudo chmod 600 /root/compass_db_secret.b64
    

    Передайте файл на резервный сервер и удалите его на основном:

    sudo rsync -a --rsync-path="sudo rsync" -e "ssh -l rider -i /home/rider/.ssh/id_rsa" /root/compass_db_secret.b64 $PEER_IP:/root/; \
    sudo shred -u /root/compass_db_secret.b64
    

    На резервном сервере создайте ключ-секрет из файла и удалите файл:

    sudo docker secret create compass_database_encryption_secret_key /root/compass_db_secret.b64; \
    sudo shred -u /root/compass_db_secret.b64; \
    sudo docker secret ls
    

    Ожидаемый результат: в списке есть compass_database_encryption_secret_key.

  13. На резервном сервере восстановите базы данных из копии.

    Добавьте сертификат в список доверенных и обновите системное хранилище доверенных сертификатов:

    sudo cp -a $INSTALLER/certs/compassRootCA.crt /usr/local/share/ca-certificates/; \
    sudo update-ca-certificates
    

    Запустите восстановление:

    sudo python3 script/restore_db.py --backups-folder $BACKUP_DIR --force-update-company-db 0
    

    Скрипт предложит выбрать копию из списка и подтвердить остановку приложения. Копия в списке одна — выберите 1, затем ответьте y.

    Дождитесь завершения скрипта и проверьте состояние развёртывания:

    sudo docker service ls | grep reserve | grep -vE "default-file|jitsi-custom"
    
  14. На резервном сервере обновите порты в сервисе go_database:

    sudo python3 script/replication/update_database_port.py
    

    Инвалидируйте порты, на которых были созданы пространства:

    sudo docker exec -it $(sudo docker ps | grep php_monolith | awk '{print $1}') bash -c "php src/Compass/Pivot/sh/php/domino/invalid_port_repairer.php"
    

    Скрипт предложит выбрать идентификатор сервера с базой данных — выберите d1 — и порты для инвалидации. Если скрипт сообщает, что порты с нужным статусом отсутствуют, пропустите эту команду.

    Восстановите пространства:

    sudo python3 script/replication/repair_all_teams.py
    

    Проверьте, что базы данных пространств запущены:

    sudo docker ps --format '{{.Names}}' | grep -c mysql-
    

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

  15. На резервном сервере создайте mysql-пользователя для репликации баз данных:

    sudo python3 script/replication/create_mysql_user.py --type monolith
    
    sudo python3 script/replication/create_mysql_user.py --type team
    

    Если у вас больше одного пространства — выберите пункт «Все».

  16. На резервном сервере сбросьте состояние репликации:

    sudo python3 script/replication/reset_slave_replication.py --all-types --all-teams
    

    Запустите репликацию баз данных с позиции, сохранённой в копии:

    sudo python3 script/replication/start_slave_replication.py --all-types --all-teams --start-from-backup
    

    Запустите репликацию данных Manticore:

    sudo python3 script/replication/start_manticore_replication.py --type reserve --master-mysql-server-id 1
    
  17. На резервном сервере проверьте состояние репликации:

    sudo python3 script/replication/show_slave_replication_status.py --all-types --all-teams --log-level=3
    

    Ожидаемый результат по каждой базе данных: Slave_IO_Running: Yes, Slave_SQL_Running: Yes, Seconds_Behind_Master: 0, в Master_Host — внутренний IP основного сервера.

  18. На обоих серверах проверьте кластер Manticore:

    sudo docker exec $(sudo docker ps -q --filter name=_mysql-monolith | head -1) mysql -h manticore-d1 -P9306 -e "SHOW STATUS LIKE 'cluster_compass_cluster_%'" | grep -E 'cluster_compass_cluster_(status|size|node_state)\s'
    

    Ожидаемый результат на обоих серверах: primary, 2 и synced. Если на резервном сервере результат другой, повторите на основном сервере запуск репликации Manticore с параметром --type master, затем на резервном — с параметром --type reserve.

Этап 4. Настройка keepalived

  1. На обоих серверах добавьте в конфигурационный файл /etc/keepalived/keepalived.conf проверку состояния баз данных. Сначала сохраните копию файла:

    sudo cp -a /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak
    

    Добавьте блок vrrp_script chk_mysql и строку chk_mysql weight -30 в блок track_script:

    sudo grep -q 'vrrp_script chk_mysql' /etc/keepalived/keepalived.conf \
      || sudo sed -i "/^vrrp_instance VI_1/i vrrp_script chk_mysql {\n   script \"$INSTALLER/script/replication/check_mysql.py\"\n   interval 10\n   fall 3\n   rise 2\n   timeout 8\n}\n" /etc/keepalived/keepalived.conf; \
    sudo grep -q 'chk_mysql weight' /etc/keepalived/keepalived.conf \
      || sudo sed -i "/track_script {/a\      chk_mysql weight -30" /etc/keepalived/keepalived.conf
    

    Проверьте результат:

    sudo grep -A7 'vrrp_script chk_mysql' /etc/keepalived/keepalived.conf; \
    sudo grep -A6 'track_script {' /etc/keepalived/keepalived.conf
    

    Ожидаемый результат: блок vrrp_script chk_mysql с путём к установщику этого сервера и строка chk_mysql weight -30 в блоке track_script. Актуальный вид конфигурационного файла приведён в статье «Настройка keepalived».

    Команду можно выполнить повторно — блок не будет продублирован. Если что-то пошло не так, верните исходный файл из копии:

    sudo cp -a /etc/keepalived/keepalived.conf.bak /etc/keepalived/keepalived.conf
    
  2. На основном сервере перечитайте конфигурацию keepalived:

    sudo systemctl reload keepalived
    

    Проверьте, что keepalived работает и VIP остался на сервере:

    systemctl is-active keepalived; ip a | grep $VIP
    

    Примечание

    Используйте именно reload: при перечитывании конфигурации VIP остаётся на сервере, и приложение продолжает работать. При restart VIP ненадолго пропадёт.

  3. На резервном сервере запустите keepalived:

    sudo systemctl enable keepalived; sudo systemctl start keepalived
    

Этап 5. Задания cron

  1. На обоих серверах замените задания отказоустойчивости в crontab. Сначала сохраните текущее расписание:

    sudo crontab -l > ~/crontab-before-690.txt 2>/dev/null; cat ~/crontab-before-690.txt
    

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

    { sudo crontab -l 2>/dev/null | grep -vE 'script/replication/(sync_files|show_slave_replication_status|ensure_replication)\.py'; \
      echo "* * * * * /usr/bin/python3 $INSTALLER/script/replication/sync_files.py --vip $VIP --monitoring > /dev/null 2>&1 &"; \
      echo "* * * * * /usr/bin/python3 $INSTALLER/script/replication/show_slave_replication_status.py --all-types --all-teams --monitoring --stop-keepalived-on-replica-failure > /dev/null 2>&1 &"; \
      echo "*/5 * * * * /usr/bin/python3 $INSTALLER/script/replication/ensure_replication.py --monitoring > /dev/null 2>&1 &"; } | sudo crontab -
    
  2. На обоих серверах проверьте задания:

    sudo crontab -l
    

    Ожидаемый результат: задания sync_files.py, show_slave_replication_status.py и ensure_replication.py с путём к установщику этого сервера.

Этап 6. Проверка результата

  1. На обоих серверах проверьте, что кластеры Docker Swarm раздельные:

    sudo docker info --format 'nodes={{.Swarm.Nodes}} cluster={{.Swarm.Cluster.ID}}'
    

    Ожидаемый результат: nodes=1 на каждом сервере и разные значения cluster.

  2. На обоих серверах проверьте состояние сервисов:

    sudo docker service ls
    

    Ожидаемый результат: у всех сервисов в колонке REPLICAS значение 1/1, кроме default-file-pivot и jitsi-custom-jitsi — для них штатно отображается 0/1.

  3. Проверьте, что VIP находится только на основном сервере. На обоих серверах выполните:

    ip a | grep $VIP
    

    Ожидаемый результат: на основном сервере строка с VIP есть, на резервном вывод пустой.

  4. На резервном сервере проверьте состояние репликации:

    sudo python3 script/replication/show_slave_replication_status.py --all-types --all-teams --log-level=3
    

    Ожидаемый результат — такой же, как при проверке репликации на этапе повторной настройки отказоустойчивости.



Напишите нам в пространстве поддержки On-premise, Telegram или на почту support@getcompass.ru, чтобы получить индивидуальную демонстрацию функционала и помощь по вопросам интеграции мессенджера в вашей компании.