Кадр из фильма «Терминатор 2: Судный день»
Кадр из фильма «Терминатор 2: Судный день»

В первой части мы остановились на том, что у нас есть отказоустойчивый кластер и теперь вроде бы все работает. Мы уже можем зайти в базу данных через лидера и что-нибудь туда записать. Последняя проблема, которую нужно решить — программно узнать, где находится лидер. Неужели нужно будет каждый раз делать эту проверку после падения, чтобы понять через какую ноду подключаться к базе?

Это было бы настолько скверно, что испортило бы все решение. Но не беспокойтесь, с помощью HA Proxy вопрос решается довольно легко и незатейливо.

HA Proxy

Также как с Patroni, сначала сделаем отдельную директорию под билд/деплой файлы и начнем их там создавать:

  • haproxy.cfg

Это файл конфига, который мы положим в наш кастомный образ.

haproxy.cfg
global
    maxconn 100
    stats socket /run/haproxy/haproxy.sock 
    stats timeout 2m # Wait up to 2 minutes for input

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 postgres
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server patroni1 patroni1:5432 maxconn 100 check port 8091
    server patroni2 patroni2:5432 maxconn 100 check port 8091
    server patroni3 patroni3:5432 maxconn 100 check port 8091

Details

В этих строчках мы назначаем порты, по которым будет получать доступ:

// этот для вывода статистики
listen stats
    mode http
    bind *:7000
//этот для подключения к postgres
listen postgres
    bind *:5000

А здесь мы просто перечисляем все сервисы Patroni, которые создали ранее:

server patroni1 patroni1:5432 maxconn 100 check port 8091
server patroni2 patroni2:5432 maxconn 100 check port 8091
server patroni3 patroni3:5432 maxconn 100 check port 8091

И последнее. Эта строка нужна, если мы хотим проверять состояние кластера с помощью специальной утилиты из контейнера с HA Proxy:

stats socket /run/haproxy/haproxy.sock 

  • Dockerfile

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

Dockerfile
FROM haproxy:1.7
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
RUN mkdir /run/haproxy &&    apt-get update -y &&    apt-get install -y hatop &&    apt-get clean

  • docker-compose-haproxy.yml

Compose файл выглядит тоже достаточно просто:

docker-compose-haproxy.yml
version: "3.7"

networks:
  patroni_patroni:
    external: true

services:
 haproxy:
    image: haproxy-patroni
    networks:
      - patroni_patroni
    ports:
      - 5000:5000
      - 7000:7000
    hostname: haproxy
    deploy:
      mode: replicated
      replicas: 1
      placement: 
        constraints: [node.hostname == floitet]

Когда все файлы готовы, можно и задеплоить весь этот сет:

// build
docker build -t haproxy-patroni
// deploy
docker stack deploy --compose-file docker-compose-haproxy.yml

Когда HА Proxy запустится, можно будет в контейнере посмотреть статистику кластера специальной командой:

sudo docker ps | grep haproxy
sudo docker exec -ti $container_id /bin/bash
hatop -s /var/run/haproxy/haproxy.sock

Выполнив эти три шага, мы увидим прямо в консоли красивый вывод статистики.

Сам я предпочитаю смотреть статистику через patronictl либо Patroni API, но HA Proxy тоже один из вариантов и почему бы не настроить и его заодно.

А теперь немного о Patroni API.

Patroni API

Я обещал показать три способа смотреть статистику кластера, и вот добрался-таки до последнего. Вывод статистики через API хорош тем, что он дает самую полную информацию о кластере (и вообще через него можно делать всё).

Более подробно почитать про API можно в доках, а здесь я покажу самые основные вещи: стандартные запросы и как их выполнить в нашем сетапе.

Также как с подключением к БД, мы не сможем обращаться к API, не находясь в сети ’patroni_patroni’. Так что нам придется слать все наши запросы из контейнера. Чтобы читать вывод json в приятном человеческому глазу формате, сделаем кастомный имейдж с curl’ом и jq.

Dockerfile
FROM alpine:3.10
RUN apk add --no-cache curl jq bash
CMD ["/bin/sh"]

И потом запустим контейнер с этим образом, подключив его к нужной сети:

docker run --rm -ti --network=patroni_patroni curl-jq

Теперь мы можем обращаться к API Patroni нод по их именам и получать cтаты в таком вот виде:

Работа с API
// Статистика ноды

curl -s patroni1:8091/patroni | jq
{
  "patroni": {
    "scope": "patroni",
    "version": "2.0.1"
  },
  "database_system_identifier": "6893104757524385823",
  "postmaster_start_time": "2020-11-15 19:47:33.917 UTC",
  "timeline": 10,
  "xlog": {
    "received_location": 100904544,
    "replayed_timestamp": null,
    "replayed_location": 100904544,
    "paused": false
  },
  "role": "replica",
  "cluster_unlocked": false,
  "state": "running",
  "server_version": 110009
}

// Статистика кластера

curl -s patroni1:8091/cluster | jq
{
  "members": [
    {
      "port": 5432,
      "host": "10.0.1.5",
      "timeline": 10,
      "lag": 0,
      "role": "replica",
      "name": "patroni1",
      "state": "running",
      "api_url": "http://10.0.1.5:8091/patroni"
    },
    {
      "port": 5432,
      "host": "10.0.1.4",
      "timeline": 10,
      "role": "leader",
      "name": "patroni2",
      "state": "running",
      "api_url": "http://10.0.1.4:8091/patroni"
    },
    {
      "port": 5432,
      "host": "10.0.1.3",
      "lag": "unknown",
      "role": "replica",
      "name": "patroni3",
      "state": "running",
      "api_url": "http://10.0.1.3:8091/patroni"
    }
  ]
}

Как тестировать?

Основная идея, что теперь мы можем ставить свои эксперименты с Patroni кластером так, как если бы это был кластер с тремя реальными серверами. Достаточно просто выключать и включать сервисы Patroni, чтобы сымитировать падения серверов. Если лидер у нас patroni3, то мы делаем:

docker service scale patroni_patroni3=0

И отключаем его, убивая его единственный контейнер. Теперь можно глянуть состояние кластера и убедиться, что лидер перешел на здоровую ноду:

postgres@patroni1:/$ patronictl -c /etc/patroni.yml list patroni
+ Cluster: patroni (6893104757524385823) --+----+-----------+
| Member   | Host      | Role    | State   | TL | Lag in MB |
+----------+-----------+---------+---------+----+-----------+
| patroni1 | 10.0.1.93 | Leader  | running |  9 |           |
| patroni2 | 10.0.1.91 | Replica | running |  9 |         0 |
+----------+-----------+---------+---------+----+-----------+

Если сделать scale для patroni3 на ’1', то он вернется в кластер и займет роль реплики.

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

Бонус

Как и обещал, ниже небольшой скрипт для тестов, а также инструкции как с ним работать. Так что, если вам нужно что-то уже готовое для быстрого теста, добро пожаловать. Если же у вас есть свои идеи и сценарии и вам не требуется готовое решение, просто пропустите спойлер.

Тестируем Patroni cluster

Для тех читателей, кто хочет потестировать кластер Patroni на каком-то готовом примере прямо сейчас, я сделал вот этот скрипт. Он супер простой, и я уверен у вас не возникнет проблем с тем, чтобы понять, как он работает. По сути, он просто пишет в базу текущее время каждую секунду. Ниже я шаг за шагом покажу, как его запустить и какие результаты теста были у меня.

  • Шаг 1

Допустим, вы уже скачали скрипт и положили его где-то у себя на машине. Если нет, то проделайте эту подготовку. Теперь нам нужно запустить Docker контейнер с официальным образом Miscrosoft SDK такой командой:

docker run --rm -ti --network=patroni_patroni -v /home/floitet/Documents/patroni-test-script:/home mcr.microsoft.com/dotnet/sdk /bin/bash

Два момента. Первое, как и раньше мы хотим подключиться к сети ’patroni_patroni’ и второе, мы делаем mount к той директории, где уже лежит готовый скрипт. Таким образом мы сможем запускать его из контейнера.

  • Шаг 2

Теперь мы хотим, чтобы у нас появился единственный dll, который нужен чтобы скрипт взлетел. Заходим в контейнер и находясь в директории ’/home’ создаем папку ’patroni-test’ для консольного приложения. Заходим в нее и выполняем следующую команду:

dotnet new console

// видим такие строчки

Processing post-creation actions...
Running 'dotnet restore' on /home/patroni-test/patroni-test.csproj...
  Determining projects to restore...
  Restored /home/patroni-test/patroni-test.csproj (in 61 ms).
