Настройка кластера высокой доступности: PostgreSQL + (Patroni и etcd)

Хабр, привет!

В этом материале будем настраивать кластер PostgreSQL с Patroni и etcd. Видели множество статей на эту тему, но наше отличие в том, что мы устанавливаем кластер в виртуальной среде, используя новые компоненты.

Немного теории. Patroni — это инструмент для управления высокодоступными кластерами PostgreSQL. Он упрощает настройку и управление репликацией благодаря автоматическому переключению на резервные узлы и восстановлению после сбоев.

В нашем материале мы рассмотрим настройку такого кластера с использованием etcd для координации, а еще будем использовать только пакеты для ручной установки. Потому что частенько в локальных репозиториях преобладают старые пакеты, в которых есть уязвимости. В таких случаях лучше устанавливать пакеты вручную.

Зачем мы это делаем?

Во-первых, интересно. Во-вторых, это нам позволит установить последние версии пакетов без открытого доступа в интернет с серверов. Во многих компаниях изолированная сетевая среда — поэтому вот вам памятка по такой задаче.:)

Итак, приступим.

ea7c0722592fba5c35e954e0306818b6.png

Систему можно использовать любую, автору захотелось Centos 8 (личное предпочтение автора, не более — прим. ред.). Совет: лучше заранее описать, какие будут имя сервера и IP-адреса. Сетевая структура у нас такая:

etcd1 192.168.60.141
etcd2 192.168.60.142
etcd3 192.168.60.143
node1 192.168.60.131
node2 192.168.60.132

У вас могут быть и другие адреса, поэтому не забудьте их поменять в конфигах. 

Установка и настройка etcd

Шаг 1: скачиваем etcd

Для этого качаем пакет с оф. гита etcd вот отсюда:  https://github.com/etcd-io/etcd/releases/download/v3.5.15/etcd-v3.5.15-linux-amd64.tar.gz

Затем распаковываем архив и перемещаем файлы:

tar -xzvf etcd-v3.5.15-linux-amd64.tar.gz
sudo mv etcd-v3.5.15-linux-amd64/etcd* /usr/bin/

Шаг 2: создаем пользователей и директории

1. Выполняем следующие команды:

sudo groupadd --system etcd
sudo useradd -s /sbin/nologin --system -g etcd etcd

2. Теперь создаем необходимые директории и устанавливаем права доступа с помощью:

