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

Вводные:

  1. AstraLinux

  2. Postgrespro-12

  3. 2 сервера, нет возможности создания облачного или постановки третьего

  4. Нет внешней связи, ограниченная среда

  5. Собрать максимально отказоустойчивую систему

Итак, получив вводные я приступил к подбору стека, на данный момент все популярные решения по репликации сводятся к трём серверам, часто используется связка: patroni+haproxy+keepalived+etcd(zookeeper, konsul), на двух машинах это тоже работает, если жива основная нода. Стоит её уронить и всё, база закрыта на запись.

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

На всякий случай поясню, что такое keepalived – это легкое приложение, для обеспечения отказоустойчивости сервисов и передачи VIP (virtual ip address).

Первым делом останавливаем базу и инициируем сервер:

sudo rm -rvf /var/lib/pgpro/std-12/data
sudo -u root /opt/pgpro/std-12/bin/pg-setup initdb

Пример положительного вывода:

5432
Server will use port 5432
OK

Теперь наш сервер доступен на порту 5432.
Идем в pg_hba.conf и вносим изменения:

local   all             all                                     peer
# IPv4 local connections:
host    all             all             127.0.0.1/32            trust
# IPv6 local connections:
host    all             all             ::1/128                 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     peer
host    replication     replicator      195.117.117.31/31       md5 #первый сервер
host    replication     replicator      195.117.117.32/32       md5 #второй сервер
host    replication     replicator      195.117.117.0/24        md5
host    all             replicator      195.117.117.31/31       md5
host    all             replicator      195.117.117.32/32       md5
host    all             all             195.117.117..30/32       trust #виртуальный ip для keepalived

Обязательно нужно зайти в postgresql.conf и отредактировать строку для получения соединения от другого сервера:

listen_addresses = '*'

Изменяем права и перезапускаем базу, после проверяем ее статус:

sudo chown postgres:postgres -Rv /var/lib/pgpro/
sudo systemctl restart postgrespro-std-12
sudo systemctl status postgrespro-std-12

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

sudo -u postgres psql -U postgres -h 195.117.117.31 -c "CREATE USER replicator REPLICATION LOGIN ENCRYPTED PASSWORD 'replicate';"

С помощью команды проверяем статус инстанса postgrespro (рекомендую положить в скрипт, потому что в процессе настройки вы будете часто её вызывать на обоих серверах) :

sudo -u postgres psql -c "
SELECT
    'Текущий сервер' AS node,
    inet_server_addr() AS server_ip,
    CASE WHEN pg_is_in_recovery() THEN 'replica' ELSE 'master' END AS role,
    current_timestamp AS check_time;
"

Примеры выводов:

      node      | server_ip |  role  |          check_time
----------------+-----------+--------+-------------------------------
 Текущий сервер |           | master | 2025-09-03 17:29:58.768637+04
(1 row)
      node      | server_ip |  role   |          check_time
----------------+-----------+---------+-------------------------------
 Текущий сервер |           | replica | 2025-09-03 18:09:30.583748+04
(1 row)

Начнем с предварительной чистки и смены прав на каталоги:

sudo rm -rvf /var/lib/pgpro/std-12/data
sudo mkdir -p /var/lib/pgpro/std-12/data
sudo chown -Rv postgres:postgres /var/lib/pgpro/std-12/data
sudo chmod -Rv 700 /var/lib/pgpro/std-12/data

Далее выполняем репликацию базы данных:

sudo -u postgres pg_basebackup \
  -h 192.168.169.31 \
  -U replicator \
  -D /var/lib/pgpro/std-12/data \
  -P  \
  -v -R -W
	#пароль созданного юзера replicator c первого сервера (replicate)

В обязательном порядке выполняем рестарт postgrespro после репликации:

sudo systemctl restart postgrespro-std-12

После выполнения всех операций возвращаемся на первый сервер и проверяем работу реплики:

sudo -u postgres psql -c "SELECT client_addr, state FROM pg_stat_replication;"

Пример положительного вывода:

  client_addr   |   state
   ----------------+-----------
 195.117.117.32 | streaming
