Пару недель назад (перевод статьи от 2021 г.) мы с Джобином (Jobin) сделали небольшую презентацию в рамках Percona Live Online с названием, подобным теме этой статьи: "PostgreSQL HA с Patroni: Сценарии отказов и способы восстановления кластера после них". Мы развернули 3-узловую среду PostgreSQL на имеющемся у нас оборудовании и решили "ломать" ее различными способами: отключали от интернета и электропитания кабели, “убивали” основные процессы, пытаясь насытить процессоры. Всё это происходило при постоянной записи и чтении данных из PostgreSQL. Идея заключалась в том, чтобы посмотреть, как Patroni справится с отказами и будет управлять кластером, продолжая обеспечивать работу. Это была интересная демонстрация!
Мы обещали в следующей статье рассказать о том, как мы настраивали среду, чтобы и вы могли попробовать сами. Вот и она!. Мы надеемся, что вам будет интересно попробовать повторить наш небольшой эксперимент, и, что еще более важно, использовать эту возможность, чтобы понять на практике, как работает среда высокой доступности PostgreSQL, управляемая Patroni. Ведь нет ничего лучше, чем собственные практические эксперименты!
Начальная настройка
Для проведения эксперимента мы использовали три мини-компьютера Intel Atom 10-летней давности, но вместо этого вы можете использовать виртуальные машины: конечно у вас не будет таких же ощущений, как при отключении реальных кабелей, но это всё равно можно смоделировать с помощью ВМ. Мы установили серверную версию Ubuntu 20.04 и настроили их на взаимное распознавание по хост-имени; вот как выглядел файл hosts первого узла:
$ cat /etc/hosts
127.0.0.1 localhost node1
192.168.1.11 node1
192.168.1.12 node2
192.168.1.13 node3
etcd
Patroni поддерживает множество систем для хранения конфигурации дистрибутива, но наиболее популярным остается etcd. На всех трех узлах мы установили версию, доступную из репозитория Ubuntu:
sudo apt-get install etcd
Необходимо инициализировать кластер etcd с одного из узлов, что и было сделано с узла 1 при помощи следующего файла конфигурации:
$ cat /etc/default/etcd
ETCD_NAME=node1
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.11:2380"
ETCD_INITIAL_CLUSTER_TOKEN="devops_token"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.11:2380"
ETCD_DATA_DIR="/var/lib/etcd/postgresql"
ETCD_LISTEN_PEER_URLS="http://192.168.1.11:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.11:2379,http://localhost:2379"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.11:2379"
Обратите внимание на то, что ETCD_INITIAL_CLUSTER_STATE
определено со значением "new".
Затем мы перезапустили сервис:
sudo systemctl restart etcd
После этого можно переходить к установке etcd на узел 2. Файл конфигурации имеет ту же структуру, что и для узла 1, за исключением того, что мы добавляем узел 2 к существующему кластеру. Поэтому нам следует указать другой узел (узлы):
ETCD_NAME=node2
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.11:2380,node2=http://192.168.1.12:2380"
ETCD_INITIAL_CLUSTER_TOKEN="devops_token"
ETCD_INITIAL_CLUSTER_STATE="existing"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.12:2380"
ETCD_DATA_DIR="/var/lib/etcd/postgresql"
ETCD_LISTEN_PEER_URLS="http://192.168.1.12:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.12:2379,http://localhost:2379"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.12:2379"
Перед перезапуском сервиса необходимо формально добавить узел 2 в кластер etcd, выполнив следующую команду на узле 1:
sudo etcdctl member add node2 http://192.168.1.12:2380
После этого мы можем перезапустить сервис etcd на узле 2:
sudo systemctl restart etcd
Файл конфигурации для узла 3 выглядит следующим образом:
ETCD_NAME=node3
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.11:2380,node2=http://192.168.1.12:2380,node3=http://192.168.1.13:2380"
ETCD_INITIAL_CLUSTER_TOKEN="devops_token"
ETCD_INITIAL_CLUSTER_STATE="existing"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.13:2380"
ETCD_DATA_DIR="/var/lib/etcd/postgresql"
ETCD_LISTEN_PEER_URLS="http://192.168.1.13:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.13:2379,http://localhost:2379"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.13:2379"
Помните, что нам необходимо добавить узел 3 в кластер, выполнив следующую команду на узле 1:
sudo etcdctl member add node3 http://192.168.1.13:2380
прежде чем мы сможем перезапустить сервис на узле 3:
sudo systemctl restart etcd
Мы можем проверить состояние кластера, чтобы удостовериться в том, что он успешно развернут, выполнив следующую команду с любого из узлов:
$ sudo etcdctl member list
2ed43136d81039b4: name=node3 peerURLs=http://192.168.1.13:2380 clientURLs=http://192.168.1.13:2379 isLeader=false
d571a1ada5a5afcf: name=node1 peerURLs=http://192.168.1.11:2380 clientURLs=http://192.168.1.11:2379 isLeader=true
ecec6c549ebb23bc: name=node2 peerURLs=http://192.168.1.12:2380 clientURLs=http://192.168.1.12:2379 isLeader=false
Как это видно выше, на данный момент лидером является узел 1, что вполне ожидаемо, поскольку бутстрап кластера etcd был сделан с него. Если вы получили другой результат, проверьте наличие записей etcd в журнале /var/log/syslog
по каждому узлу.
Watchdog (сторожевой таймер)
Цитирую мануал от Patroni:
Сторожевые таймеры - программные или аппаратные механизмы, которые выполняют перезагрузку всей системы, если в течение заданного времени они не получают сердцебиение (heartbeat) keepalive. Это добавляет дополнительный уровень безопасности на случай, когда обычные механизмы защиты сплит-брейна Patroni не сработают.
Хотя применение механизма сторожевого таймера в Patroni является необязательным, не рекомендуется осуществлять деплой среды PostgreSQL HA в продакшне без него.
Для наших тестов мы использовали стандартную программную реализацию сторожевого таймера, поставляемую с Ubuntu 20.04, - модуль под названием softdog. Вот процедура, которую мы использовали на всех трех узлах при настройке загрузки модуля:
sudo sh -c 'echo "softdog" >> /etc/modules'
Компонентом, взаимодействующим со сторожевым устройством, будет Patroni. Поскольку Patroni запускается пользователем postgres, нам необходимо установить права доступа к устройству watchdog. Это позволяет пользователю postgres производить запись на него, либо сделать устройство собственностью самого postgres, что представляется нам более безопасным подходом (поскольку он является более жестким):
sudo sh -c 'echo "KERNEL=="watchdog", OWNER="postgres", GROUP="postgres"" >> /etc/udev/rules.d/61-watchdog.rules'
На первый взгляд, эти два шага - все, что нужно для работы watchdog, но, к нашему удивлению, после перезапуска серверов модуль softdog не загрузился. Потратив немало времени на выяснение обстоятельств, мы пришли к выводу, что модуль по умолчанию находится в черном списке, а strain-файл с такой директивой все еще существует:
$ grep blacklist /lib/modprobe.d/* /etc/modprobe.d/* |grep softdog
/lib/modprobe.d/blacklist_linux_5.4.0-72-generic.conf:blacklist softdog
Редактирование этого файла на каждом из узлов с удалением вышеуказанной строки и перезапуск серверов привели к положительному результату:
$ lsmod | grep softdog
softdog 16384 0
$ ls -l /dev/watchdog*
crw-rw---- 1 postgres postgres 10, 130 May 21 21:30 /dev/watchdog
crw------- 1 root root 245, 0 May 21 21:30 /dev/watchdog0
PostgreSQL
Дистрибутив Percona для PostgreSQL может быть легко установлен из репозитория Percona Repository с помощью нескольких простых шагов:
sudo apt-get update -y; sudo apt-get install -y wget gnupg2 lsb-release curl
wget https://repo.percona.com/apt/percona-release_latest.generic_all.deb
sudo dpkg -i percona-release_latest.generic_all.deb
sudo apt-get update
sudo percona-release setup ppg-12
sudo apt-get install percona-postgresql-12
Для среды с высокой доступностью такой как PostgreSQL, важно понимать, что PostgreSQL не следует автоматически запускать systemd
во время инициализации сервера: мы должны полностью доверить эту задачу Patroni, включая процесс запуска и остановки сервера. Таким образом, нам следует отключить данный сервис:
sudo systemctl disable postgresql
Для тестирования мы хотим начать с новой установки PostgreSQL и позволить Patroni осуществить бутстрап кластера. Поэтому мы останавливаем сервер и удаляем каталог данных, созданный в процессе инсталляции PostgreSQL:
sudo systemctl stop postgresql
sudo rm -fr /var/lib/postgresql/12/main
Данные шаги следует повторить и в узлах 2 и 3.
Patroni
Репозиторий Percona также включает пакет для Patroni. Поэтому, настроив его на узлах, мы можем осуществить инсталляцию Patroni достаточно просто:
sudo apt-get install percona-patroni
Вот файл конфигурации, который мы использовали для узла 1:
$ cat /etc/patroni/config.yml
scope: stampede
name: node1
restapi:
listen: 0.0.0.0:8008
connect_address: node1:8008
etcd:
host: node1:2379
bootstrap:
# this section will be written into Etcd:/<namespace>/<scope>/config after initializing new cluster
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
# master_start_timeout: 300
# synchronous_mode: false
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
logging_collector: 'on'
max_wal_senders: 5
max_replication_slots: 5
wal_log_hints: "on"
#archive_mode: "on"
#archive_timeout: 600
#archive_command: "cp -f %p /home/postgres/archived/%f"
#recovery_conf:
#restore_command: cp /home/postgres/archived/%f %p
# some desired options for 'initdb'
initdb: # Note: It needs to be a list (some options need values, others are switches)
- encoding: UTF8
- data-checksums
pg_hba: # Add following lines to pg_hba.conf after running 'initdb'
- host replication replicator 192.168.1.1/24 md5
- host replication replicator 127.0.0.1/32 trust
- host all all 192.168.1.1/24 md5
- host all all 0.0.0.0/0 md5
# - hostssl all all 0.0.0.0/0 md5
# Additional script to be launched after initial cluster creation (will be passed the connection URL as parameter)
# post_init: /usr/local/bin/setup_cluster.sh
# Some additional users users which needs to be created after initializing new cluster
users:
admin:
password: admin
options:
- createrole
- createdb
postgresql:
listen: 0.0.0.0:5432
connect_address: node1:5432
data_dir: "/var/lib/postgresql/12/main"
bin_dir: "/usr/lib/postgresql/12/bin"
# config_dir:
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: vagrant
superuser:
username: postgres
password: vagrant
parameters:
unix_socket_directories: '/var/run/postgresql'
watchdog:
mode: required # Allowed values: off, automatic, required
device: /dev/watchdog
safety_margin: 5
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
После того как файл конфигурации создан, а кластер etcd уже запущен, остается только перезапустить сервис Patroni:
sudo systemctl restart patroni
Когда Patroni запускается, он самостоятельно инициализирует PostgreSQL (поскольку сервис в данный момент еще не функционирует и каталог данных пуст), следуя директивам секции bootstrap конфигурационного файла Patroni. Если все прошло согласно плану, то у вы сможете подключиться к PostgreSQL, используя учетные данные из файла конфигурации (пароль - vagrant):
$ psql -U postgres
psql (12.6 (Ubuntu 2:12.6-2.focal))
Type "help" for help.
postgres=#
Повторите операцию установки Patroni на узлы 2 и 3 с той лишь разницей, что в конфигурационном файле необходимо заменить ссылки на node1 (их четыре, выделены жирным шрифтом) на имя соответствующего узла.
Проверить состояние только что созданного кластера Patroni можно также с помощью:
$ sudo patronictl -c /etc/patroni/config.yml list
+----------+--------+-------+--------+---------+----+-----------+
| Cluster | Member | Host | Role | State | TL | Lag in MB |
+----------+--------+-------+--------+---------+----+-----------+
| stampede | node1 | node1 | Leader | running | 2 | |
| stampede | node2 | node2 | | running | 2 | 0 |
| stampede | node3 | node3 | | running | 2 | 0 |
+----------+--------+-------+--------+---------+----+-----------+
Узел 1 запустил кластер Patroni, поэтому он автоматически стал лидером и, соответственно, primary (главным)/мастер сервером PostgreSQL. Узлы 2 и 3 сконфигурированы как реплики чтения (поскольку в конфигурационном файле Patroni была включена опция hot_standby
).
HAProxy
Распространенной реализацией принципа высокой доступности (HA - high availability) в среде PostgreSQL является использование прокси-сервера: вместо прямого подключения к серверу базы данных приложение подключается к прокси-серверу, который перенаправляет запрос к PostgreSQL. При использовании HAproxy можно также направлять запросы на чтение в одну или несколько реплик для балансировки нагрузки. Однако данный процесс не является прозрачным: приложение должно это учитывать и самостоятельно разделять трафик на чтение и запись. С HAproxy это достигается путем предоставления приложению двух разных портов для подключения. Мы выбрали следующую конфигурацию:
Записи (Writes) → 5000
Чтения (Reads) → 5001
HAproxy может быть установлен в качестве независимого сервера (и их может быть сколько угодно). Также его можно инсталлировать на сервер приложений или на сам сервер базы данных - он представляет собой достаточно легкий сервис. В наших тестах мы планировали использовать собственные рабочие станции под управлением Linux (на которых также установлена Ubuntu 20.04) для имитации трафика приложений. Поэтому HAproxy мы установили на них:
sudo apt-get install haproxy
Установив программу, мы изменили основной файл конфигурации следующим образом:
$ cat /etc/haproxy/haproxy.cfg
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 primary
bind *:5000
option httpchk OPTIONS /master
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server node1 node1:5432 maxconn 100 check port 8008
server node2 node2:5432 maxconn 100 check port 8008
server node3 node3:5432 maxconn 100 check port 8008
listen standbys
balance roundrobin
bind *:5001
option httpchk OPTIONS /replica
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server node1 node1:5432 maxconn 100 check port 8008
server node2 node2:5432 maxconn 100 check port 8008
server node3 node3:5432 maxconn 100 check port 8008
Обратите внимание, что есть две секции: primary (основная), использующая порт 5000, и standbys (резервная), использующая порт 5001. Все три узла включены в обе секции: это связано с тем, что они являются потенциальными кандидатами на роль основного или дублирующего узла. Для того чтобы HAproxy узнал, какова роль каждого узла в данный момент, он посылает HTTP-запрос на порт 8008 узла: Patroni ответит на него. Patroni предоставляет встроенную поддержку REST API для мониторинга проверки работоспособности, которая идеально интегрируется для этого с HAproxy:
$ curl -s http://node1:8008
{"state": "running", "postmaster_start_time": "2021-05-24 14:50:11.707 UTC", "role": "master", "server_version": 120006, "cluster_unlocked": false, "xlog": {"location": 25615248}, "timeline": 1, "database_system_identifier": "6965869170583425899", "patroni": {"version": "1.6.4", "scope": "stampede"}}
Мы настроили группу standbys для балансировки запросов на чтение при помощи алгоритма round-robin, поэтому каждый запрос на подключение (или переподключение) будет чередоваться между доступными репликами. Мы сможем проверить это на практике, для чего сохраним пароль пользователя postgres в файле:
echo "localhost:5000:postgres:postgres:vagrant" > ~/.pgpass
echo "localhost:5001:postgres:postgres:vagrant" >> ~/.pgpass
chmod 0600 ~/.pgpass
Затем мы можем выполнить два запроса на чтение, чтобы убедиться, что механизм round-robin работает как положено:
$ psql -Upostgres -hlocalhost -p<strong>5001</strong> -t -c "select inet_server_addr()"
192.168.1.13
$ psql -Upostgres -hlocalhost -p<strong>5001</strong> -t -c "select inet_server_addr()"
192.168.1.12
а также протестировать доступ к порту writer:
$ psql -Upostgres -hlocalhost -p<strong>5000</strong> -t -c "select inet_server_addr()"
192.168.1.11
Вы также можете проверить состояние HAproxy на своем браузере по адресу http://localhost:7000/.
Тестирование настроек
Для воссоздания реальной производственной среды и проверки реакции нашей системы на сбои, мы хотели поддерживать постоянный поток запросов на чтение и запись данных в базу данных. Вместо использования инструментов для бенчмаркинга, таких как Sysbench или Pgbench, нашей основной целью было наблюдать, как плавно происходит переключение источника данных при сбое сервера, а не просто создавать большую нагрузку. Для достижения этой цели Джобин создал простой скрипт на Python под названием HAtester. Этот сценарий идеально подходил для наших потребностей. Как и с HAproxy, мы запускали данный сценарий с нашей рабочей станции Linux. Важно отметить, что поскольку HAtester - это Python-скрипт, для успешного выполнения требуется установленный драйвер PostgreSQL для Python.
sudo apt-get install python3-psycopg2
curl -LO https://raw.githubusercontent.com/jobinau/pgscripts/main/patroni/HAtester.py
chmod +x HAtester.py
Измените скрипт, указав учетные данные для доступа к серверам PostgreSQL (через HAproxy), если вы используете другие настройки, отличные от наших. Единственное требование для его работы - предварительно создать целевую таблицу, поэтому сначала подключитесь к базе данных postgres (если вы не используете другую целевую базу) на основном сервере и выполните следующее:
CREATE TABLE HATEST (TM TIMESTAMP);
Затем можно запустить два разных сеанса:
Один для записей:
./HAtester.py 5000
Один для чтений:
./HAtester.py 5001
Идея заключается в том, чтобы наблюдать за тем, что происходит с трафиком базы данных, когда в среде происходит сбой, т.е. как HAproxy будет маршрутизировать чтения и записи, пока Patroni настраивает кластер PostgreSQL. Вы можете непрерывно мониторить Patroni с точки зрения узлов, открыв сессию на каждом из них и выполнив следующую команду:
sudo -u postgres watch patronictl -c /etc/patroni/config.yml list
Чтобы упростить наблюдаемость и лучше отслеживать изменения в реальном времени, мы использовали терминальный мультиплексор Tmux для визуализации всех 5 сессий на одном экране:
Слева для каждого из 3 узлов открыто по одному сеансу, который непрерывно выполняется:
sudo -u postgres watch patronictl -c /etc/patroni/config.yml list
Лучше иметь представление Patroni для каждого узла отдельно, поскольку при запуске тестов на отказ можно потерять связь с частью кластера.
С правой стороны - мы выполняем скрипт HAtester.py с нашей рабочей станции:
Отправляем записи через порт 5000:
./HAtester.py 5000
и операции чтения через порт 5001:
./HAtester.py 5001
Несколько замечаний по выполнению скрипта HAtester.py
:
Нажатие клавиш Ctrl+C приведет к разрыву соединения, но скрипт подключится заново, на этот раз к другой реплике (в случае операции чтения), поскольку группа Standbys в HAproxy была настроена с балансировкой при помощи алгоритма round-robin.
При переключении или если происходит отказ и перераспределение узлов в кластере временно могут наблюдаться случаи, когда запись отправляется на узел, который ранее был репликой и только что был переведен в разряд основных, а чтение - на узел, который ранее был основным и был переведен во вторичные: это ограничение скрипта HAtester.py, но "по замыслу"; в демонстрационных целях мы предпочли более быстрое переподключение и минимальную проверку роли узла. В продакшн-приложении эта часть должна быть реализована иначе.
Тестирование сценариев отказов
Теперь начинается самое интересное! Мы предоставляем вам возможность самостоятельно провести тестирование и посмотреть, что происходит с кластером PostgreSQL на практике при возникновении сбоя. В качестве предложений мы оставляем тесты, которые были проведены в нашей презентации. Для каждого сценария отказа наблюдайте как кластер автоматически перестраивается и каким образом это влияет на трафик чтения и записи.
1) Отсутствие соединения с сетью
Отключите сетевой кабель от одного из узлов (или смоделируйте это условие в своей виртуальной машине):
Сначала от реплики;
Затем от основного узла.
Отключите сетевой кабель от одной реплики и основного узла одновременно:
Возникает ли в Patroni ситуация сплит-брейна?
2) Отключение питания
Отключите кабель питания от основного узла;
Подождите, пока не будет произведена перенастройка кластера, затем снова подключите кабель питания и запустите узел.
3) Симуляция SEGFAULT
Для имитации ситуации сбоя вы можете вызвать ошибку сегментации (SEGFAULT), выполнив следующие действия:,
Найдите процесс, отвечающий за управление базой данных PostgreSQL, часто называемый "postmaster".
Используйте команду "kill" с параметром -9. Откройте терминал и выполните команду: kill -9 <process_id>. Замените <process_id> на фактический идентификатор процесса - postmaster. Это приведет к принудительному завершению процесса и созданию сценария SEGFAULT.
*Имейте в виду, что данное действие предназначено только для имитации. Будьте осторожны, так как это может вызвать потерю данных или их повреждение, если его применить на рабочем окружении.
4) Прекращение работы Patroni
Помните, что Patroni управляет PostgreSQL. Что произойдет, если процесс Patroni (а не PostgreSQL) будет завершен?
5) Насыщение процессора
Симулировать насыщение процессора можно с помощью инструмента для бенчмаркинга, например, Sysbench:
sysbench cpu --threads=10 --time=0 run
Данная задача достаточно сложна. При работе с интенсивной загрузкой ЦП и последовательными операциями чтения/записи с использованием одного потока следуйте этим шагам:
Снизьте приоритет процессов HAtester.py с помощью утилиты renice.
Усильте Sysbench. Рассмотрите возможность повышения приоритета процессов Sysbench.
Балансировка приоритетов помогает эффективно управлять загрузкой ЦП.
6) Ручное переключение
Patroni облегчает процесс изменений в иерархии PostgreSQL. Операции переключения могут быть запланированы, команда, приведенная ниже, является интерактивной и подскажет вам возможные опции:
sudo -u postgres patronictl -c /etc/patroni/config.yml switchover
В качестве альтернативы можете конкретно указать Patroni, что именно нужно сделать:
sudo -u postgres patronictl -c /etc/patroni/config.yml switchover --master node1 --candidate node2 --force
В завершение хочу пригласить вас на бесплатный урок курса PostgreSQL, по теме: "БД + внешние источники или как устроены Postgres Foreign Data Wrappers".