Кластер Patroni состоит из нескольких кластеров баз данных PostgreSQL и использует кластер etcd. Каждый из членов кластера - хост либо виртуальная машина. Для тестовых целей иметь несколько виртуальных машин - ресурсоёмко и неудобно для использования.
В статье рассматривается создание кластера Patroni и etcd в одной виртуальной машине в контейнерах docker. Приводится пример, когда Patroni не может автоматически восстановить и запустить кластер PostgreSQL.
Patroni в докере
Задача запуска Patroni в докере обсуждалась на реддит и гитхаб. Я воспользовался наиболее простой сборкой batonogov/patroni-docker, преимущество которой в том, что она работает «из коробки». Сборка состоит из 7 контейнеров: трёх с кластером etcd 3.5.18 и трёх с PostgreSQL 17.3 под управлением Patroni 4.0.4 (мастер и две реплики), один контейнер с HAProxy 3.1.3.
Установка сборки
Установка docker и docker-compose из репозитория linux:
apt-get update
apt-get install docker.io docker-compose-v2
или по документации официального докера, если был добавлен его репозиторий:
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extra
Установка сборки:
su -
wget https://github.com/batonogov/patroni-docker/archive/refs/heads/main.zip
unzip main.zip
cd patroni-docker-main/examples/docker
Дальше можно редактировать файл docker-compose.yaml, как описано дальше, и создать контейнеры командой, находясь в директории с файлом docker-compose.yaml:
docker-compose up -d
Если что-то пошло не так, то удалить контейнеры можно командой:
docker-compose down
после чего отредактировать файл docker-compose.yaml и снова создать контейнеры.
Проблемы сборок docker
Сборка запускается, но имеет недостатки, общие для большинства сборок:
1) смапированы только порт 8080 веб-интерфейса HAProxy и порты 5432 и 5433, на которых прослушивает HAProxy. Порт 5432 направляется на текущего мастера, а 5433 на какую-то из реплик. HAProxy, направляет соединение на одну из реплик, для тестов нужны прямые соединения на порт экземпляра PostgreSQL, а их нет.
2) не доступны порты 8008 REST API Patroni. Должны быть доступны REST API всех трёх контейнеров. Хочется работать с Patroni и через его веб-интерфейс.
3) докеры делают упор независимость от окружения, в котором запускаются. Например, автоматически назначаются IP-адреса, имена хостов, изолируются контейнеры. В реальности в базовых вещах нет независимости от окружения: на хосте порты порты 5432 и 5433 уже заняты, а сборка пытается их мапировать.
4) при перезапуске контейнеров каждый раз контейнерам выдаются новые IP адреса. У докера нет встроенного DNS, чтобы можно было по именам находить IP-адреса контейнеров. Даже если бы он и был, конфигурировать зоны DNS не просто.
5) хост не имеет доступа к ip-адресам контейнеров. В целях безопасности это может быть оправданно и надо открывать только порты, но это делает докер не удобным для тестирования.
6) имена хостов установлены как CONTAINER ID и имеют вид 4ee2dc1ed4f3, что хорошо для промышленного использования с большим числом контейнеров, но неудобно для тестирования. Это исправляется добавлением строки с атрибутом (ключом) в описатели служб в файле docker-compose.yaml:
hostname: имя
Хотя хост не сможет разрешить это имя в ip, с понятными именами изучение Patroni упрощается.
7) файл hosts в контейнерах нельзя отредактировать. Точнее, отредактировать можно, только это нестабильно. Докер в процессе запуска сам редактирует /etc/hosts контейнеров, а атомарности и согласованности не предусмотрено. Команды типа:
docker exec -it -u root docker-patroni0-1 echo "10.0.2.15 hostname" >> /etc/hosts
могут не выполняться или выполняются случайным образом. Вероятно, они выполняются асинхронно (как сигнал службе докера), докер их может случайным образом игнорировать, не выдавая ошибки.
8) У процесса patroni PID=1, а этого не должно быть, так как с осиротевшими процессами он не умеет работать:
root@71f9940f164b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
postgres 1 0 0 19:01 ? 00:00:01 /usr/bin/python3 /usr/bin/patroni /patroni.yml
Доступ хоста к сетям контейнеров
Для тестов удобно, чтобы хост имел доступ к сетям контейнеров.
Об этой проблеме задумались только к 28 версии докера. В 28 версии можно будет настроить доступ в файле конфигурации:
cat << EOF > /etc/docker/daemon.json
{ "allow-direct-routing": true }
EOF
До 28 версии можно установить в файле службы докера /etc/systemd/system/multi-user.target.wants/docker.service
переменную окружения в разделе Service:
[Service]
Environment="DOCKER_INSECURE_NO_IPTABLES_RAW=1"
Для применения параметра нужно перечитать параметры служб и перезапустить контейнеры:
systemctl daemon-reload
systemctl restart docker
Однако, это не решает проблему с доступом из контейнера в контейнер разных сборок. Если контейнеры используют разные сети докера (это происходит по умолчанию), то доступ они могут иметь разве что через мапирование порта (ключ ports) на хосте или ключ expose.
Пятая проблема решена.
Изменение смапированых портов
Можно поменять порты в docker-compose.yaml у ключа ports на незанятые на хосте (5434 и 5435):
ports:
- 5434:5432
- 5435:5433
- 8080:8080
Третья проблема решена.
Назначение статических IP-адресов
Для тестирования удобно, чтобы три контейнера Patroni имели статические IP-адреса. Для их назначения в трёх местах файла docker-compose.yaml в конец конфигурации трёх контейнеров добавить строки вида:
patroni1:
networks:
patroni:
ipv4_address: 172.21.1.1
extra_hosts:
- "education.tantorlabs.ru:10.0.2.15"
Ключом extra_hosts решается седьмая проблема редактирования /etc/hosts контейнеров, в которые добавляется строка с адресом хоста, чтобы службы внутри контейнера могли использовать имя хоста.
Четвертая и седьмая проблемы решены.
Доступ к ip-адресам контейнеров из контейнеров других сборок docker-compose
Контейнеры, которые созданы и запущены из разных файлов docker-compose.yaml (разных "сборок"), используют разные подсети. Для каждой сборки назначается своя подсеть 172.x.0.0. Трафик между подсетями заблокирован фаерволом.
Можно создавать сети вручную и назначать их по именам сетей. Чтобы не плодить подсети, можно использовать одну для всех контейнеров, которые планируется использовать. Например, Платформа Tantor использует подсеть с названием eco_default. В конец файла docker-compose.yaml сборок можно добавить ключ:
networks:
patroni:
name: eco_default
external: true
Это позволит использовать контейнерам сборки существующую сеть с имененм eco_default. Особенность в том, что статические IP адреса нужно будет указывать из этой подсети. Также эта сеть должна уже существовать.
Посмотреть какие IP-адреса использует сеть можно командой:
docker network inspect eco_default | grep Subnet
"Subnet": "172.18.0.0/16",
Если статитческий адрес будет не из этой подсети, то при создании контейнера будет выдана ошибка:
Error response from daemon: invalid config for network e05bace43f1...f9062506eff: invalid endpoint settings:
user specified IP address is supported only when connecting to networks with user configured subnets
Использование одной сети разными сборками позволяет контейнерам общаться друг с другом и уменьшает число подсетей.
Можно было бы использовать сеть, как описано в документации докера:
networks:
patroni:
ipam:
driver: default
config:
- subnet: "172.16.238.0/24"
в этой сети всем контейнерам нужно будет указать статические IP из этой подсети, так как подсеть ipam не может выдавать динамические ip-адреса, это приводит к ошибке:
Error response from daemon: Address already in use
Проблема в том, что контейнеры существующих сборок не могут иметь доступ к этой подсети - по умолчанию докер блокирует доступ между подсетями и эта теоретически правильная методика назначения адресов практически бесполезна. Переконфигурирование работающих контейнеров сборки требует пересоздание контейнеров. Более того, перед редактированием docker-compose.yaml нужно разрушить контейнера (docker-compose down), иначе правильно разрушить их докеру не удастся и от старой конфигурации останутся, например, сети.
Первая и вторая проблемы решены: все сборки (Patroni, Grafana, Платформа) теперь имеют доступ к портам контейнеров Patroni: экземплярам и REST IP (Платформа управляет Patroni через REST).
Что можно улучшить
Недостатки сборки описаны в конце обсуждения :
1) HAProxy doesn't reconnect restarted database nodes, seems to cache IP addresses, needs to configure Docker dns resolver
Статические IP адреса у контейнеров Patroni устранили эту проблему.
2) etcd container doesn't come back automatically as issued in etcd/issues/20327
Если не предполагаются эксперименты с рестартом etcd-контейнеров, то это не проблема. Устраняется увеличением числа etcd-контейнеров. При использовании --initial-cluster-state=new трёх контейнеров недостаточно, нужно пять.
3) для диагностики хотелось бы видеть лог Patroni. В последней строке файла /root/patroni-docker-main/examples/docker/entrypoint.sh можно добавить переменные окружения:
export PATRONI_LOG_DIR="/var/lib/postgresql/patroni/main/log"
export PATRONI_LOG_LEVEL=DEBUG
Лог Patroni мне показался бесполезным: например, запросы через REST API не все или не всегда логируются. Выяснить причину проблемы незапуска экземпляра (описывается дальше) по логу не удалось. Лог должен помогать администраторам, а не быть артефактом тестирования программного кода.
4) Используется версия протокола etcd v2, который был убран в etcd версии 3.6. etcd стал использовать протокл v3. Можно убрать из docker-compose.yaml строки "--enable-v2=true", если хочется проверить работу с другой версией etce, то заменить тэг в адресе образа на нужную версию, например, "image: quay.io/coreos/etcd:v3.6.5", а в файле patroni.yml заменить ключ "etcd:" на "etcd3:". Patroni в контейнерах обновляется по стандартной процедуре.
Установка дополнительных программ в контейнеры
В контейнеры можно доставить программы:
docker exec -it -u root docker-patroni0-1 bash
apt update
apt install procps net-tools mc curl iputils-ping -y
В контейнере программы, обычно, запускаются через скрипт entrypoint.sh, а не systemd. Если нужно оставить программу, которая запускается как служба, то самое простое - добавить её в файл entrypoint.sh. Пример добавления программы /usr/sbin/pmaagent:
root@tantor:~/patroni-docker-main/examples/docker# tail -n 2 entrypoint.sh
/usr/sbin/pmaagent &
exec /usr/bin/patroni /patroni.yml
Директория PGDATA кластеров PostgreSQL монтируются в три контейнера как тома (volumes) находятся они в директориях:
/root/patroni-docker-main/examples/docker/patroni-dataN
Удобно просматривать с хоста файлы параметров. Если добавляемая в контейнер программа использует свои файлы (так же как PostgreSQL использует PGDATA), можно добавить том (volume). Например, смапировать директорию программы /var/lib/pma, как это сделано для /var/lib/postgresql/data:
volumes:
- ./patroni-data0:/var/lib/postgresql/patroni/main
Для чего мне понадобился Patroni в докере
В августе была выпущена 6 версия Платформы Tantor (приложение класса Enterprise Manager для управления любыми форками PostgreSQL). Возникла идея сделать пример управления кластером Patroni, так как Patroni становится всё более популярным у пользователей Платформы. Создавать несколько виртуальных машин не хотелось и я решил использовать контейнеры docker. Знакомство с docker начал с Prometheus и Grafana. Платформа Tantor может с ними взаимодействовать, инструкция по установке Prometheus и Grafana в контейнерах docker довольно простая и описана в документации. Установка контейнеров с ними помогла освоиться с докером. Я познакомился с файлом docker-compose.yaml Расширение у файла может быть yaml или yml. Особенность формата yaml в том, что в нём важны пробелы, а они при копировании в текстовых редакторах часто теряются или меняются на табуляции.