(1 row)

Первый этап завершен, теперь первый сервер - мастер, второй - реплика, пора приступать к следующему этапу. Я использую MobaXterm и его функцию MultiExec, для ввода команд сразу на нескольких серверах. Теперь нам необходимо создать скрипты на обоих серверах и сделать их исполняемыми:

  1. Проверяем, запущен ли PostgreSQL (на любом сервере):

#check-pg-alive.sh
#!/bin/bash

if sudo -u postgres pg_isready -h 127.0.0.1 -U postgres -t 3 >/dev/null 2>&1; then
    exit 0
else
    exit 1
fi
  1. Поднятие инстанса в статусе мастера, если сосед не отвечает или не мастер:

#promote-standby.sh
#!/bin/bash

# Путь к логу
LOG="/var/log/postgresql/promote.log"
mkdir -p /var/log/postgresql
exec >> "$LOG" 2>&1
echo "$(date): Starting promotion process..."

# Проверяем, не мастер ли мы уже (если НЕ в recovery — значит, уже мастер)
if sudo -u postgres psql -tAc "SELECT pg_is_in_recovery();" 2>/dev/null | grep -q "f"; then
    echo "$(date): Already master, nothing to do."
    exit 0
fi

# Проверяем, что мы в режиме восстановления (реплика)
if ! sudo -u postgres psql -tAc "SELECT pg_is_in_recovery();" 2>/dev/null | grep -q "t"; then
    echo "$(date): Not in recovery mode — cannot promote (not a replica)."
    exit 1
fi

# Дополнительно: проверим, есть ли standby.signal (опционально, но полезно)
STANDBY_SIGNAL="/var/lib/pgpro/std-12/data/standby.signal"
if [ ! -f "$STANDBY_SIGNAL" ]; then
    echo "$(date): Warning: standby.signal not found. Promotion will still proceed if in recovery."
fi

# Запускаем promote
echo "$(date): Promoting to master..."
sudo -u postgres pg_ctl promote -D /var/lib/pgpro/std-12/data

# Ждём 3 секунды — promote обычно синхронный, но даём время
sleep 3

# Проверяем результат
if sudo -u postgres psql -tAc "SELECT pg_is_in_recovery();" 2>/dev/null | grep -q "f"; then
    echo "$(date): Promotion successful — server is now master."
    exit 0
else
    echo "$(date): Promotion failed — still in recovery or PostgreSQL unreachable."
    exit 1
fi
  1. Делает из текущего инстанса реплику, если сосед мастер:

#rejoin-replica.sh
#!/bin/bash
set -euo pipefail

# === НАСТРОЙКИ ===
LOG="/var/log/postgresql/rejoin.log"
DATA_DIR="/var/lib/pgpro/std-12/data"
PEER_IP="195.117.117.32"       # IP предполагаемого мастера 32/31 в зависимости от того на какой ноде висит скрипт
REPL_USER="replicator"
PASSWORD="replicate"
PORT="5432"

MAX_ATTEMPTS=3
RETRY_DELAY=5
STARTUP_TIMEOUT=20
ROLE_CHECK_ATTEMPTS=5
ROLE_CHECK_DELAY=5


sleep 100 # указывается для второго сервера, что бы не допустить одновременного поднятия двух мастеров  

# === ПОДГОТОВКА ЛОГОВ ===
mkdir -p "$(dirname "$LOG")"
chown postgres:postgres "$(dirname "$LOG")" || true
chmod 755 "$(dirname "$LOG")" || true
exec >> "$LOG" 2>&1
echo "[$(date)] === STARTING SMART REJOIN ==="

# === ФУНКЦИЯ: ПРОВЕРКА, ЯВЛЯЕТСЯ ЛИ УЗЕЛ МАСТЕРОМ ===
is_master() {
    local ip=$1
    if runuser -l postgres -c "
        export PGPASSWORD='$PASSWORD';
        psql -h '$ip' -U '$REPL_USER' -p '$PORT' -d 'postgres' -Atc 'SELECT pg_is_in_recovery();'
    " 2>/dev/null | grep -q "^f$"; then
        return 0  # Это мастер
    else
        return 1  # Не мастер (реплика или недоступен)
    fi
}

