Patroni - настройка кластера PostgreSQL на ОС Astra Linux
Patroni - это менеджер кластера, используемый для настройки и автоматизации развертывания и обслуживания кластеров PostgreSQL HA (High Availability). Он использует распределенные хранилища конфигурации, такие как etcd, consul, ZooKeeper или Kubernetes для максимальной доступности.
В этом руководстве мы будем использовать три локальных экземпляра etcd и три локальных экземпляра patroni. ОС на виртуалках Alt Linux 8.
Список ВМ - участников кластера
172.29.11.197 patroni1
172.29.11.198 patroni2
172.29.11.199 patroni3
Requirements
Ubuntu/Debian
apt update
apt install --no-install-recommends postgresql-11 postgresql-client-11
apt install etcd
apt install patroni
Alt Linux
sudo apt-get -y install postgresql11-server etcd
sudo apt-get -y install python3-module-psycopg2
sudo apt-get -y install python3-module-pip python-module-pip
sudo pip3 install psycopg2>=2.5.4
sudo apt-get -y install python3-module-setuptools
sudo pip install psycopg2-binary
sudo pip3 install patroni[etcd]
Изменим параметр ядра для max_connections increase
etcd
Инициализация кластера
Кластер etcd будет развёрнут на всех машинах стенда.
Построение кластера начинаем с первого узла - partoni1
Создаем файл конфигурации /etc/etcd/etcd.conf
#[Member]
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_NAME="core"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ELECTION_TIMEOUT="5000"
#[Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://patroni1:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://patroni1:2379"
ETCD_INITIAL_CLUSTER="core=http://patroni1:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-example"
ETCD_INITIAL_CLUSTER_STATE="new"
Справка по параметрам:
- ETCD_DATA_DIR - указывает расположение каталога данных кластера
- ETCD_LISTEN_PEER_URLS - задаёт схему и точку подключения для остальных узлов кластера, по шаблону scheme://IP:port. Схема может быть http, https. Альтернатива, unix:// или unixs:// для юникс сокетов. Если в качестве IP адреса указано 0.0.0.0, то указанный порт будет прослушиваться на всех интерфейсах.
- ETCD_LISTEN_CLIENT_URLS - задаёт схему и точку подключения для клиентов кластера. В остальном совпадает с ETCD_LISTEN_PEER_URLS.
- ETCD_NAME - человекочитаемое имя этого узла кластера. Должно быть уникально в кластере. Для первого узла может быть любым. Для последующих должно совпадать с именем, указанным при добавлении узла.
- ETCD_HEARTBEAT_INTERVAL - время в миллисекудах, между рассылками лидером оповещений о том, что он всё ещё лидер. Рекомендуется задавать с учётом сетевой задержки между узлами кластера.
- ETCD_ELECTION_TIMEOUT - время в миллисекундах, которое проходит между последним принятым оповещением от лидера кластера, до попытки захватить роль лидера на ведомом узле. Рекомендуется задавать его в несколько раз большим, чем ETCD_HEARTBEAT_INTERVAL. Более подробно о этих параметрах можно прочесть в документации.
- ETCD_INITIAL_ADVERTISE_PEER_URLS - Список равноправных URL-адресов, по которым его могут найти остальные узлы кластера. Эти адреса используются для передачи данных по кластеру. По крайней мере, один из этих адресов должен быть маршрутизируемым для всех членов кластера. Могут содержать доменные имена. Используется только при первом запуске нового узла кластера.
- ETCD_ADVERTISE_CLIENT_URLS - Список равноправных URL-адресов, по которым его могут найти остальные узлы кластера. Эти адреса используются для передачи данных по кластеру. По крайней мере, один из этих адресов должен быть маршрутизируемым для всех членов кластера. Могут содержать доменные имена.
- ETCD_INITIAL_CLUSTER - Список узлов кластера на момент запуска. Используется только при первом запуске нового узла кластера.
- ETCD_INITIAL_CLUSTER_TOKEN - Токен кластера. Должен совпадать на всех узлах кластера. Используется только при первом запуске нового узла кластера.
- ETCD_INITIAL_CLUSTER_STATE - может принимать два значения “new” и “existing”. Значение “new” используется при первом запуске первого узла в кластере. При значении “existing”, узел при старте будет пытаться установить связь с остальными узлами кластера.
Добавляем сопоставление IP-адреса и имена хостов на каждой из трех ВМ:
Запускаем сервис etcd на patroni1:
Проверяем, что демон запущен и находится в состоянии “active”.
Посмотрим список членов кластера - так пока только один узел - patroni1
etcdctl member list
e630a6438e488ddc, started, core, http://patroni1:2380, http://patroni1:2379, false
Добавление нового члена кластера
Добавление нового узла в кластер etcd происходит в два этапа. На первом этапе кластер предупреждается о появлении нового узла. На втором запускается сам новый узел. Следующие действия необходимо последовательно выполнить на всех оставшихся серверах стенда. Для примера будет использоваться patroni2
Создаем файл конфигурации второго члена кластера на ВМ patroni2:
#[Member]
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_NAME="patroni2"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ELECTION_TIMEOUT="5000"
#[Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://patroni2:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://patroni2:2379"
ETCD_INITIAL_CLUSTER="core=http://patroni1:2380,patroni2=http://patroni2:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-example"
ETCD_INITIAL_CLUSTER_STATE="existing"
Добавляем его:
etcdctl member add patroni2 --peer-urls=http://patroni2:2380
Member b76a9d1b01d7d4e9 added to cluster bafb2cc87b631fb0
ETCD_NAME="patroni2"
ETCD_INITIAL_CLUSTER="patroni2=http://patroni2:2380,core=http://patroni1:2380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://patroni2:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
Запускаем службу etcd и смотрим обновленный список членов кластера:
systemctl start etcd.service
systemctl start etcd
etcdctl member list
b76a9d1b01d7d4e9, started, patroni2, http://patroni2:2380, http://patroni2:2379, false
e630a6438e488ddc, started, core, http://patroni1:2380, http://patroni1:2379, false
Если все хорошо, повторяем операцию на 3-ем узле (patroni3)
#[Member]
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_NAME="patroni3"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ELECTION_TIMEOUT="5000"
#[Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://patroni3:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://patroni3:2379"
ETCD_INITIAL_CLUSTER="core=http://patroni1:2380,patroni2=http://patroni2:2380,patroni3=http://patroni3:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-example"
ETCD_INITIAL_CLUSTER_STATE="existing"
Добавляем, запускаем, проверяем:
rm -Rf /var/lib/etcd/default.etcd
etcdctl member add patroni3 --peer-urls=http://patroni3:2380
systemctl start etcd
etcdctl member list
a4e2c463094b9e35, started, patroni3, http://patroni3:2380, http://patroni3:2379, false
b76a9d1b01d7d4e9, started, patroni2, http://patroni2:2380, http://patroni2:2379, false
e630a6438e488ddc, started, core, http://patroni1:2380, http://patroni1:2379, false
Закрепление кластера
После установки и успешного запуска etcd на всех серверах, следует привести содержание файла /etc/etcd/etcd.conf в окончательное состояние. Для этого необходимо изменить следующие параметры в этом файле на всех серверах.
параметр “ETCD_INITIAL_CLUSTER” должен быть одинаковым на всех узлах
ETCD_INITIAL_CLUSTER="core=http://patroni1:2380,patroni2=http://patroni2:2380,patroni3=http://patroni3:2380"
параметр “ETCD_INITIAL_CLUSTER_STATE” следует установить в значение “existing”
После этого необхолимо сделать рестарт службы etcd на всех узлах, кроме 3-го (на нем не меняли конфигурацию).
Авторизация
Добавим авторизацию по логину и паролю при обращениях на клиентский интерфейс etcd.
Создаём пользователя root
:
Назначаем роль root
:
Проверим:
Включаем проверку логина и пароля:
Проверяем, что изменения вступили в силу:
etcdctl user get root
Insufficient credentials
etcdctl --user root user get root
Password:
User: root
Roles: root
Patroni
Перед запуском кластера Patroni убедитесь, что кластер PostgreSQL по умолчанию остановлен и отключен.
YML
Создаем файл описание сервиса patroni - /etc/patroni/patroni.yml
name: patroni1
#namespace: /db/
scope: edir_cluster
restapi:
listen: 0.0.0.0:8008
connect_address: patroni1:8008
authentication:
username: patroni
password: patroni
etcd:
hosts: patroni1:2379,patroni2:2379,patroni3:2379
username: root
password: bVeSXiUx
bootstrap:
dcs:
ttl: 100
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
wal_keep_segments: 5120
max_wal_senders: 5
max_replication_slots: 5
checkpoint_timeout: 30
initdb:
- auth-host: md5
- auth-local: peer
- encoding: UTF8
- data-checksums
- locale: ru_RU.UTF-8
pg_hba:
- host replication postgres ::1/128 md5
- host replication postgres 127.0.0.1/8 md5
- host replication postgres 172.29.11.197/24 md5
- host replication postgres 172.29.11.198/24 md5
- host replication postgres 172.29.11.199/24 md5
- host all all 0.0.0.0/0 md5
- host replication replicator samenet md5
- host replication all 127.0.0.1/32 md5
- host replication all ::1/128 md5
users:
admin:
password: admin
options:
- superuser
postgresql:
listen: 0.0.0.0:5432
connect_address: patroni1:5432
#conf_dir: /etc/postgresql/11.9/main
bin_dir: /usr/bin/
data_dir: /data/patroni
pgpass: /tmp/pgpass
authentication:
superuser:
username: postgres
password: N29b6R4HKO1I
replication:
username: replicator
password: DDqNmBRyjyik
rewind:
username: rewind_user
password: kI3Sowj9KHZz
parameters:
unix_socket_directories: '/var/lib/pgsql/'
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
Важно!
unix_socket_directories: ‘/var/lib/pgsql/’ нельзя ставить /var/run/postgresql, т.к. не хватит прав на создание каталога.
В параметре scope содержится имя кластера (postgres).
В приведённом примере настроек, есть ряд параметров, влияющих на выполнение переключения на резервный сервер.
- maximum_lag_on_failover - максимальное количество байт, на которые может отставать резервный сервер от ведущего, участвующий в выборах нового лидера.
- master_start_timeout - задержка в секундах, между обнаружением аварийной ситуации и началом отработки переключения на резервный сервер. По умолчанию 300 секунд. Если задано 0, то переключение начнётся немедленно.
При использовании асинхронной репликации (как в приведённом примере) это может привести к потере последних транзакций.
Максимальное время переключения на реплику равно “loop_wait” + “master_start_timeout” + “loop_wait”.
Если “master_start_timeout” установленно в 0, то это время становится равно значению параметра “loop_wait”.
Подробнее о возможных вариантах репликации можно прочитать в документации https://patroni.readthedocs.io/en/latest/replication_modes.html#replication-modes
Unit
Создаем юнит сервиса:
# cat /etc/systemd/system/patroni.service
[Unit]
Description=Runners to orchestrate a high-availability PostgreSQL
After=syslog.target network.target
[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=no
[Install]
WantedBy=multi-user.target
Master node bootstrap
Создаем нужные каталоги и запускаем сервис:
mkdir -p /etc/postgresql/11.9/main /var/run/postgresql/ /etc/patroni /data/patroni
chown -R postgres: /etc/postgresql /var/run/postgresql/ /etc/patroni /data/patroni
chmod 750 /data/patroni
systemctl enable patroni; systemctl start patroni
Смотрим статус порта 5432:
В каталоге /data/patroni должны появиться структура баз данных и настройки службы.
Состояние сервиса будет примерно таким:
patroni.service - Runners to orchestrate a high-availability PostgreSQL
Loaded: loaded (/etc/systemd/system/patroni.service; enabled; vendor preset: disabled)
Active: active (running) since Thu 2021-07-29 15:04:52 MSK; 21min ago
Main PID: 182875 (patroni)
Tasks: 13 (limit: 4707)
Memory: 77.6M
CGroup: /system.slice/patroni.service
├─182875 /usr/bin/python3 /usr/local/bin/patroni /etc/patroni/patroni.yml
├─182894 /usr/bin/postgres -D /data/patroni --config-file=/data/patroni/postgresql.conf --listen_addresses=0.0.0.0 --port=5432 --cluster_name=postgres --wal_level=replica --hot_standby=on --m>
├─182897 postgres: postgres: checkpointer
├─182898 postgres: postgres: background writer
├─182899 postgres: postgres: stats collector
├─182903 postgres: postgres: postgres postgres 127.0.0.1(59188) idle
├─182908 postgres: postgres: walwriter
├─182909 postgres: postgres: autovacuum launcher
└─182910 postgres: postgres: logical replication launcher
июл 29 15:24:54 dro-super-dev-db-master-cluster.super-dev.lan patroni[182875]: 2021-07-29 15:24:54,575 INFO: no action. I am (patroni1) the leader with the lock
Посмотрим список членов кластера:
patronictl -d etcd://localhost:2379 list postgres
+ Cluster: postgres (uninitialized) +-----------+
| Member | Host | Role | State | TL | Lag in MB |
+--------+------+------+-------+----+-----------+
| patroni1 | patroni1 | Leader | running | 4 | |
+--------+------+------+-------+----+-----------+
Add new cluster members
Для добавления нового узла в кластер Patroni, выполняются действия из раздела Master node bootstrap. Различия будут заключаться только в файле /etc/patroni/patroni.yml - все изменения заключаются в замене “patroni1” на имя текущего сервера.
После запуска Patroni на резервном сервере происходит следующее:
Patroni подключается к кластеру на мастер-ноде patroni1
Создаётся новый кластер PostgreSQL и заполняется данными с patroni1
Новый член кластера PostgreSQL переводится в “slave mode”
В результате в кластере Patroni должно быть три узла:
patronictl -d etcd://patroni1:2379 list edir_cluster
+ Cluster: edir_cluster (6990307902978054488) -+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+----------+----------+---------+---------+----+-----------+
| patroni1 | patroni1 | Leader | running | 4 | |
| patroni2 | patroni2 | Replica | running | 4 | 0 |
| patroni3 | patroni3 | Replica | running | 4 | 0 |
+----------+----------+---------+---------+----+-----------+
Чтобы использовать короткий синтаксис команды patronictl, лучше создать файл с настройками кластера для нужного пользователя: ~/.config/patroni/patronictl.yaml
dcs_api:
etcd://localhost:2379
#namespace: /db/
scope: edir_cluster
authentication:
username: patroni
password: patroni
Теперь можно смотреть список членов кластера краткой командой:
Пробное подключение
Теперь можно подключиться к базе данных, используя порт, на котором в настоящее время находится лидер вашего кластера:
Создание тестовой БД
От имени пользователя postgres запускаем командный процессор psql и в нем выполняем команды:
postgres=>
CREATE USER sani WITH PASSWORD 'Passw@rd';
CREATE DATABASE customers ENCODING 'UTF8' owner sani;
GRANT ALL PRIVILEGES ON DATABASE customers TO sani;
Далее создаем описание таблицы accounts:
cat >> accounts.sql << EOF
CREATE TABLE accounts (
user_id serial PRIMARY KEY,
username VARCHAR ( 50 ) UNIQUE NOT NULL,
password VARCHAR ( 50 ) NOT NULL,
email VARCHAR ( 255 ) UNIQUE NOT NULL,
created_on TIMESTAMP NOT NULL
);
EOF
Запускаем создание таблицы accounts в БД customers:
Keepalived
Демон keepalived используется службой haproxy для организации высокой доступности сервиса. Ставим на всех трех узлах кластера.
keepalived.conf
Настраиваем /etc/keepalived/keepalived.conf
global_defs {
router_id patroni1
}
vrrp_script haproxy {
script "killall -0 haproxy"
interval 2
weight 2
}
vrrp_instance ADMIN {
virtual_router_id 50
advert_int 1
priority 101
state MASTER
interface ens192
virtual_ipaddress {
172.29.11.200/24
}
track_script {
haproxy
}
}
Здесь необходимо заменить имя интерфейса (ens192) на свое реальное.
router_id patroni1 - будет, соответственно patroni2 - на втором узле, patroni3 - на третьем узле.
После того, как нак всех узлах будет настроен keepalived.conf, разрешаем и стартуем службу:
Haproxy
Ставим haproxy:
Разрешаем привязку нелокальных IP-адресов
haproxy.conf
Создаем файл конфигурации /etc/haproxy/haproxy.conf
global
log 127.0.0.1 local2
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user _haproxy
group _haproxy
daemon
maxconn 300
defaults
log global
mode tcp
retries 2
mode http
option http-server-close
timeout client 50s
timeout server 50s
timeout connect 8s
timeout check 5s
listen stats
mode http
bind *:7000
stats enable
stats uri /
frontend patroni_front
bind *:5433
default_backend patroni_backend
backend patroni_backend
option httpchk
http-check expect status 200
default-server inter 3s fall 3 rise 2
server patroni1 patroni1:5432 maxconn 100 check port 8008
server patroni2 patroni2:5432 maxconn 100 check port 8008
server patroni3 patroni3:5432 maxconn 100 check port 8008
В соответствии с этой конфигурацией haproxy слушает порт 5433, и отправляет трафик с него на мастер сервер.
Проверка статуса происходит с интервалом в 1 секунду, для перевода сервера в даун требуется 3 неудачных ответа (код 500), для переключения сервера назад -2 удачных ответа (с кодом 200). В любой момент времени можно обратиться непосредственно на любой haproxy, и он корректно запроксирует трафик на мастер сервер.
Также в комплекте с Patroni есть шаблон для настройки демона confd, и пример его интеграции с etcd, что позволяет динамически менять конфиг haproxy при удалении или добавлении новых серверов. Посмотреть можно на github
Корректируем права и запускаем:
Смотирим состояние и статистику кластера:
Cluster usage
После настройки high avalibility сервиса, мы будем подключаться к базе через виртуальный IP-адрес
В ответ на запрос пароля вводим пароль из файла /etc/patroni/patroni.yml: N29b6R4HKO1I
Добавим тестовую базу данных:
Переподключимся к новой БД:
Смена настроек кластера
Смена настроек делается синхронно на всех серверах кластера, где установлен patroni, корректировкой файла /etc/patroni/patroni.yml в секции “parameters:”
После смены запускаем скрипт:
for node in patroni1 patroni2 patroni3; do systemctl reload patroni $node; systemctl restart patroni $node; done
Выполнение запроса в БД
Найдем максимальный номер клиента:
patronictl query --format yaml -U sani --password -d customers edir_cluster -c "select max(user_id) as max_user_id from accounts"
- max_user_id: 14733
В данном запросе еще можно задать через ключ “-m” конкретный узел кластера, или через ключ “-r” - роль узла (master,replica,any).
Таким образом, для выполнения запроса на вставку данных необходим такой запрос:
patronictl query -U sani --password -d customers edir_cluster -r master -c "INSERT into accounts (user_id, username, password, email) VALUES ('14734', 'timofey', 'T1m0feY_pass', 'timofey@yahoo.com');"
Проверим:
customers=> select * from accounts where user_id in (select max(user_id) from accounts);
user_id | username | password | email | created_on
---------+----------+--------------+-------------------+----------------------------
14734 | timofey | T1m0feY_pass | timofey@yahoo.com | 2021-08-03 10:45:39.160541
(1 строка)
Тестовый скрипт
Скрипт заполняет случайными данными таблицу accounts в БД customers.
#!/bin/bash
## first (start) user_id
FID=$1
function usage() {
echo "Usage: "$0" start user_id"
}
if [[ $# -ne 1 ]]; then usage
else
for (( count=$FID; count<20000; count++ )); do
echo $count
echo "INSERT into accounts (user_id, username, password, email) VALUES ('$count', '$(openssl rand -base64 6)', '$(openssl rand -base64 15)', '$(openssl rand -base64 6)@yahoo.com');" | psql -U sani -h 172.29.11.200 -p 5433 customers
done
fi
Кроме того, это скрипт демонстрирует, что запись данных проходит “через два раза”, т.к. установленный нами механизм балансировки haproxy (roundrobin) направляет запрос на случайный узел. Когда запрос insert попадает на Leader (master) то он успешно отрабатывает, а когда на Replica (slave) - то запрос отвергается:
Таким образом бэкенд работы с кластером должен содержать какой-то дополнительный механизм, распределяющий запросы на чтение и запись соответственно ролям членов кластера.
Одним из простых способов может быть использование механизма балансировки по заголовкам - HDR.
В этом механизме сервер выбирается на основе заголовка HTTP запроса. Если искомое значение отсутствует в заголовке, то используется принцип Round Robin.
Например, при такой записи в /etc/haproxy/haproxy.cfg:
HAProxy будет искать в заголовке запроса запись User-Agent: Mozilla/5.0 и направлять его на заданный сервер.
В рекомендациях от Google Cloud (https://cloud.google.com/architecture/architectures-high-availability-postgresql-clusters-compute-engine#query_routing_2)
четко сказано, что pg_auto_failure не предоставляет никаких возможностей маршрутизации запросов на стороне сервера.
Вместо этого pg_auto_failure полагается на маршрутизацию запросов на стороне клиента, которая использует официальный драйвер клиента PostgreSQL libpq.
Когда вы определяете URI подключения, драйвер может принимать несколько хостов в своем пареметре host, разделенных запятой.
Подробнее можно посмотреть на сайте postgresql.org
Клиентская библиотека, которую использует ваше приложение, должна либо заключать в оболочку libpq, либо реализовывать возможность предоставления нескольких хостов для архитектуры для поддержки полностью автоматизированного аварийного переключения.
Схема компонентов отказоустойчивого кластера на базе ПО patroni:
Troubleshooting
localhost:5432 - не принимает подключения
Остановить службу patroni и удалить все содержимое каталога /data/patroni/.
После запуска службы все будет хорошо.
Sources
- Построение отказоустойчивого кластера PostgreSQL. Настройка внешней синхронизации на PostgreSQL для механизма копий баз данных
- Patroni - setting up a highly available postgresql cluster
- Управление высокодоступными PostgreSQL кластерами с помощью Patroni. А.Клюкин, А.Кукушкин
- Официальное руководство по patroni REST API
- Исходный код и документация
- Конфигурирование haproxy
- Доклад по внедрению Partoni в компании Miro на видеоконференции Siberia 2019
- Доклад Ильи Космодемьянского на тему “Зачем нужен мультимастер”
create: 09.08.2021 last update: 08.01.2024