Restore succeeded.

И теперь мы можем добавить в проект нужный нам для работы пакет:

dotnet add package npgsql

А потом просто упаковываем проект:

dotnet pack

Если все прошло удачно, то мы получим ’Npgsql.dll’ по адресу: ’patroni-test/bin/Debug/net5.0/Npgsql.dll’.

Этот путь мы и добавляем как референс в скрипте, так что если у вас он отличается от моего, то в скрипте это нужно подправить.

А дальше просто запускаем скрипт:

dotnet fsi /home/patroni-test.fsx

// и в output мы увидим как скрипт пошел писать время: 

11/18/2020 22:29:32 +00:00
11/18/2020 22:29:33 +00:00
11/18/2020 22:29:34 +00:00

Важно не закрывать терминал со скриптом, пока идет тест.

  • Шаг 3

Давайте посмотрим, где сейчас находится лидер, чтобы знать кого ронять. Можно использовать любой из трёх способов, я смотрел через patronictl:

+ Cluster: patroni (6893104757524385823) --+----+-----------+
| Member   | Host      | Role    | State   | TL | Lag in MB |
+----------+-----------+---------+---------+----+-----------+
| patroni1 | 10.0.1.18 | Replica | running | 21 |         0 |
| patroni2 | 10.0.1.22 | Leader  | running | 21 |           |
| patroni3 | 10.0.1.24 | Replica | running | 21 |         0 |
+----------+-----------+---------+---------+----+-----------+

Теперь нам нужно открыть новый терминал и «убить» лидера:

docker service ls | grep patroni
docker service scale $patroni2-id=0

Через какое-то время в окне со скриптом мы увидим сообщения об ошибке:

// давайте запомним время последней удачной записи 

11/18/2020 22:33:06 +00:00
Error
Error
Error

Если мы проверим статус кластера, то можем заметить некоторую задержку — он всё ещё показывает patroni2 в качестве лидера. Но спустя n секунд он все-таки перестроится и, пройдя короткую стадию по выбору лидера, придет в такое состояние:

+ Cluster: patroni (6893104757524385823) --+----+-----------+
| Member   | Host      | Role    | State   | TL | Lag in MB |
+----------+-----------+---------+---------+----+-----------+
| patroni1 | 10.0.1.18 | Replica | running | 21 |         0 |
| patroni3 | 10.0.1.24 | Leader  | running | 21 |           |
+----------+-----------+---------+---------+----+-----------+

Если же вернемся к терминалу со скриптом, то увидим, что соединение наконец-то восстановлено и запись возобновилась:

Error
Error
Error
11/18/2020 22:33:48 +00:00
11/18/2020 22:33:49 +00:00
11/18/2020 22:33:50 +00:00
11/18/2020 22:33:51 +00:00
  • Шаг 4

Теперь проверим, как там поживает сама база данных и всё ли с ней в порядке после падения лидера:

docker run --rm -ti --network=patroni_patroni postgres:11 /bin/bash
psql --host haproxy --port 5000 -U approle -d postgres
postgres=> \c patronitestdb
You are now connected to database "patronitestdb" as user "approle".
// Я установил время чуть раньше, чем произошла авария
patronitestdb=> select * from records where time > '22:33:04' limit 15;
time       
-----------------
 22:33:04.171641
 22:33:05.205022
 22:33:06.231735
 // как мы видим в моем случае Patroni понадобилось 42 секунды 
 // чтобы восстановить соединение 
 22:33:48.345111
 22:33:49.36756
 22:33:50.374771
 22:33:51.383118
 22:33:52.391474
 22:33:53.399774
 22:33:54.408107
 22:33:55.416225
 22:33:56.424595
 22:33:57.432954
 22:33:58.441262
 22:33:59.449541

Вывод по тестам

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

Послесловие

Полагаю, на этом можно считать туториал завершенным. Надеюсь, он помог вам разобраться с основными моментами в настройке и работе с Patroni и в целом был полезен. Спасибо за внимание и да хранит Patroni ваши данные, отстреливаясь от багов и героически поднимаясь после падений!

За помощь в настройке этого решения отдельное спасибо коллеге Андрею Юрченко. Без этого отзывчивого парня мои Patroni застряли бы в магазине/стволе и не убили бы ни одного врага.

Все файлы использованные во второй части здесь.