sudo mkdir -p /var/lib/etcd /etc/default/etcd/.tls
sudo chown -R etcd:etcd /var/lib/etcd /etc/default/etcd
```

Шаг 3: генерируем сертификаты

Если у вас нет возможности использовать собственные сертификаты, то берем самописные. Небольшой скрипт ниже в этом нам поможет:

Скрытый текст

#!/bin/bash
 
# Директория для сертификатов:
CERT_DIR="/etc/default/etcd/.tls"
mkdir -p ${CERT_DIR}
cd ${CERT_DIR}
 
# Создание CA-сертификата:
openssl genrsa -out ca.key 4096
openssl req -x509 -new -key ca.key -days 10000 -out ca.crt -subj "/C=RU/ST=Moscow Region/L=Moscow/O=MyOrg/OU=MyUnit/CN=myorg.com"
 
# Функция для генерации сертификатов для нод:
generate_cert() {
    NODE_NAME=$1
    NODE_IP=$2
 
    cat < ${CERT_DIR}/${NODE_NAME}.san.conf
[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name
req_extensions     = req_ext
[ req_distinguished_name ]
countryName                 = RU
stateOrProvinceName         = Moscow Region
localityName                = Moscow
organizationName            = MyOrg
commonName                  = ${NODE_NAME}
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1   = ${NODE_NAME}
IP.1    = ${NODE_IP}
IP.2    = 127.0.0.1
EOF
 
openssl genrsa -out ${NODE_NAME}.key 4096
openssl req -config ${NODE_NAME}.san.conf -new -key ${NODE_NAME}.key -out ${NODE_NAME}.csr -subj "/C=RU/ST=Moscow Region/L=Moscow/O=MyOrg/CN=${NODE_NAME}"
    openssl x509 -extfile ${NODE_NAME}.san.conf -extensions req_ext -req -in ${NODE_NAME}.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out ${NODE_NAME}.crt -days 10000
 
    rm -f ${NODE_NAME}.san.conf ${NODE_NAME}.csr
}
 
# Список нод etcd и их IP-адресов:
ETCD_NODES=("etcd01" "etcd02" "etcd03")
ETCD_IPS=("192.168.60.141" "192.168.60.142" "192.168.60.143")
 
# Список нод Patroni и их IP-адресов:
PATRONI_NODES=("node01" "node02")
PATRONI_IPS=("192.168.60.131" "192.168.60.132")
 
 
# Генерация сертификатов для нод etcd:
for i in "${!ETCD_NODES[@]}"; do
generate_cert "${ETCD_NODES[$i]}" "${ETCD_IPS[$i]}"
done
 
# Генерация сертификатов для нод Patroni:
for i in "${!PATRONI_NODES[@]}"; do
generate_cert "${PATRONI_NODES[$i]}" "${PATRONI_IPS[$i]}"
done
 
 
chown -R etcd:etcd ${CERT_DIR}
chmod 600 ${CERT_DIR}/*.key
chmod 644 ${CERT_DIR}/*.crt
 
echo "Сертификаты успешно сгенерированы и сохранены в ${CERT_DIR}"

Запускаем скрипт:

chmod +x generate_etcd_certs.sh
sudo ./generate_etcd_certs.sh

Сделали. Чтобы узлы etcd взаимодействовали между собой, копируем ca.crt и node[01 или 03] на остальные узлы.

Теперь меняем права на всех узлах etcd:

chown -R etcd:etcd /etc/default/etcd/.tls
chmod -R 744 /etc/default/etcd/.tls
chmod 600 /etc/default/etcd/.tls/*.key

Шаг 4: определяем, как должен работать etcd (параметры)

На каждой ноде создаем конфиг, который минимально отличается от ноды к ноде (подсветили комментариями):

# /etc/etcd/etcd.conf.yml
name: etcd01 # Изменить на других нодах
data-dir: /var/lib/etcd/default
listen-peer-urls: https://0.0.0.0:2380
listen-client-urls: https://0.0.0.0:2379
advertise-client-urls: https://etcd01:2379 # Изменить на других нодах
initial-advertise-peer-urls: https://etcd01:2380 # Изменить на других нодах
initial-cluster-token: etcd_scope
initial-cluster: etcd01=https://etcd01:2380,etcd02=https://etcd02:2380,etcd03=https://etcd03:2380
initial-cluster-state: new
election-timeout: 5000
heartbeat-interval: 500
 
client-transport-security:
  cert-file: /etc/default/etcd/.tls/etcd01.crt # Изменить на других нодах
  key-file: /etc/default/etcd/.tls/etcd01.key
  client-cert-auth: true
  trusted-ca-file: /etc/default/etcd/.tls/ca.crt
 
peer-transport-security:
  cert-file: /etc/default/etcd/.tls/etcd01.crt # Изменить на других нодах
  key-file: /etc/default/etcd/.tls/etcd01.key
  client-cert-auth: true
  trusted-ca-file: /etc/default/etcd/.tls/ca.crt

Шаг 5: для запуска etcd cоздаем Systemd-Unit-файл

Как правило, сервис создается по следующему пути (мы использовали его): /etc/systemd/system/etcd.service

[Unit]
Description=etcd key-value store
Documentation=https://etcd.io/docs/
Wants=network-online.target
After=network-online.target
 
[Service]
User=etcd
Type=notify
ExecStart=/usr/bin/etcd --config-file=/etc/etcd/etcd.conf.yml
Restart=always
RestartSec=5
LimitNOFILE=40000
 
[Install]
WantedBy=multi-user.target

Шаг 6: применяем следующие команды

sudo systemctl daemon-reload
sudo systemctl enable etcd

Заметка на полях: Помним, что любое изменение конфига Systemd для новых настроек требует выполнения команд выше. А также не забываем, что поезд etcd ждать не будет :) (у нас есть только 30 секунд, чтобы запустить другие ноды кластера).

На этом этапе поочередно запускаем сервис с 1-й по 3-ю ноду с помощью команды: sudo systemctl start etcd

Шаг 7: настраиваем alias для etcdctl

Для этого добавляем alias в наш `~/.bashrc` или `~/.zshrc`:

echo 'alias ectl=«etcdctl --cacert=/etc/default/etcd/.tls/ca.crt --cert=/etc/default/etcd/.tls/$(hostname).crt --key=/etc/default/etcd/.tls/$(hostname).key --endpoints=https://etcd01:2379, https://etcd02:2379, https://etcd03:2379»' >> ~/.bashrc
source ~/.bashrc

Благодаря этому мы получаем статус кластера etcd, не прописывая каждый раз сертификаты.

Примечание. Учтите, что имя crt и key нод должно быть аналогично hostname. Иначе команда выше не сработает и придется все настраивать вручную.

Получаем удобную таблицу:

e79c9ed7e924e0c22365f1cb977262a8.png

После того как кластер запустился, на всех узлах редактируем конфиг `/etc/default/etcd` и устанавливаем параметр:

ETCD_INITIAL_CLUSTER_STATE="existing"
```

Перезапускаем службу:

systemctl restart etcd