Краткое описание docker-compose.yaml: https://dba1.ru/pl6/PL6-Book.htm#id.5dfczvduofzc
Вторая проблема - старые версии docker и docker-compose, из-за которых могут не запускаться сборки. Создатели сборок используют свежие версии докера.
Версии докера и docker-compose
В документации докера написано, что производители linux создают неофициальные пакеты с докером, которые называют docker.io, docker-compose, docker-compose-v2 и эти пакеты несовместимы с "официальным" докером:
Your Linux distribution may provide unofficial Docker packages, which may conflict with the official packages provided by Docker. You must uninstall these packages before you install the official version of Docker Engine.
Например, параметры файла docker-compose.yaml могут появиться, начиная с какой-то версии:

на старых версиях docker-compose параметр не будет распознан.
Приведённая картинка полезна тем, что показывает ссылку на описание файла docker-compose.yaml. При заходе на страницу документации, левая часть страницы (меню) его не отображает. Чтобы попасть на неё, нужно найти на странице ссылку Reference.
На чистых инсталляциях linux докер работает корректно и с неофициальными и с официальными пакетами. После нескольких обновлений, мешанина альтернатив и значений в PATH в linux может приводить к тому, что откуда-то достаются старые версии docker-compose. Чтобы избежать "чудес" можно установить docker-compose так:
1) Посмотреть номер последней версии docker-compose на странице: https://github.com/docker/compose/releases/latest
2) Вставить в следующую строку номер версии и скачать:
curl -SL https://github.com/docker/compose/releases/download/v2.39.4/dockercompose-linux-x86_64 -o /usr/local/bin/docker-compose
3) Установить право на выполнение и создать символическую ссылку, на случай если в /usr/bin появится другой файл:
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
После установки можно убедиться, что используется установленная версия:
docker-compose -v
Docker Compose version v2.39.4
Также можно проверить версию докера:
docker -v
Docker version 26.1.4
Если версии свежие, то проблемы с созданием и работой контейнеров уменьшаются.
Patroni не всегда реагирует на проблемы
При работе Patroni в докере, после запуска контейнеров, у меня обнулялись размеры нескольких файлов:
-rw------- 1 0 Oct 17 15:23 patroni.dynamic.json
drwx------ 2 4096 Oct 3 17:34 pg_commit_ts
drwx------ 2 4096 Oct 3 17:34 pg_dynshmem
-rw------- 1 0 Oct 17 15:23 pg_hba.conf
-rw------- 1 0 Oct 17 15:23 pg_hba.conf.backup
-rw------- 1 0 Oct 17 15:23 pg_ident.conf
-rw------- 1 0 Oct 17 15:23 pg_ident.conf.backup
Файловые системы нетранзакционны и манипуляции с файлами чреваты сбоями. Например, PostgreSQL при работе с журналами заранее создает WAL-файл, (записывает в него нули до нужного размера по параметру wal_init_zero), зная, что место может закончиться в любой момент, checkpointer заранее резервирует файлы WAL для будущей контрольной точки (параметр wal_recycle). Благодаря такой аккуратности PostgreSQL высокостабилен, журнальные файлы неправильного размера не встречаются, транзакции не теряются даже при нехватке места. Возможно, Patroni работал с файлами менее аккуратно, раз они обнулились.
Если на экземпляре PostreSQL файл pg_hba.conf нулевого размера, то экземпляр не стартует и Patroni ничего с экземпляром не делает:
docker exec -u root -it docker-patroni1-1 patronictl -c /patroni.yml list
+ Cluster: patroni (7557003904941080601) ------------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+--------------+------------+---------+--------------+----+-----------+
| 71f9940f164b | 172.21.1.3 | Replica | streaming | 6 | 0 |
| 83c751356e8f | 172.21.1.1 | Leader | running | 6 | |
| b53657ba2894 | 172.21.1.2 | Replica | start failed | | unknown |
+--------------+------------+---------+--------------+----+-----------+
Можно было бы назвать это фичей, но это баг. При рестарте виртуальной машины обнуление файлов на реплике происходит довольно часто, так как сразу после запуска виртуальной машины нагрузка большая и всплывают проблемы с асинхронностями.
Для возврата экземпляра в строй можно руками выполнить пересоздание (переинициализацию) через командную строку:
docker exec -u root -it docker-patroni2-1 patronictl -c /patroni.yml reinit patroni b53657ba2894
Are you sure you want to reinitialize members b53657ba2894? [y/N]: y
Success: reinitialize for member b53657ba2894
После чего все экземпляры работоспособны:
docker exec -u root -it docker-patroni2-1 patronictl -c /patroni.yml list
+ Cluster: patroni (7557003904941080601) ---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+--------------+------------+---------+-----------+----+-----------+
| 71f9940f164b | 172.21.1.3 | Replica | streaming | 6 | 0 |
| 83c751356e8f | 172.21.1.1 | Leader | running | 6 | |
| b53657ba2894 | 172.21.1.2 | Replica | streaming | 6 | 0 |
+--------------+------------+---------+-----------+----+-----------+
В команде видно, что приходится указывать путь к файлу patroni.yml. Чтобы этого не делать каждый раз, в файл docker-compose.yaml достаточно добавить путь к файлу в ключ с переменной окружения, задающей утилите patronictl путь к конфигурационному файлу:
environment:
PATRONICTL_CONFIG_FILE: /patroni.yml
Перечитывать файл docker-compose не умеет, только разрушать и пересоздавать контейнеры командами docker-compose down и docker-compose up -d. При выполнии этих команд, изменения в контейнерах, не вынесенные на тома (volumes), теряются. Изменения в других файлах перечитываются без разрушения контейнера - командой docker restart.
Проблема defunct (зомби) процессов в контейнерах
Если в docker-compose.yaml добавить в ключ patroni1 ключ user: root, чтобы скрипт entrypoint.sh запускался под root (а не под postgres) и в entrypoint.sh заменить строку /usr/bin/patroni /patroni.yml на exec su postgres -c "/usr/bin/patroni /patroni.yml", то при switchover процессы postmaster и logger станут defunct:
root@patroni2:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:19 ? 00:00:00 su postgres -c /usr/bin/patroni /patroni.yml
postgres 8 1 0 21:19 ? 00:00:01 /usr/bin/python3 /usr/bin/patroni /patroni.yml
postgres 24 1 0 21:19 ? 00:00:00 [postgres] <defunct>
postgres 25 1 0 21:19 ? 00:00:00 [postgres] <defunct>
такой экземпляр будет показываться patronictl состоянием unknown
root@patroni1:/# patronictl list
+ Cluster: patroni (7557003904941080601) ----------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+----------+------------+--------------+-----------+----+-----------+
| patroni1 | 172.21.1.1 | Sync Standby | streaming | 26 | 0 |
| patroni2 | 172.21.1.2 | Replica | starting | | unknown |
| patroni3 | 172.21.1.3 | Leader | running | 26 | |
+----------+------------+--------------+-----------+----+-----------+
и поможет только рестарт контейнера. Чтобы такого не было, контейнера с PostgreSQL ВСЕГДА должны запускаться с init, чтобы PID=1 занял процесс /sbin/docker-init:
root@patroni2:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:53 ? 00:00:00 /sbin/docker-init -- /bin/sh /entrypoint.sh
То есть в файле docker-compose.yaml у контейнеров с PostgreSQL добавлять ключ init: true Пример:
patroni1: &patroni-base
image: ghcr.io/batonogov/patroni-docker:v4.0.4-pg17.3
hostname: patroni1
user: root
init: true
entrypoint: [/bin/sh, /entrypoint.sh]
...
Результат
Готовый образ виртуальной машины с последними версиями Patroni (4.0.7, etcd 3.6.5) выложен под названием Tantor Platform 6.ova на https://disk.360.yandex.ru/d/eUu522ezEGuXvA Раскрытый образ занимает на диске 28Гб и 8Гб памяти, пароли пользователей linux такие же, как их имена. После запуска виртуальной машины можно запустить браузер Firefox.
Образ запускается в Oracle Virtualbox (или аналогах). Его можно использовать, чтобы посмотреть как работает Patroni. Patroni добавлен в Платформу Tantor и можно тестировать переключение, изменение параметров в браузере. Использование веб-интерфейса помогает сконцентрироваться на логике работы Patroni и упрощает изучение его работы.
Переключение Patroni в синхронный режим и switchover
После установки сборки, кластер Patroni работает в асинхронном режиме. Как пример переконфигурирования Patroni, можно переключить в синхронный режим, поменяв параметр synchronous_mode:

На странице собраны все параметры Patroni и есть их описание (символ “i” в кружке).
Можно выполнить switchover (переключиться на реплику, сменить лидера):

Переключение отобразится в истории переключений (switchover, failover):

В статье описано, как не тратя много времени познакомиться с Patroni; снять ограничения докера на сетевое взаимодействие; даётся пример проблемы с Patroni, требующей ручного вмешательства