Patroni Proxy

patroni_proxy — TCP-балансировщик для кластеров PostgreSQL под управлением Patroni. Слушает один или несколько портов, спрашивает у Patroni REST API кто сейчас leader / sync / async, и направляет новые соединения на нужную роль по стратегии least-connections. Не выполняет пулинг соединений, не парсит wire-протокол, не знает что за SQL внутри — это работа pg_doorman, развёрнутого ниже по цепочке.

Что он делает

  • Открытие членов кластера через опрос /cluster с интервалом cluster_update_interval (по умолчанию 3 с) и по запросу GET /update_clusters.
  • Маршрутизация по ролям. Каждый listen-порт привязан к одной или нескольким ролям (leader, sync, async, any). Соединения с этого порта попадают на члена, у которого совпадает одна из указанных ролей.
  • Least-connections. Для портов с несколькими допустимыми членами прокси держит счётчик соединений на каждого члена и отправляет новое соединение туда, где их меньше. Обновление кластера не сбрасывает эти счётчики.
  • Отбрасывает реплики со старыми данными. max_lag_in_bytes per-port исключает членов, у которых replication_lag (из /cluster) выше порога. Leader по лагу никогда не исключается.
  • Пропускает не-running. Допускаются только члены со state: "running"; starting, stopped, crashed и узлы с тегом noloadbalance фильтруются.

Операционно важно следующее: при изменении топологии patroni_proxy обновляет таблицу маршрутизации только для новых соединений. Существующие TCP-соединения к ещё живому backend не трогаются. По сравнению с HAProxy + confd, где reload рвёт все соединения через затронутую backend-секцию, это значит, что cluster_update_interval не воюет с долгоживущими транзакциями.

Роли

РольОписание
leaderPrimary / master
syncСинхронные standby-реплики
asyncАсинхронные реплики
anyЛюбой running-член кластера

Рекомендуемое развёртывание

graph TD
    App1[Приложение A] --> PP(patroni_proxy<br/>TCP-балансировка)
    App2[Приложение B] --> PP
    App3[Приложение C] --> PP

    PP --> D1(pg_doorman<br/>пулинг)
    PP --> D2(pg_doorman<br/>пулинг)
    PP --> D3(pg_doorman<br/>пулинг)

    D1 --> PG1[(PostgreSQL<br/>leader)]
    D2 --> PG2[(PostgreSQL<br/>sync-реплика)]
    D3 --> PG3[(PostgreSQL<br/>async-реплика)]
  • pg_doorman живёт на хостах PostgreSQL. Делает пулинг, ведёт кеш prepared statements и парсит протокол — работа, которой выгодна низкая задержка до локального сокета.
  • patroni_proxy живёт рядом с приложением. Маршрутизирует TCP, владеет role-aware failover-решением и не лезет в дела пулера.

Если поток приложения небольшой и одного pg_doorman на кластер достаточно, схема сжимается до одного pg_doorman с включённым fallback через Patroni, а patroni_proxy можно вообще не разворачивать.

Конфигурация

Пример patroni_proxy.yaml:

# Интервал обновления кластера в секундах (по умолчанию: 3)
cluster_update_interval: 3

# Адрес HTTP API для health checks и ручных обновлений (по умолчанию: 127.0.0.1:8009)
listen_address: "127.0.0.1:8009"

clusters:
  my_cluster:
    # Endpoint'ы Patroni API (несколько — для отказоустойчивости)
    hosts:
      - "http://192.168.1.1:8008"
      - "http://192.168.1.2:8008"
      - "http://192.168.1.3:8008"
    
    # Опционально: TLS-конфигурация для Patroni API
    # tls:
    #   ca_cert: "/path/to/ca.crt"
    #   client_cert: "/path/to/client.crt"
    #   client_key: "/path/to/client.key"
    #   skip_verify: false
    
    ports:
      # Соединения к primary/master
      master:
        listen: "0.0.0.0:6432"
        roles: ["leader"]
        host_port: 5432
      
      # Read-only соединения к репликам
      replicas:
        listen: "0.0.0.0:6433"
        roles: ["sync", "async"]
        host_port: 5432
        max_lag_in_bytes: 16777216  # 16MB

Параметры конфигурации

ПараметрПо умолчаниюОписание
cluster_update_interval3Интервал в секундах между опросами Patroni API
listen_address127.0.0.1:8009Адрес для HTTP API
clusters.<name>.hostsСписок endpoint'ов Patroni API
clusters.<name>.tlsОпциональная TLS-конфигурация для Patroni API
clusters.<name>.ports.<name>.listenАдрес для listener этого порта
clusters.<name>.ports.<name>.rolesСписок разрешённых ролей
clusters.<name>.ports.<name>.host_portПорт PostgreSQL на бэкенд-хостах
clusters.<name>.ports.<name>.max_lag_in_bytesМаксимальный лаг репликации (опционально)

Использование

Запуск patroni_proxy

# Запуск с файлом конфигурации
patroni_proxy /path/to/patroni_proxy.yaml

# С debug-логированием
RUST_LOG=debug patroni_proxy /path/to/patroni_proxy.yaml

Перечитывание конфигурации

Перечитать конфигурацию без перезапуска (добавить или удалить порты, обновить hosts):

kill -HUP $(pidof patroni_proxy)

Ручное обновление кластера

Запустить немедленное обновление всех членов кластера через HTTP API:

curl http://127.0.0.1:8009/update_clusters

HTTP API

EndpointМетодОписание
/update_clustersGETЗапустить немедленное обновление всех членов кластера
/GETHealth check (возвращает "OK")

Сравнение с HAProxy + confd

Возможностьpatroni_proxyHAProxy + confd
Сохранение соединений при обновленииДаНет (reload разрывает соединения)
Hot-обновления upstreamНативныеТребуется confd + reload
Учёт лага репликацииВстроенТребуются кастомные проверки
Сложность конфигурацииОдин YAMLНесколько конфигов
Потребление ресурсовЛёгкийПроцессы HAProxy + confd
Маршрутизация по ролямНативнаяТребуются кастомные шаблоны

Сборка

# Сборка release-бинарника
cargo build --release --bin patroni_proxy

# Запуск тестов
cargo test --test patroni_proxy_bdd

Диагностика

No backends available

Если видите предупреждения вроде no backends available, проверьте:

  1. Patroni API доступен с хоста patroni_proxy.
  2. У членов кластера state: "running".
  3. Роли в конфигурации совпадают с реальными ролями members.
  4. Если используется max_lag_in_bytes -- проверьте текущий лаг реплик.

Соединения разрываются после обновления

С patroni_proxy этого происходить не должно. Если соединения всё-таки разрываются:

  1. Проверьте, действительно ли бэкенд-хост был удалён из кластера.
  2. Убедитесь, что порог max_lag_in_bytes не превышается.
  3. Включите debug-логирование, чтобы увидеть детальный жизненный цикл соединений.