Фух! С etcd закончили, :) приступаем к Postgres и Patroni.

Установка PostgreSQL

Шаг 1: скачиваем и устанавливаем пакеты из оф. репозитория

Делаем это отсюда: https://download.postgresql.org/pub/repos/

NB: В системе по умолчанию создается daemon PostgreSQL. Его требуется отключить. Применяем команду:

systemctl --now disable postgresql-16

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

mkdir /home/postgres
chown postgres:postgres /home/postgres
usermod --home /home/postgres postgres

Шаг 2: готовим каталог PGDATA

Для этого делаем:

mkdir -p /data/16
chmod -R 700 /data
mkdir /data/log
chown -R postgres:postgres /data
mkdir -p /var/run/postgresql
chown postgres:postgres /var/run/postgresql

Важное примечание на полях: на astra linux встречали удаление директории  /var/run/postgresql для сокетов, если ставить Postgres из оф. репозитория. Это можно поправить, если изменить сервис Postgres и добавить RuntimeDirectory=postgresql

Теперь нам понадобится каталог для сертификатов:
mkdir -p /opt/patroni/.tls

Помните, как мы сгенерировали сертификаты для etcd? Там же есть сертификаты для Patroni. Забираем их оттуда на узлы Patroniи назначаем права:

chmod -R 744 /opt/patroni/.tls
chmod 600 /opt/patroni/.tls/*.key

Установка Patroni: подготовительные работы

Шаг 1: устанавливаем свежую версию Python

Если вдруг ее не оказалось:

yum install openssl-devel libffi-devel bzip2-devel
tar -xzvf Python-3.12.4.tgz
cd Python-3.12.4/
./configure --enable-optimizations --with-ssl
make altinstall
sudo ln -sf /usr/local/bin/python3.12 /usr/bin/python3
sudo ln -sf /usr/local/bin/pip3.12 /usr/bin/pip3

Зачем? Так мы можем установить Patroni, не повредив систему, а также получить очень удобный каталог с бинарями.

Шаг 2: создаем виртуальную среду

Для этого используем команды:

python3 –m venv /opt/patroni
source /opt/patroni/bin/activate
mkdir –p /opt/patroni/packages
cd /opt/patroni/packages

Сама установка

Копируем whl и архивы в /opt/patroni/packages. Затем скачиваем следующие пакеты (примерный список):

click-8.1.7-py3-none-any.whl
dnspython-2.6.1-py3-none-any.whl
patroni-3.3.2-py3-none-any.whl
prettytable-3.10.2-py3-none-any.whl
psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl
python_dateutil-2.9.0.post0-py2.py3-none-any.whl
python-etcd-0.4.5.tar.gz
PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
setuptools-72.1.0-py3-none-any.whl
six-1.16.0-py2.py3-none-any.whl
urllib3-2.2.2-py3-none-any.whl
wcwidth-0.2.13-py2.py3-none-any.whl
ydiff-1.3.tar.gz
psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Применяем команды:

pip3 install --no-index --find-links=/opt/patroni/packages patroni[etcd3]
pip3 install --no-index --find-links=/opt/patroni/packages psycopg2-binary
chown -R postgres:postgres /opt/patroni

Patroni есть! Теперь добавляем в профайл PostgreSQL параметры:

export PG_CONFIG=/usr/pgsql-16/bin/pg_config
export PATRONI_CONFIG_FILE=/etc/patroni/config.yml

А теперь активируем виртуальную среду: source /opt/patroni/bin/activate

Настройка Patroni

Cоздаем конфигурационный файл /etc/patroni/config.yml на нодах кластера Patroni:

Скрытый текст

patroni
scope: patroni_cluster
namespace: /patroni
name: patroni_node01 # Изменить на 2 ноде
log:
  level: INFO
  dir: /data/log/patroni
  file_size: 50000000
  file_num: 10
restapi:
  listen: 0.0.0.0:8008
  connect_address: node01:8008 # Изменить на 2 ноде
  verify_client: optional
  cafile: /opt/patroni/.tls/ca.crt
  certfile: /opt/patroni/.tls/node01.crt # Не забыть изменить сертификаты на 2 ноде
  keyfile: /opt/patroni/.tls/node01.key
ctl:
  cacert: /opt/patroni/.tls/ca.crt # Не забыть изменить сертификаты на 2 ноде
  certfile: /opt/patroni/.tls/node01.crt
  keyfile: /opt/patroni/.tls/node01.key
etcd3:
  hosts: ["etcd01:2379", "etcd02:2379", "etcd03:2379"]
  protocol: https
  cacert: /opt/patroni/.tls/ca.crt
  cert: /opt/patroni/.tls/node01.crt # Не забыть изменить сертификаты на 2 ноде
  key: /opt/patroni/.tls/node01.key
watchdog:
  mode: off # Если настроен, можно включить
bootstrap:
  dcs:
    failsafe_mode: true
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    synchronous_mode: true
    synchronous_mode_strict: true
    synchronous_mode_count: 1
    master_start_timeout: 30
    slots:
      prod_replica1:
        type: physical
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        shared_buffers: '512MB'
        wal_level: 'replica'
        wal_keep_size: '512MB'
        max_connections: 100
        effective_cache_size: '1GB'
        maintenance_work_mem: '256MB'
        max_wal_senders: 5
        max_replication_slots: 5
        checkpoint_completion_target: 0.7
        log_connections: 'on'
        log_disconnections: 'on'
        log_statement: 'ddl'
        log_line_prefix: '%m [%p] %q%u@%d '
        logging_collector: 'on'
        log_destination: 'stderr'
        log_directory: '/data/log'
        log_filename: 'postgresql-%Y-%m-%d.log'
        log_rotation_size: '100MB'
        log_rotation_age: '1d'
        log_min_duration_statement: -1
        log_min_error_statement: 'error'
        log_min_messages: 'warning'
        log_error_verbosity: 'verbose'
        log_hostname: 'off'
        log_duration: 'off'
        log_timezone: 'Europe/Moscow'
        timezone: 'Europe/Moscow'
        lc_messages: 'C.UTF-8'
        password_encryption: 'scram-sha-256'
        debug_print_parse: 'off'
        debug_print_rewritten: 'off'
        debug_print_plan: 'off'
        superuser_reserved_connections: 3
        synchronous_commit: 'on'
        synchronous_standby_names: '*'
        hot_standby: 'on'
        compute_query_id: 'on'
      pg_hba:
        - local all all peer
        - host all all 127.0.0.1/32 scram-sha-256
        - host all all 0.0.0.0/0 md5
        - host replication replicator 127.0.0.1/32 scram-sha-256
        - host replication replicator 192.168.60.0/24 scram-sha-256
  pg_hba:
    - local all all peer
    - host all all 127.0.0.1/32 scram-sha-256
    - host all all 0.0.0.0/0 md5
    - host replication replicator 127.0.0.1/32 scram-sha-256
    - host replication replicator 192.168.60.0/24 scram-sha-256
  initdb: ["encoding=UTF8", "data-checksums", "username=postgres", "auth=scram-sha-256"]
  users:
    admin:
      password: 'new_secure_password1'
      options: ["createdb"]
postgresql:
  listen: 0.0.0.0
  connect_address: 192.168.60.131:5432 # не забываем заменить адрес на 2 ноде.
  use_unix_socket: true
  data_dir: /data/16
  config_dir: /data/16
  bin_dir: /usr/pgsql-16/bin
  pgpass: /home/postgres/.pgpass_patroni
  authentication:
    replication:
      username: replicator
      password: 'new_repl_password'
    superuser:
      username: postgres
      password: 'new_superuser_password'
    rewind:
      username: postgres
      password: 'new_superuser_password'
  parameters:
    unix_socket_directories: "/var/run/postgresql"
  create_replica_methods: ["basebackup"]
  basebackup:
    max-rate: 100M
    checkpoint: fast
tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false

Чтобы не пропустить ошибки, проверяем валидность конфига:

patroni --validate-config /etc/patroni/config.yml

Patroni требуется журналирование. Для этого создаем подкаталог для логов:

mkdir /data/log/patroni
chown -R postgres:postgres /data/log/patroni

Теперь — Systemd-Unit-файл для Patroni:

service patroni
[Unit]
Description=Patroni high-availability PostgreSQL
After=network.target
 
[Service]
User=postgres
Type=simple
ExecStart=/opt/patroni/bin/patroni /etc/patroni/config.yml
Restart=always
RestartSec=5
LimitNOFILE=1024
 
[Install]
WantedBy=multi-user.target

Перезапустите Systemd и запустите Patroni:

sudo systemctl daemon-reload
sudo systemctl enable patroni
sudo systemctl start patroni

Проверяем список узлов в кластере: patronictl list

Проверяем переключение (switchover)*:
patronictl  switchover
и убеждаемся, что Мастер переехал: у БД есть мастер-нода и slave-нода. Для просмотра подробной работы Patroni и PostgreSQL в арсенале есть логи:

Лог Patroni: /data/log/patroni/patroni.log

Логи экземпляра СУБД: /data/log/postgresql-*.log

Теперь у вас настроен высокодоступный кластер PostgreSQL с использованием Patroni и etcd. В следующей части мы расскажем, как интегрировать балансировщик и пулер pgbouncer в кластер, созданный в этой статье. До встречи!

© Habrahabr.ru