# === ФУНКЦИЯ: ПРОВЕРКА, ЗАПУЩЕН ЛИ ЛОКАЛЬНЫЙ ПОСТГРЕС ===
is_local_postgres_running() {
    pg_ctl -D "$DATA_DIR" status &>/dev/null
}

# === ФУНКЦИЯ: ОСТАНОВКА ЛОКАЛЬНОГО ПОСТГРЕСА ===
stop_postgres() {
    echo "[$(date)] Stopping PostgreSQL..."
    if is_local_postgres_running; then
        runuser -l postgres -c "pg_ctl -D '$DATA_DIR' stop -m fast"
    fi
}

# === ФУНКЦИЯ: ЗАПУСК ПОСТГРЕСА (КАК РЕПЛИКА) ===
start_postgres() {
    echo "[$(date)] Starting PostgreSQL..."
    if ! is_local_postgres_running; then
        runuser -l postgres -c "pg_ctl -D '$DATA_DIR' start"
    fi
}

# === ФУНКЦИЯ: ПОВЫШЕНИЕ ДО МАСТЕРА ===
promote_to_master() {
    echo "[$(date)] Promoting to master..."

    # Убедимся, что PostgreSQL запущен
    start_postgres

    # Проверяем, в режиме ли recovery (т.е. реплика)
    if runuser -l postgres -c "psql -tAc 'SELECT pg_is_in_recovery();'" 2>/dev/null | grep -q "^t$"; then
        runuser -l postgres -c "pg_ctl promote -D '$DATA_DIR'"
        sleep 3
        if runuser -l postgres -c "psql -tAc 'SELECT pg_is_in_recovery();'" 2>/dev/null | grep -q "^f$"; then
            echo "[$(date)] Promotion successful — now master."
        else
            echo "[$(date)] Promotion failed or still in recovery."
            exit 1
        fi
    elif runuser -l postgres -c "psql -tAc 'SELECT pg_is_in_recovery();'" 2>/dev/null | grep -q "^f$"; then
        echo "[$(date)] Already master, nothing to do."
    else
        echo "[$(date)] PostgreSQL is unreachable."
        exit 1
    fi
}

# === ФУНКЦИЯ: СТАТЬ РЕПЛИКОЙ ===
become_replica() {
    echo "[$(date)] Master $PEER_IP is alive and is master. Becoming replica..."

    stop_postgres

    echo "[$(date)] Cleaning old data directory..."
    rm -rf "$DATA_DIR"
    mkdir -p "$DATA_DIR"
    chown postgres:postgres "$DATA_DIR"
    chmod 700 "$DATA_DIR"

    echo "[$(date)] Running pg_basebackup from $PEER_IP..."
    if ! runuser -l postgres -c "
        export PGPASSWORD='$PASSWORD';
        pg_basebackup \
            -h '$PEER_IP' \
            -p '$PORT' \
            -U '$REPL_USER' \
            -D '$DATA_DIR' \
            -v \
            -P \
            -R
    "; then
        echo "[$(date)] pg_basebackup failed!"
        exit 1
    fi
    echo "[$(date)] Base backup completed."

    # Убедимся, что standby.signal есть
    if [[ ! -f "$DATA_DIR/standby.signal" ]]; then
        echo "[$(date)] Creating standby.signal..."
        touch "$DATA_DIR/standby.signal"
        chown postgres:postgres "$DATA_DIR/standby.signal"
        chmod 600 "$DATA_DIR/standby.signal"
    fi

    start_postgres
    echo "[$(date)] Replica setup complete and PostgreSQL started."
}

# === ОСНОВНАЯ ЛОГИКА ===

# Ждём немного, чтобы сеть поднялась
sleep 15

# Проверяем, можем ли мы достучаться до мастера и является ли он мастером
echo "[$(date)] Checking master $PEER_IP status..."

