Всем привет, получил от руководства интересную задачу, возможно кому то это поможет сэкономить много рабочих часов. Итак суть – необходимо настроить репликацию базы данных, по возможности сделать систему отказоустойчивой.
Вводные:
AstraLinux
Postgrespro-12
2 сервера, нет возможности создания облачного или постановки третьего
Нет внешней связи, ограниченная среда
Собрать максимально отказоустойчивую систему
Итак, получив вводные я приступил к подбору стека, на данный момент все популярные решения по репликации сводятся к трём серверам, часто используется связка: 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, для ввода команд сразу на нескольких серверах. Теперь нам необходимо создать скрипты на обоих серверах и сделать их исполняемыми:
Проверяем, запущен ли 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
Поднятие инстанса в статусе мастера, если сосед не отвечает или не мастер:
#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
Делает из текущего инстанса реплику, если сосед мастер:
#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
Итоги проделанной работы:
Если падает первый сервер, то второй становится мастером, при поднятии первого, мастер остается на втором.
При одновременном падении серверов, первый поднимется мастером, второй репликой
При одновременном падении серверов и полном отказе первого, второй станет мастером, через 100 секунд после поднятия, первый после поднятия станет репликой.
Учитывайте возможность Split Brain - поднятия одновременно двух мастеров, в моем кейсе это допустимо, потому что приложение, использующее базу живет только на одном сервере, при потере и последующем восстановлении связи, достаточно просто перезагрузить второй сервер, либо можно добавить еще один скрипт, который будет пинговать соседа на вторую машину, если сосед поднялся мастером - буду репликой.
P.S. Это моя первая статья, прошу не кидать тапки сразу, напомню что кейс в изолированной среде, по этому я не работал с настройками безопасности.
Комментарии (6)
AlekseyPeregudov
10.09.2025 19:29Я так понимаю, защиты от SplitBrain в принципе не предусмотрено?
pmax-ev Автор
10.09.2025 19:29Тут играет роль сам кейс, приложение, для которого используется данная сборка функционирует на одном сервере, к примеру у нас возникла ситуация, что нет связи между серверами и поднялись оба, имеем два мастера, приложение запущено все так же на первом, но репликации нет, в виду отсутствия связи, что мы делаем после того, как связь появилась, ребутаем второй сервак и он поднимается репликой, данные снова согласованны. P.S. Данное решение не панацея) Конкретно в моем случае подходит, возможно его можно доработать и под другие кейсы
AlekseyPeregudov
10.09.2025 19:29Безусловно, если в ваших условиях Split Brain допустим, то норм.
Просто я считаю необходимым предупреждать ищущих решение в интернете о возможности такой ситуации чтобы это не было сюрпризом. Что при потере связности узлов появятся два активных мастера и keepalive с чистой душой поднимет два одинаковых IP-шника на обоих нодах. И что в итоге за каша в данных может получиться даже бог не ведает.
Именно поэтому (почти) нет штатных двухузловых конфигураций кластеров на основе репликации. А не из-за вредности разработчиков :)
Про "почти" могу отдельно написать, если интересно. Но для двух локальных узлов можно посмотреть в сторону кластеризации не на основе репликации, а на основе Shared Storage. Как пример - PaceMaker (не к ночи помянут будет).
pmax-ev Автор
10.09.2025 19:29Благодарю за освещение узкого места, внес информацию об этом в статью=) На досуге поковыряю Shared Storage и PaceMaker
jackfs1919
то ,что надо
pmax-ev Автор
Спасибо)