Недавно мне посчастливилось заниматься переносом кластера PostgreSQL под управлением Patroni на новое железо. Задача казалась простой — я и не думал, что могут возникнуть проблемы.
Но в процессе реализации встретились некоторые сложности, которые натолкнули на мысль поделиться полученным опытом. В этой работе описываются практические шаги и нюансы, которые встретились во время переноса кластера на новую платформу. Использовались следующие версии ПО: PostgreSQL 11.13, Patroni 2.1.1, etcd 3.2.17 (API version 2).
Итак, поехали!
Введение
Patroni — это известный Open Source-проект для СУБД PostgreSQL от Zalando, написанный на Python и созданный для автоматизации построения кластеров высокой доступности. По своей сути его можно назвать своеобразным фреймворком. В основе Patroni стоит механизм потоковой репликации (streaming replication), работа которого строится на WAL (Write-Ahead Log). Когда мы вносим изменения в базу данных, все изменения сперва записываются в WAL. После записи в WAL СУБД производит системный вызов fsync
, в результате чего данные записываются на диск. Это обеспечивает возможность сохранения незавершенных операций, например, в случае аварийного завершения работы сервера. При включении сервера СУБД прочитает последние записи из WAL и применит к базе данных соответствующие изменения.
Потоковая репликация — это механизм, который реализует передачу записей из WAL от мастера к репликам. При такой конфигурации репликации право на запись есть только у мастера, а читать возможно как с мастера, так и с реплик (если разрешено). Для разрешения чтения с реплики она должна работать в режиме hot_standby
. Так как большинство запросов к СУБД — запросы на чтение, репликация позволяет масштабировать базу данных горизонтально. Потоковая репликация имеет два режима работы:
асинхронная — запросы выполняются на мастер-узле сразу же, в то время как изменения из WAL репликам передаются отдельно;
синхронная — данные записываются в WAL на мастере и как минимум на одной реплике. Только в этом случае транзакция считается выполненной. Конкретные условия устанавливаются в настройках PostgreSQL.
Для того, чтобы все члены кластера Patroni знали о состоянии друг друга, предусмотрено хранение данных в Distributed Configuration Store (DCS) — распределенном хранилище конфигурации. В качестве DCS могут использоваться различные хранилища типа key-value, например: Consul, etcd (v3), ZooKeeper. В контексте статьи мы будем рассматривать работу с etcd.
Постановка задачи. Исходное состояние инфраструктуры
Предоставляя клиентам обслуживание инфраструктуры под ключ, мы столкнулись с довольно типичной ситуацией: на сервере базы данных заканчивалось место, но работающий в ЦОДе сервер физически не позволял подключить дополнительный диск. Как быть в такой ситуации? Ответ довольно прост — мигрировать сервис с СУБД на новый сервер, предусмотрев возможность расширения. В результате было заказано 3 более мощных физических сервера для переезда.
Исходное состояние инфраструктуры:
кластер PostgreSQL на базе Patroni, состоящий из трех серверов и работающих в режиме асинхронной репликации;
кластер etcd (для хранения состояния Patroni), состоящий из трех инстансов (по одному на каждом из серверов кластера Patroni);
серая сеть внутри контура с PostgreSQL с адресацией 192.168.0.0/24;
load balancer, передающий трафик на master-узел кластера PgSQL.
Для удобства ниже я буду проводить все действия и рассматривать узкие места в конфигурации на тестовом стенде. Условная схема взаимодействия сервисов представлена ниже:
Между всеми узлами и load balancer’ом существует сетевая связанность, то есть каждый хост доступен для всех остальных по IP.
Для комфортного чтения конфигурации на каждом из узлов добавлены следующие записи в файл /etc/hosts
:
# etcd
192.168.0.16 server-1 etcd1
192.168.0.9 server-2 etcd2
192.168.0.12 server-3 etcd3
Состояние кластера Patroni:
Листинг файла конфигурации /etc/patroni.yaml
на узле server-1
:
scope: patroni_cluster
name: server-1
namespace: /patroni/
restapi:
listen: 192.168.0.16:8008 # IP-адрес узла и порт, на котором будет работать Patroni API
connect_address: 192.168.0.16:8008
authentication:
username: patroni
password: 'mysuperpassword'
etcd:
hosts: etcd1:2379,etcd2:2379,etcd3:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
synchronous_mode: false
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: hot_standby
synchronous_commit: off
hot_standby: "on"
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- local all postgres trust
- host postgres all 127.0.0.1/32 md5
- host replication replicator 0.0.0.0/0 md5
- host replication all 192.168.0.16/32 trust # server-1
- host replication all 192.168.0.9/32 trust # server-2
- host replication all 192.168.0.12/32 trust # server-3
- host all all 0.0.0.0/0 md5
users:
admin:
password: 'mysuperpassword2'
options:
- createrole
- createdb
postgresql:
listen: 192.168.0.16:5432 # IP-адрес интерфейса и порт, на которых будет слушать postgresql
connect_address: 192.168.0.16:5432
data_dir: /data/patroni
bin_dir: /usr/lib/postgresql/11/bin
config_dir: /data/patroni
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: 'mysuperpassword3'
superuser:
username: postgres
password: 'mysuperpassword4'
rewind:
username: rewind_user
password: 'mysuperpassword5'
parameters:
unix_socket_directories: '/tmp'
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
Состояние кластера etcd:
Листинг файла конфигурации /etc/default/etcd
на узле server-1
:
ETCD_LISTEN_PEER_URLS="http://127.0.0.1:2380,http://192.168.0.16:2380"
ETCD_LISTEN_CLIENT_URLS="http://127.0.0.1:2379,http://192.168.0.16:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://etcd1:2380"
ETCD_INITIAL_CLUSTER_STATE="new-cluster"
ETCD_INITIAL_CLUSTER_TOKEN="myclustertoken"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_ELECTION_TIMEOUT="5000"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_NAME="etcd1"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.0.16:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.0.16:2380"
Примечание: файл конфигурации /etc/default/etcd
используется для бутстрапа кластера etcd, т. е. параметры, описанные в нём, применяются в момент инициализации (первого запуска) процесса etcd. После того, как кластер инициализирован, конфигурация читается из рабочего каталога, заданного параметром ETCD_DATA_DIR
.
Наша задача — перенести данные с серверов #1, 2 и 3 на новые серверы без простоя в работе сервиса. Для достижения результата мы начнем постепенно расширять кластер PostgreSQL и кластер etcd, наделяя новые узлы небольшими отличиями от оригинальных узлов PgSQL:
Не будем добавлять endpoint’ы новых серверов в распределение трафика load balancer’ом.
Новые узлы PostgreSQL не будут принимать участие в выборе master-узла при failover.
В реальной задаче сетевая связность между узлами была организована через интернет, что было неприемлемо с точки зрения безопасности. Поэтому средствами ЦОДа был подготовлен VPN для достижения L2-связности между узлами. Не буду подробно описывать этот этап подготовки к переезду, и для удобства и упрощения схемы помещу новые серверы в сеть 192.168.0.0/24, чтобы обеспечить всем узлам связь в пределах broadcast-домена.
Таким образом, промежуточная схема взаимодействия узлов в кластере будет выглядеть следующим образом:
Реализация
План по реализации задуманного таков:
расширение etcd-кластера;
расширение PostgreSQL-кластера средствами Patroni;
вывод из PostgreSQL-кластера «старых» узлов;
вывод из etcd-кластера «старых» инстансов.
Итак, начнём!
Шаг №1. Расширяем кластер etcd
Важно! Так как etcd (или любое другое хранилище типа key-value) является фундаментальным компонентом в функционировании Patroni, лучше всего проектировать систему так, чтобы кластер работал на отдельных инстансах, имел свою собственную подсеть и не зависел от самих узлов с Patroni. Но в рамках рассматриваемого стенда я сэкономил и разместил кластер etcd на тех же серверах, что и PostgreSQL с Patroni.
Первым делом нужно расширить кластер etcd так, чтобы при переключении лидера всем узлам (и новым, и старым) был доступен новый лидер. Для этого сначала внесем изменения в конфигурационный файл /etc/hosts
на каждом из шести узлов и приведем его примерно к следующему виду (в сегменте ранее добавленных строк):
# etcd
192.168.0.16 server-1 etcd1
192.168.0.9 server-2 etcd2
192.168.0.12 server-3 etcd3
192.168.0.13 new_server-1 etcd-1 # IP-адрес первого нового сервера
192.168.0.17 new_server-2 etcd-2 # ... второго
192.168.0.18 new_server-3 etcd-3 # ... третьего
Далее начинаем по очереди добавлять инстансы в кластер etcd. На машине new_server-1
(192.168.0.13) проверим, что сервис etcd не запущен:
root@new_server-1:~# systemctl status etcd
● etcd.service - etcd - highly-available key value store
Loaded: loaded (/lib/systemd/system/etcd.service; disabled; vendor preset: enabled)
Active: inactive (dead)
Docs: https://github.com/coreos/etcd
man:etcd
На всякий случай очистим каталог /var/lib/etcd
, чтобы быть уверенными в том, что конфигурация будет получена из файла /etc/default/etcd
:
root@new_server-1:~# rm -rf /var/lib/etcd/*
Теперь приведём файл /etc/default/etcd
к следующему виду:
ETCD_LISTEN_PEER_URLS="http://192.168.0.13:2380,http://127.0.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.0.13:2379,http://127.0.0.1:2379"
ETCD_INITIAL_CLUSTER_TOKEN="myclustertoken"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_ELECTION_TIMEOUT="5000"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.0.13:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.0.13:2380"
Переходим на один из серверов etcd-кластера (например, server-1
) и смотрим список членов кластера:
root@server-1:~# etcdctl member list
862db4122a92dc3: name=etcd3 peerURLs=http://etcd3:2379 clientURLs=http://192.168.0.12:2379 isLeader=false
46d7a702fdb60fff: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://192.168.0.16:2379 isLeader=true
d129ecfd4c627e1f: name=etcd2 peerURLs=http://etcd2:2379 clientURLs=http://192.168.0.9:2379 isLeader=false
Добавляем нового члена кластера:
root@server-1:~# etcdctl member add etcd-1 http://etcd-1:2380
Added member named etcd-1 with ID 6d299012c6ad9595 to cluster
ETCD_NAME="etcd-1"
ETCD_INITIAL_CLUSTER="etcd1=http://etcd1:2380,etcd-1=http://etcd-1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
Etcd сообщил параметры, которые мы должны использовать в новом инстансе для подключения к существующему кластеру. Добавляем эти параметры в /etc/default/etcd
на сервере etcd-1
(new_server-1
). В конечном итоге получаем такой файл конфигурации (к изначальному конфигу добавились три последние строки):
ETCD_LISTEN_PEER_URLS="http://192.168.0.13:2380,http://127.0.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.0.13:2379,http://127.0.0.1:2379"
ETCD_INITIAL_CLUSTER_TOKEN="myclustertoken"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_ELECTION_TIMEOUT="5000"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.0.13:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.0.13:2380"
ETCD_NAME="etcd-1"
ETCD_INITIAL_CLUSTER="etcd1=http://etcd1:2380,etcd-1=http://etcd-1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
Проверяем на сервере состояние кластера etcd:
root@new_server-1:~# etcdctl member list
46d7a702fdb60fff: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://192.168.0.16:2379 isLeader=true
6d299012c6ad9595: name=etcd-1 peerURLs=http://etcd-1:2380 clientURLs=http://192.168.0.13:2379 isLeader=false
c32185ccfd4b4b41: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://192.168.0.9:2379 isLeader=false
d56f1524a8fe199e: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://192.168.0.12:2379 isLeader=false
Как мы видим, etcd-1
успешно добавлен в кластер. Повторяем все действия для следующего сервера, заранее подготовив для него шаблон конфигурации (/etc/default/etcd
), где потребуется поменять адреса сетевых интерфейсов в соответствии с адресом сервера. На выходе получаем:
root@new_server-2:~# etcdctl member list
40ebdfb25cac6924: name=etcd-2 peerURLs=http://etcd-2:2380 clientURLs=http://192.168.0.17:2379 isLeader=false
46d7a702fdb60fff: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://192.168.0.16:2379 isLeader=true
6d299012c6ad9595: name=etcd-1 peerURLs=http://etcd-1:2380 clientURLs=http://192.168.0.13:2379 isLeader=false
c32185ccfd4b4b41: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://192.168.0.9:2379 isLeader=false
d56f1524a8fe199e: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://192.168.0.12:2379 isLeader=false
… и такой файл конфигурации /etc/default/etcd
:
ETCD_LISTEN_PEER_URLS="http://192.168.0.17:2380,http://127.0.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.0.17:2379,http://127.0.0.1:2379"
ETCD_INITIAL_CLUSTER_TOKEN="myclustertoken"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_ELECTION_TIMEOUT="5000"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.0.17:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.0.17:2380"
ETCD_NAME="etcd-2"
ETCD_INITIAL_CLUSTER="etcd-2=http://etcd-2:2380,etcd1=http://etcd1:2380,etcd-1=http://etcd-1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
Повторяем процедуру для третьего сервера и проверяем результат:
root@new_server-3:~# etcdctl member list
40ebdfb25cac6924: name=etcd-2 peerURLs=http://etcd-2:2380 clientURLs=http://192.168.0.17:2379 isLeader=false
46d7a702fdb60fff: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://192.168.0.16:2379 isLeader=true
6c2e836d0c3a51c3: name=etcd-3 peerURLs=http://etcd-3:2380 clientURLs=http://192.168.0.18:2379 isLeader=false
6d299012c6ad9595: name=etcd-1 peerURLs=http://etcd-1:2380 clientURLs=http://192.168.0.13:2379 isLeader=false
c32185ccfd4b4b41: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://192.168.0.9:2379 isLeader=false
d56f1524a8fe199e: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://192.168.0.12:2379 isLeader=false
Файл конфигурации /etc/default/etcd
:
ETCD_LISTEN_PEER_URLS="http://192.168.0.18:2380,http://127.0.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.0.18:2379,http://127.0.0.1:2379"
ETCD_INITIAL_CLUSTER_TOKEN="myclustertoken"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_ELECTION_TIMEOUT="5000"
ETCD_HEARTBEAT_INTERVAL="1000"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.0.18:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.0.18:2380"
ETCD_NAME="etcd-3"
ETCD_INITIAL_CLUSTER="etcd-2=http://etcd-2:2380,etcd1=http://etcd1:2380,etcd-3=http://etcd-3:2380,etcd-1=http://etcd-1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
Мы расширили etcd-кластер с трех инстансов до шести.
Шаг №2. Расширяем кластер PostgreSQL
Следующим нашим шагом будет расширение кластера PgSQL. Так как кластер управляется Patroni, нужно подготовить файл конфигурации /etc/patroni.yaml
с примерно следующим содержанием:
scope: patroni_cluster
name: new_server-1
namespace: /patroni/
restapi:
listen: 192.168.0.13:8008
connect_address: 192.168.0.13:8008
authentication:
username: patroni
password: 'mynewpassword'
etcd:
hosts: etcd-1:2379,etcd-2:2379,etcd-3:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
synchronous_mode: false
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: hot_standby
synchronous_commit: off
hot_standby: "on"
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- local all postgres trust
- host postgres all 127.0.0.1/32 md5
- host replication replicator 0.0.0.0/0 md5
- host replication all 192.168.0.16/32 trust # server-1
- host replication all 192.168.0.9/32 trust # server-2
- host replication all 192.168.0.12/32 trust # server-3
- host all all 0.0.0.0/0 md5
users:
admin:
password: 'mynewpassword2'
options:
- createrole
- createdb
postgresql:
listen: 192.168.0.13:5432
connect_address: 192.168.0.13:5432
data_dir: /data/patroni
bin_dir: /usr/lib/postgresql/11/bin
config_dir: /data/patroni
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: 'mynewpassord3'
superuser:
username: postgres
password: 'mynewpassord4'
rewind:
username: rewind_user
password: 'mynewpassword5'
parameters:
unix_socket_directories: '/tmp'
tags:
nofailover: true
noloadbalance: true
clonefrom: false
nosync: false
Примечания:
Мы изменяем настройки etcd для Patroni на новых серверах (см. значение
hosts
в секцииetcd
), ограничивая endoint’ы только новыми серверами, так как в дальнейшем мы планируем выводит старые инстансы etcd из кластера. Если сейчас сервер обратится к инстансу etcd-1 для записи значения, а лидером будет, скажем, etcd2 (его endpoint мы явно не указываем в конфигурации Patroni), то etcd сам отдаст нужный endpoint лидера и, поскольку сетевая видимость между всеми членами кластера существует, работа системы не нарушится.Мы устанавливаем 2 тега в конфигурации:
nofailover: true
иnoloadbalance: true
. Пока не планируется добавлять новые серверы в качестве target для load balancer, поэтому явно запрещаем им участвовать в гонке за лидерство.На новых серверах должны быть правильно определены параметры
data_dir
иconfig_dir
. Желательно, чтобы эти параметры не отличались от оригинальных значений. Возможна ситуация, когда в файлеpostgresql.base.conf
кто-то явно указал пути к этим директориям, и в момент бутстрапа новой реплики эти параметры приедут на новый сервер.Важно убедиться, что файл конфигурации
pg_hba.conf
— одинаковый на всех узлах и содержит разрешающие правила для подключения как новых, так и старых серверов. Да, мы описываем эти правила вpatroni.yaml
, но они используются только на этапе бутстрапа кластера. После этого добрый кто-то может изменить его, а Patroni не будет приводить его в соответствие своему конфигу… Это очень важный момент, с которым я столкнулся при реализации переноса.
Проверяем, что каталог /data/patroni
— пустой и принадлежит пользователю postgres
. Если это не так, то очищаем и устанавливаем нужные права:
root@new_server-1:~# rm -rf /data/patroni/*
root@new_server-1:~# chown -R postgres:postgres /data/patroni
Стартуем Patroni и проверяем состояние кластера:
root@new_server-1:~# systemctl start patroni
root@new_server-1:~# patronictl -c /etc/patroni.yml list
Новая реплика — в состоянии running
. Отлично!
Обратите внимание! При запуске Patroni он читает информацию из etcd и, если обнаруживает, что уже есть работающий кластер (а это наш случай!), пытается провести бутстрап от лидера. При этом используется pg_basebackup. Если существующая база — большая, может потребоваться много времени для завершения этой операции. Например, в реальном кейсе, который дал начало этой статье, была база объёмом в 2,8 ТБ, и её бутстрап занимал около 10 часов на гигабитном канале.
Также важно понимать, что в период бутстрапа мы создадим дополнительную нагрузку на сетевой интерфейс, поэтому для добавления новой реплики в кластер рекомендуется выбирать время минимальной нагрузки на БД.
Дождавшись, когда новая реплика завершила бутстрап, мы можем поочередно повторить процедуру для оставшихся серверов. После завершающей итерации должен получиться следующий результат:
Важно! Каждый узел в кластере создает пассивную нагрузку на лидера, потому что по умолчанию подтягивает все изменения от него. Это значит, что если база испытывает большую сетевую нагрузку со стороны сервисов, то добавление сразу 3 реплик в кластер может сыграть злую шутку. Я наблюдал среднюю загрузку сетевого интерфейса лидера кластера ~500 Мбит/сек на исходящий трафик, когда в кластере было три члена. Добавление четвертого узла увеличило нагрузку, но явных пиков не было. Однако после добавления пятого узла ситуация изменилась: некоторые узлы начали отставать от лидера (параметр Lag in MB постоянно увеличивался). Причина проста: в этот момент нагрузка на сетевой интерфейс достигла максимума (1 Гбит/сек).
Решить эту проблему удалось, настроив каскадную репликацию, которая позволила бутстрапить новую реплику от уже существующей. Для реализации этого метода в конфигурации Patroni нужно установить на одну из существующих реплик специальный тег — clonefrom: true
, а перед запуском бутстрапа новой реплики в её конфигурационном файле установить тег: replicatefrom: <название_узла>
.
Если ваша production-база достаточно нагружена, после добавления новой реплики может быть полезным выключение одной из старых (эта операция рассматривается дальше — см. шаг №4) либо конфигурация каскадной репликации. Так будет поддерживаться общее количество реплик в стандартном количестве.
Отступление про балансировку
Так как у нас в схеме используется load balancer, то перед тем, как переходить к следующим шагам, стоит рассказать, каким образом он принимает решение, куда нужно отправить трафик.
Когда мы готовили конфигурацию Patroni, описывали следующий сегмент:
restapi:
listen: 192.168.0.13:8008
connect_address: 192.168.0.13:8008
authentication:
username: patroni
password: 'mynewpassword'
Здесь указано, на каком интерфейсе и на каком порту будет работать Patroni API. Через API можно определить, является ли на данный момент узел leader’ом или replica’ой. Зная это, мы можем настроить health check для балансера так, чтобы в момент переключения лидера балансер знал, на какой target нужно переключить трафик.
Например: на картинке выше видно, что узел с адресом 192.168.0.16 (server-1
) является лидером на данный момент. Отправим пару GET-запросов по следующим URL:
root@new_server-2:~# curl -I -X GET server-1:8008/leader
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.6.9
Date: Sun, 10 Oct 2021 11:40:38 GMT
Content-Type: application/json
root@new_server-2:~# curl -I -X GET server-1:8008/replica
HTTP/1.0 503 Service Unavailable
Server: BaseHTTP/0.6 Python/3.6.9
Date: Sun, 10 Oct 2021 11:37:57 GMT
Content-Type: application/json
Получили коды ответов 200 и 503 соответственно. Отправим ещё пару запросов в API сервера с Patroni, который не является лидером на данный момент:
root@new_server-2:~# curl -I -X GET new_server-1:8008/leader
HTTP/1.0 503 Service Unavailable
Server: BaseHTTP/0.6 Python/3.6.9
Date: Sun, 10 Oct 2021 11:41:14 GMT
Content-Type: application/json
root@new_server-2:~# curl -I -X GET new_server-1:8008/replica
HTTP/1.0 503 Service Unavailable
Server: BaseHTTP/0.6 Python/3.6.9
Date: Sun, 10 Oct 2021 11:41:17 GMT
Content-Type: application/json
В обоих случаях мы получили 503. Почему так? Потому мы использовали тег noloadbalance: true
. Изменим значение этого тега на false
на новых узлах и перезапустим Patroni:
А теперь попробуем ещё раз:
root@new_server-2:~# curl -I -X GET new_server-1:8008/leader
HTTP/1.0 503 Service Unavailable
Server: BaseHTTP/0.6 Python/3.6.9
Date: Sun, 10 Oct 2021 11:45:40 GMT
Content-Type: application/json
root@new_server-2:~# curl -I -X GET new_server-1:8008/replica
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.6.9
Date: Sun, 10 Oct 2021 11:45:44 GMT
Content-Type: application/json
Всё корректно. Настроив health check для load balancer’а через Patroni API, мы можем распределять трафик на мастер-узел (для запросов на запись) и на реплики (для запросов на чтение). Это очень удобно.
В нашем случае использовался load balancer от облачного провайдера, и заниматься какими-то особыми настройками (помимо health check) не пришлось. Но в общем случае для балансировки можно использовать HAproxy в режиме TCP. Тогда его примерный конфиг будет выглядеть так:
global
maxconn 100
defaults
log global
mode tcp
retries 2
timeout client 30m
timeout connect 4s
timeout server 30m
timeout check 5s
listen stats
mode http
bind *:7000
stats enable
stats uri /
listen leader
bind *:5000
option httpchk
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server server_1 192.168.0.16:5432 maxconn 100 check port 8008
server server_2 192.168.0.9:5432 maxconn 100 check port 8008
server server_3 192.168.0.12:5432 maxconn 100 check port 8008
Шаг №3. Донастраиваем кластер PostgreSQL
Вернемся к нашей реализации. Мы добавили новые реплики в кластер PostgreSQL. Теперь нужно разрешить всем членам кластера принимать участие в гонке за лидерство и добавить новые endoint’ы кластера в tagets у load balancer’а. Меняем значение тега nofailover
на false
и перезапускаем Patroni:
Добавляем в список targets для load balancer’а новые серверы и назначаем лидером сервер new_server-1
:
root@new_server-1:~# patronictl -c /etc/patroni.yml switchover
Master [server-1]:
Candidate ['new_server-1', 'new_server-2', 'new_server-3', 'server-2', 'server-3'] []: new_server-1
When should the switchover take place [now]: now
Current cluster topology
Are you sure you want to switchover cluster patroni_cluster, demoting current master server-1? [y/N]: y
Successfully switched over to "new_server-1"
Шаг №4. Выводим серверы из кластера Patroni
Выведем server-1
, server-2
и server-3
из кластера и уберем их из targets для load balancer’а — они своё отработали:
root@server-3:~# systemctl stop patroni
root@server-3:~# systemctl disable patroni
Removed /etc/systemd/system/multi-user.target.wants/patroni.service.
root@server-2:~# systemctl stop patroni
root@server-2:~# systemctl disable patroni
Removed /etc/systemd/system/multi-user.target.wants/patroni.service.
root@server-1:~# systemctl stop patroni
root@server-1:~# systemctl disable patroni
Removed /etc/systemd/system/multi-user.target.wants/patroni.service.
Проверим состояние кластера:
root@new_server-1:# patronictl -c /etc/patroni.yml list
Остались только новые серверы. Мы почти закончили!
Шаг №5. Приводим в порядок кластер etcd
Последний шаг — разбираем кластер etcd:
root@new_server-1:~# etcdctl member list
40ebdfb25cac6924: name=etcd-2 peerURLs=http://etcd-2:2380 clientURLs=http://192.168.0.17:2379 isLeader=false
46d7a702fdb60fff: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://192.168.0.16:2379 isLeader=true
6c2e836d0c3a51c3: name=etcd-3 peerURLs=http://etcd-3:2380 clientURLs=http://192.168.0.18:2379 isLeader=false
6d299012c6ad9595: name=etcd-1 peerURLs=http://etcd-1:2380 clientURLs=http://192.168.0.13:2379 isLeader=false
c32185ccfd4b4b41: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://192.168.0.9:2379 isLeader=false
d56f1524a8fe199e: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://192.168.0.12:2379 isLeader=false
root@new_server-1:~# etcdctl member remove d56f1524a8fe199e
Removed member d56f1524a8fe199e from cluster
root@new_server-1:~# etcdctl member remove c32185ccfd4b4b41
Removed member c32185ccfd4b4b41 from cluster
root@new_server-1:~# etcdctl member remove 46d7a702fdb60fff
Removed member 46d7a702fdb60fff from cluster
root@new_server-1:~# etcdctl member list
root@new_server-1:~# etcdctl member list
40ebdfb25cac6924: name=etcd-2 peerURLs=http://etcd-2:2380 clientURLs=http://192.168.0.17:2379 isLeader=true
6c2e836d0c3a51c3: name=etcd-3 peerURLs=http://etcd-3:2380 clientURLs=http://192.168.0.18:2379 isLeader=false
6d299012c6ad9595: name=etcd-1 peerURLs=http://etcd-1:2380 clientURLs=http://192.168.0.13:2379 isLeader=false
Всё! Переезд окончен! Итоговая схема взаимодействия выглядит следующим образом:
Заключение
В результате проведенных манипуляций удалось перевезти кластер PostgreSQL на базе Patroni на новое железо. И в целом весь процесс получился довольно предсказуемым, контролируемым — пожалуй, во многом это заслуга Patroni. Я постарался описать в статье все сложности и узкие моменты в конфигурации, с которыми столкнулся по ходу миграции. Надеюсь, что этот опыт будет кому-нибудь полезным.
Простой при переключении был минимальным: составил около 8 секунд и был обусловлен тем, что в момент переключения лидера health check нашего load balancer’а сделал три попытки (с интервалом в три секунды и таймаутом в две секунды) с целью убедиться, что leader действительно изменился. Сервис, обращающийся к базе данных, поддерживал переподключение, поэтому соединение было восстановлено автоматически. (А вообще, хорошим тоном при переездах является остановка всех подключений к отключаемому узлу.)
P.S.
Читайте также в нашем блоге:
«Обзор операторов PostgreSQL для Kubernetes»: часть 1 (наш опыт и выбор) и часть 2 (дополнения и итоговое сравнение);
«Мониторинг PostgreSQL. Расшифровка аудиочата Data Egret и Okmeter»;
«Postgres-вторник №5: PostgreSQL и Kubernetes. CI/CD. Автоматизация тестирования».
Комментарии (13)
Kazzman
16.10.2021 04:16Есть множество статей про Патрони, с разными dcs. Версии обновляются, что-то меняется. Суть остается.
Но если бы в вашей статье был подъем версии patroni/etcd/postgre, то это было бы супер. К сожалениию, моего профиля не достаточно чтобы это осознать, а вот появляющиеся плюшки в новых версиях бд я готов понимать, объяснять и потреблять. Научите поднимать версию postgre, пожалуйста. С простоем в 8 секунд.
n_bogdanov
16.10.2021 09:41+4К сожалению в текущих реалиях (с 11 на 13 или 14 postgres) это равносильно запуску еще одного кластера патрони. Алгоритм прост - делаем 2ой кластер, подымаем логическую репликацию, переключаемся на новый кластер после синхронизации.
Kazzman
16.10.2021 11:19Но, на сколько я понимаю, логическая репликация сейчас имеет ограничения (сходу не перечислю) и не гарантирует оригинальную очередность применения dml. Поэтому (с патрони) и используется чтение журнала. И получится ли одновременно настроить два вида репликаций - для поддержания кластера и для наливки нового. Опять же, из мира mssql . Делаем полный бакап, наливаем. Разностный, опять наливаем. Обрубаем клиентов, последняя разница и переключение клиентов на новый кластер. Либо вообще detach-attach базы.
Получается, что нужно делать pg_upgrade. А в 14 версии индексы поменялись, значит несовместимость и хардлинком не получится переехать?
n_bogdanov
16.10.2021 11:55+3Логическая репликация даёт консистентные таблицы, единственная проблема в том, что она не переносит значения последовательностей и их придётся восстановить в момент переключения. В итоге при схеме с логической репликацией мы получаем много подготовительной работы и мараторий на schema change, но переключение за 8 секунд.
Вариант с pg_upgrade потребует от администратора базы меньше времени на подготовку, однако придётся обновлять все индексы(при обновлении до 12 или 13 версии) и пересобирать toast таблицы(в случае обновления до 14) . На работающем проде это может быть больнее, нежели настройка логической реплики.
DmitryKoterov
17.10.2021 10:09Что ж так сложно-то все. Уже лет 10 я жду, когда появится утилита, которая требует вообще нулевого конфигурирования для PostgreSQL-кластера (типа zero config), но она никак не появляется.
Как это может работать: apt-get install super-postgres-cluster, дальше кладем на машину в ~postgresql/.ssh/ один и тот же id_rsa и authorized_keys на всех машинах кластера. Даем всем машинам hostname вида pgNN.xyz где NN - цифра. Это реально необходимый минимум, имея который, данная несуществующая утилита могла бы уже все сама настроить и поддерживать в живом состоянии (с автофэйловером и т.д.). Не нужен ни реестр хостов, ничего (потому что можно тупо взять текущий hostname и затем простучать все цифры от 00 до 99, пытаясь приконнектиться по ssh; если вышло, вот и сосед). Самый первый хост - мастер, остальные автоматически становятся репликами и автосинкаются с текущего мастера, без всяких настроек, и конфиги тоже с мастера стягивают. Даже пароли суперпользователя не нужны, потому что есть же ssh-доступ с любой машины на любую под юзером postgres. И распределенная БД (типа etcd) тоже может сама поставиться, запуститься и держаться в актуальном состоянии, без танцев с бубном с конфигами.
Добавил машину с указанной схемой именования - она автоналилась и стала репликой. Удалил машину - она удалилась из кластера. Положил файлик на нужный хост - он стал мастером, и все реплики на него автоматом переключились. Вообще ноль конфигурации и не нужна даже утилита командной строки.
n_bogdanov
17.10.2021 10:24+1Вам стоит посмотреть либо в сторону операторов postgres в kubernetes. Либо в сторону repmgr
DmitryKoterov
17.10.2021 10:44+1Да ну щаз, конечно. Там настроек везде и тонкостей огромное количество. Недавно настраивал как раз repmgr, и близко не похоже на автоматику. Ощущение, что авторы утилит не слышали о принципах DRY и convention over configuration просто. Везде гигантская избыточность.
И вообще, если есть доступ по ssh с каждой машины на каждую, никакой etcd тоже не нужен. Можно поверх ssh все реализовать. Ssh - это универсальная штука, и имея ее, не обязательно даже порты постгреса открывать на фаерволе и делать, чтобы все машины друг друга видели по 5432 - можно через ssh просто тупо делать, и будет работать, заодно и энкрипшен трафика еще бесплатно.
Интересно, я понятно излагаю, или впечатление «о чем это вообще, ерунда какая-то»? Раньше ведь и на ассемблере программировали, а про другие языки говорили «да ну хрень какая-то избыточная». И про смартфоны без хардварной клавиатуры тоже.
У постгреса совершенно железобетонный движок репликации и сторадж, которые не ломается просто никогда. Можно даже файлы базы копировать rsync-ом при наливке реплики, всяко быстрее чем через replication протокол (да еще и можно продолжать с прерванного места). Но он очень низкоуровневый, и все существующие утилиты тоже слишком низкоуровневые. Тут громадная пропасть: штука хорошая, а управление ей - кабина экскаватора с 25 избыточными ручками вместо одной кнопки «сделать хорошо». В этом суть.
n_bogdanov
17.10.2021 10:55Если вас не устраивают существующие решения, которые позволяют сделать то, что вы хотите - никто не мешает реализовать свое.
Ваш скептицизм по поводу repmgr или операторов крайне странный. Так как требуется либо создать ресурс из 20 строк либо конфигурационный файл в 5 строк и разложить ключи, как в случае repmgr.
n_bogdanov
17.10.2021 11:05Я в принципе могу расписать, если интересует, как упростить конфигурации, чтобы было легче.
shurup
18.10.2021 05:49+1Мне видится, что философия PostgreSQL с самого начала была в минимальном техдолге — в том, чтобы проектировать и делать всё с «самых низов» очень красиво и правильно. Благодаря этому они и достигли той самой железобетонности. В то же время это естественным образом увеличивало порог вхождения. Начинающим пользователям было сложно (что, впрочем, не помешало проекту собрать свою аудиторию…), и тут прослеживается некий такой исторический след, который мешает. Считать ли это техдолгом перед массовыми пользователями? :-)
С другой стороны, с тех пор многое изменилось — в том числе и подход к инфраструктуре. Операторы для K8s, которые здесь предлагает Коля (и по которым он делал обзор), — это как раз новый, сегодняшний взгляд на проблему (и вариант её решения).
niklep
После этой фразы я до конца статьи ждал проблему. Но не дождался. Интересно, спасибо.
Kylin
думаю под проблемой подразумевалось что канал забился до 1 гигабита и что надо каскадировать реплики.
статья классная, спасибо.
AlexGluck
Та же мысль, а про канал я и не подумал. Я бы тоже попался на этом этапе.