if is_master "$PEER_IP"; then
    echo "[$(date)] Master $PEER_IP is alive and is master."
    become_replica
else
    echo "[$(date)] Master $PEER_IP is unreachable or not master. Promoting self to master."
    promote_to_master
fi

echo "[$(date)] === SMART REJOIN FINISHED ==="

Обновляем конфигурацию systemd:

sudo systemctl daemon-reload

Второй этап завершен, переходим к установке и настройке keepalived(VIP), для статьи я взял ip 195.117.117.30.

Устанавливаем keepalived:

sudo apt update
sudo apt install keepalived

Находим конфиг keepalived.conf и редактируем его:

global_defs {
    router_id LVS_DEVEL
}

vrrp_script chk_postgresql {
    script "/usr/local/bin/check-pg-alive.sh"
    interval 1
    timeout 1
    weight 2
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 101 # на реплике значение 100
    advert_int 1
    nopreempt # данная настройка предотвращает передачу VIP, если второй сервер стал мастером, несмотря на приоритет

    authentication {
        auth_type PASS
        auth_pass your_keepalived_password
    }

    virtual_ipaddress {
        195.117.117.30/32 dev eth0 label eth0:0
    }

    track_script {
        chk_postgresql
    }

    notify_master /usr/local/bin/promote-standby.sh
}

После перезагрузки сервиса keepalived репликация настроена!

Проверить где сейчас VIP можно командой:

ip addr show | grep 195.117.117.30

Итоги проделанной работы:

  1. Если падает первый сервер, то второй становится мастером, при поднятии первого, мастер остается на втором.

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

  3. При одновременном падении серверов и полном отказе первого, второй станет мастером, через 100 секунд после поднятия, первый после поднятия станет репликой.

  4. Учитывайте возможность Split Brain - поднятия одновременно двух мастеров, в моем кейсе это допустимо, потому что приложение, использующее базу живет только на одном сервере, при потере и последующем восстановлении связи, достаточно просто перезагрузить второй сервер, либо можно добавить еще один скрипт, который будет пинговать соседа на вторую машину, если сосед поднялся мастером - буду репликой.

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

Комментарии (6)


  1. jackfs1919
    10.09.2025 19:29

    то ,что надо


    1. pmax-ev Автор
      10.09.2025 19:29

      Спасибо)


  1. AlekseyPeregudov
    10.09.2025 19:29

    Я так понимаю, защиты от SplitBrain в принципе не предусмотрено?


    1. pmax-ev Автор
      10.09.2025 19:29

      Тут играет роль сам кейс, приложение, для которого используется данная сборка функционирует на одном сервере, к примеру у нас возникла ситуация, что нет связи между серверами и поднялись оба, имеем два мастера, приложение запущено все так же на первом, но репликации нет, в виду отсутствия связи, что мы делаем после того, как связь появилась, ребутаем второй сервак и он поднимается репликой, данные снова согласованны. P.S. Данное решение не панацея) Конкретно в моем случае подходит, возможно его можно доработать и под другие кейсы


      1. AlekseyPeregudov
        10.09.2025 19:29

        Безусловно, если в ваших условиях Split Brain допустим, то норм.

        Просто я считаю необходимым предупреждать ищущих решение в интернете о возможности такой ситуации чтобы это не было сюрпризом. Что при потере связности узлов появятся два активных мастера и keepalive с чистой душой поднимет два одинаковых IP-шника на обоих нодах. И что в итоге за каша в данных может получиться даже бог не ведает.

        Именно поэтому (почти) нет штатных двухузловых конфигураций кластеров на основе репликации. А не из-за вредности разработчиков :)

        Про "почти" могу отдельно написать, если интересно. Но для двух локальных узлов можно посмотреть в сторону кластеризации не на основе репликации, а на основе Shared Storage. Как пример - PaceMaker (не к ночи помянут будет).


        1. pmax-ev Автор
          10.09.2025 19:29

          Благодарю за освещение узкого места, внес информацию об этом в статью=) На досуге поковыряю Shared Storage и PaceMaker