Маршрутизация запросов чтения на реплики

Содержание

Маршрутизация запросов чтения на реплики#

1. Общие положения#

Документ описывает архитектуру и порядок настройки дополнительных пулов подключения (Read Pool) в Global ERP для работы с читающими репликами PostgreSQL.

Вся функциональность разделяется на два логических режима:

  • Синхронные реплики — поддерживаемый и рекомендованный режим.

  • Асинхронные реплики — режим находится в разработке и в текущих версиях системы не поддерживается.

2. Синхронные реплики#

2.1 Назначение и область применения#

PostgreSQL имеет ограниченные возможности вертикального масштабирования, упирающиеся в физические ресурсы сервера (CPU, RAM, дисковая подсистема). Практическая рекомендация при эксплуатации продуктивных систем — средняя загрузка CPU мастер-базы не должна превышать ~50%.

При достижении данного порога дальнейший рост нагрузки рекомендуется обеспечивать за счёт подключения читающих реплик, на которые переносится часть read-нагрузки (вплоть до 100%). Это позволяет:

  • снизить нагрузку на мастер-базу;

  • стабилизировать производительность при росте числа пользователей и отчётов;

  • обеспечить масштабирование без увеличения ресурсов мастер-базы.

Ключевой принцип работы#

В режиме синхронных реплик сервер приложений распределяет все запросы чтения, выполняемые вне активных транзакций.

  • все write-запросы и любые запросы внутри транзакций всегда выполняются на мастер-базе;

  • все read-запросы вне транзакций рассматриваются как равнозначные и распределяются глобально;

  • отсутствует управление конкретными SQL-запросами — невозможно указать, какой именно запрос должен выполняться на мастер-базе или реплике;

  • управление осуществляется только процентным распределением read-нагрузки между пулами.

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

Планируемые улучшения#

В разработке находится инструмент, который позволит на прикладном уровне управлять отправкой запросов на реплики, отключать использование реплик для интерфейсов, отчётов, печатных форм.

Данный механизм предназначен для сглаживания архитектурного ограничения, связанного с невозможностью управлять распределением отдельных SQL-запросов, и позволит исключать чувствительные сценарии из использования читающих реплик без изменения конфигурации БД.

2.2 Поддерживаемые сценарии и ограничения#

  • Поддерживаются только синхронные реплики PostgreSQL.

  • Асинхронная репликация в данном режиме не используется.

  • Распределение читающих запросов осуществляется глобально на уровне системы, а не на уровне отдельных интерфейсов или SQL-запросов.

Внимание

Каждая подключаемая реплика должна быть синхронной.

2.3 Рекомендации по количеству реплик#

Рекомендуется использовать не менее двух читающих реплик.

Основная причина данной рекомендации — защита мастер-ноды от резкого возврата нагрузки при отказе одной из реплик:

  • при использовании одной реплики значительная часть read-нагрузки уходит с мастера;

  • при отказе этой реплики вся нагрузка мгновенно возвращается на мастер;

  • в условиях высокой загрузки мастера это может привести к деградации или отказу системы.

Использование двух и более реплик позволяет перераспределять нагрузку без резкого скачка нагрузки на мастер.

2.4 Типы синхронной репликации PostgreSQL#

PostgreSQL поддерживает несколько режимов синхронной репликации, управляемых параметром synchronous_commit:

  • off — коммит не ожидает подтверждений;

  • local — ожидание записи WAL на диск мастера;

  • on (по умолчанию) — ожидание записи WAL на синхронную реплику;

  • remote_write — ожидание записи WAL в ОС реплики;

  • remote_flush — ожидание fsync WAL на реплике;

  • remote_apply — ожидание применения WAL на реплике.

Для работы читающих реплик в Global ERP критически необходим режим synchronous_commit = remote_apply.

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

2.5 Архитектурные ограничения и риски#

Основной риск — блокировка COMMIT на мастере#

При использовании synchronous_commit = remote_apply подтверждение COMMIT на мастере происходит только после применения WAL на всех синхронных репликах.

Это означает:

  • производительность системы определяется самой медленной репликой;

  • деградация одной реплики напрямую влияет на скорость работы мастера;

  • при блокировке WAL replay на реплике COMMIT-операции на мастере будут ожидать.

Конфликты WAL replay#

Длительные read-запросы на реплике могут конфликтовать с применением WAL (VACUUM, DDL, обновление visibility map).

В этом случае PostgreSQL вынужден:

  • либо задерживать применение WAL;

  • либо отменять конфликтующий read-запрос.

До момента разрешения конфликта COMMIT на мастере может быть заблокирован.

2.6 Управление конфликтами WAL replay#

Для ограничения времени блокировки WAL replay используются параметры:

  • max_standby_streaming_delay — для потоковой репликации;

  • max_standby_archive_delay — для восстановления из архива.

После превышения заданного лимита конфликтующие read-запросы принудительно отменяются.

Отмена read-запросов является штатным и ожидаемым механизмом, направленным на защиту мастер-базы и поддержание реплик в актуальном состоянии.

Пользовательский опыт (UX)#

При автоматической отмене read-запроса пользователь получит понятное и явное сообщение об ошибке, содержащее следующую информацию:

  • операция была прервана автоматически;

  • причина прерывания — поддержание реплики в актуальном состоянии;

  • действие, рекомендуемое пользователю — повторить операцию позже.

Такое поведение является ожидаемым для архитектуры с синхронными репликами и не свидетельствует о сбое системы.

В перспективе рассматривается доработка сервера приложений, позволяющая:

  • автоматически повторять отменённые read-запросы;

  • выполнять повтор либо на той же реплике, либо на мастер-базе;

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

2.7 Параметр hot_standby_feedback#

При использовании читающих реплик параметр hot_standby_feedback должен быть включён.

Назначение параметра:

  • передача информации о snapshot’ах standby на мастер;

  • снижение вероятности конфликтов между VACUUM на мастере и WAL replay на реплике.

Важно понимать, что hot_standby_feedback:

  • не гарантирует, что VACUUM или autovacuum на мастере не будет выполнен;

  • не блокирует уже запущенные процессы VACUUM;

Параметр предназначен для уменьшения частоты возникновения конфликтов между длительными read-запросами на репликах и очисткой старых версий строк (cleanup) на мастере.

Основной эффект использования:

  • VACUUM на мастере может быть отложен до завершения read-запросов на реплике;

  • снижается вероятность отмены read-запросов из-за конфликтов WAL replay.

Как следствие возможен временный рост таблиц и bloat. Требуется мониторинг VACUUM и состояния таблиц.

2.8 Позиция вендора по использованию синхронных реплик#

Вендор осознанно принимает следующие риски:

  • возможный рост таблиц из-за отложенного VACUUM;

  • возможность блокировки COMMIT при длительных read-запросах на репликах.

При этом:

  • отмена read-запросов с помощью max_standby_streaming_delay считается штатным механизмом;

  • длительные аналитические запросы на активно изменяемых таблицах требуют отдельной настройки и должны выполняться на мастер-базе, без использования читающих реплик;

  • режим признаётся пригодным для продуктивных систем при корректной эксплуатации.

2.9 Архитектура приложения и распределение нагрузки#

Сервер приложений не выполняет функции балансировки и отказоустойчивости.

Распределение нагрузки:

  • write-запросы → WritePool;

  • read-запросы вне транзакций → ReadPool.

Для отказоустойчивой схемы используется внешний балансировщик нагрузки.

Вендором рекомендуется использование HAProxy в качестве балансировщика нагрузки.

Текстовая схема:

Application Server
  |
  |-- WritePool --> Master DB
  |
  |-- ReadPool --> Load Balancer (HAProxy)
                    |
                    |-- Replica 1
                    |-- Replica 2
                    |-- Master (резерв)

Балансировщик нагрузки выполняет:

  • распределение read-нагрузки между репликами;

  • автоматическое исключение деградировавших или недоступных реплик;

  • переключение на мастер-ноду в случае отказа всех реплик.

При отказе реплики балансировщик автоматически исключает её из распределения нагрузки без изменения конфигурации приложения.

Особенности балансировки на уровне соединений#

Важно учитывать, что балансировка в HAProxy выполняется не на уровне отдельных SQL-запросов, а на уровне TCP-соединений между сервером приложений и балансировщиком.

Это означает следующее:

  • Сервер приложений использует собственный пул соединений с базой данных.

  • При выполнении запроса сервер:

    • либо берёт уже существующее соединение из пула,

    • либо создаёт новое, если свободных нет.

  • В момент создания нового соединения HAProxy выбирает конкретную реплику и закрепляет за этим соединением маршрут.

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

Практические последствия#
  • При работе одного пользователя:

    • высока вероятность, что используется одно и то же соединение из пула;

    • его запросы будут попадать в одну и ту же реплику;

    • равномерного распределения нагрузки между репликами не происходит.

  • При многопользовательской работе:

    • создаётся больше соединений;

    • они распределяются между репликами более равномерно;

    • однако равномерность нагрузки не гарантируется, так как разные соединения могут генерировать разную нагрузку.

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

3. Асинхронные реплики (в разработке)#

Поддержка асинхронных реплик находится в разработке и в текущих версиях Global ERP не поддерживается.

Планируемая архитектура:

  • асинхронных реплик может быть произвольное количество;

  • сервер приложений никогда не использует их автоматически;

  • выбор реплики будет осуществляться на прикладном уровне (интерфейсы, отчёты, печатные формы);

  • управление запросами будет выполняться на уровне бизнес-логики, а не SQL.

Как и в случае синхронных реплик, для асинхронного режима требуется внешний балансировщик нагрузки:

  • балансировщик скрывает конкретные узлы БД от сервера приложений;

  • обеспечивает отказоустойчивость;

  • содержит резервную мастер-ноду для fallback-сценариев.

Использование балансировщика нагрузки является обязательным элементом архитектуры асинхронных реплик.

4. Настройка подключения#

4.1 Настройка Standalone#

Для подключения читающих реплик в режиме standalone используется extraConnectionPools.

В файле global3config.xml необходимо заполнить секцию <databases>.

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

<databases>
        <database alias="<db_alias>" driver="org.postgresql.Driver" schema="PUBLIC"
                connectionType="proxyShared" authenticationType="btk"
                maxActiveThreadCount="1000"
                activeThreadTimeout="20">

                <users>
                        <user name="username" password="password"/>
                </users>

                <extraConnectionPools>
                        <!-- WritePool - мастер-база -->
                        <pool name="WritePool" schema="PUBLIC"
                                url="jdbc:postgresql://<host>:<port>/<dbname>"
                                acceptPrimary="true"
                                acceptSecondary="true"
                                acceptTxSession="true"
                                acceptNoTxSession="false"
                                priority="1"
                                minPoolSize="100"
                                maxPoolSize="110"
                                initialPoolSize="2"
                                inactiveConnectionTimeout="30"
                                poolTimeout="10"
                                timeBetweenEvictionRunsMillis="5000"
                                usageRatio="1" />

                        <!-- ReadPool - внешний балансировщик между репликами -->
                        <pool name="ReadPool" schema="PUBLIC"
                                url="jdbc:postgresql://<host>:<port>/<dbname>"
                                acceptPrimary="true"
                                acceptSecondary="true"
                                acceptTxSession="false"
                                acceptNoTxSession="true"
                                priority="100"
                                minPoolSize="100"
                                maxPoolSize="110"
                                initialPoolSize="2"
                                inactiveConnectionTimeout="30"
                                poolTimeout="2"
                                timeBetweenEvictionRunsMillis="5000"
                                usageRatio="1" />
                </extraConnectionPools>
        </database>
</databases>

4.2 Описание параметров пулов подключения#

Используемые параметры пула (Tomcat JDBC Pool) интерпретируются следующим образом:

  • minPoolSize — минимальное количество свободных соединений;

  • maxPoolSize — максимальное количество соединений в пуле;

  • initialPoolSize — количество соединений при старте сервера;

  • usageRatio — относительный вес пула при распределении нагрузки (является константой и не подлежит изменению);

  • acceptTxSession — принимает соединения внутри транзакций;

  • acceptNoTxSession — принимает соединения вне транзакций;

  • poolTimeout — время ожидания свободного соединения;

  • inactiveConnectionTimeout — таймаут неактивного соединения.

Подробная документация по параметрам пула доступна по ссылке: https://help.globalerp.ru/books/gs-docs-sphinx/master/reference/configuration/global3_config_xsd/configuration/databases/database/extraconnectionpools/Pool.html

4.3 Настройка при использовании кластера Kubernetes (gs-ctk)#

Начиная с версии gs-ctk 6.1.0, поддерживается возможность настроить пулы при помощи параметров GlobalConfiguration.

Примечание

Параметры применяются, только если вы используете profile последней версии. Если вы используете profile, сформированный одной из предыдущих версий gs-ctk, параметры не будут влиять на работу сервера приложений.

Чтобы обновить profile, замените файлы на те, что находятся в папке default/profile в nscli версии 6.1.0 или выше. Либо воспользуйтесь командой ./appkit.sh prepare_profile --appkit-dir workspace/appkit/v2 и добавьте к profile оставшиеся компоненты комплекта приложений.

Не забудьте применить обновленный profile, сделав ./appkit.sh push, ./appkit.sh switch_local/./appkit.sh switch_remote и применив конфигурацию.

Добавьте настройки дополнительных пулов, аналогично настройке для Standalone, используя параметр database-extra-pools:

apiVersion: global-system.ru/v1
kind: GlobalConfiguration
metadata:
  name: config
spec:
  type: advanced
  resgroups:
  - name: gs-cluster-1
    database_url: jdbc:postgresql://<host>:<port>/<dbname>
    database-write-pool-parameters:
      name: WritePool
      schema: PUBLIC
      acceptPrimary: true
      acceptSecondary: true
      acceptTxSession: true
      acceptNoTxSession: false
      priority: 1
      minPoolSize: 100
      maxPoolSize: 110
      initialPoolSize: 2
      inactiveConnectionTimeout: 30
      poolTimeout: 10
      timeBetweenEvictionRunsMillis: 5000
      usageRatio: 1
    database-extra-pools:
      name: ReadPool
      schema: PUBLIC
      url: jdbc:postgresql://<host>:<port>/<dbname>
      acceptPrimary: true
      acceptSecondary: true
      acceptTxSession: false
      acceptNoTxSession: true
      priority: 100
      minPoolSize: 100
      maxPoolSize: 110
      initialPoolSize: 2
      inactiveConnectionTimeout: 30
      poolTimeout: 2
      timeBetweenEvictionRunsMillis: 5000
      usageRatio: 1
    ...

Данная конфигурация полностью соответствует конфигурации для Standalone-версии выше.

Обратите внимание:

  • Для базы данных, указанной через параметр database_url создается пул соединений с параметрами из database-write-pool-parameters (и аналогично для database_readonly_replica_url с database_read_only_pool_parameters). Название для пула будут создано автоматически, но вы можете его переопределить через параметр name, как сделано в примере.

  • Вы можете записывать параметры, как в snake_case, так и в camelCase, то есть min_pool_size и minPoolSize это один и тот же параметр.

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

5. Конфигурация балансировщика нагрузки (HAProxy)#

В качестве балансировщика нагрузки для распределения read-запросов между читающими репликами PostgreSQL вендором рекомендуется использование HAProxy. Балансировщик выполняет следующие задачи:

  • равномерное распределение read-запросов между репликами;

  • автоматическое исключение недоступных или деградировавших реплик;

  • обеспечение отказоустойчивости — fallback на мастер-ноду при отказе всех реплик;

  • прозрачность для сервера приложений (приложение не знает о топологии БД и работает с виртуальными адресами).

Ключевые принципы конфигурации HAProxy#

  • Read-пул указывает на виртуальный адрес балансировщика.

  • Мастер-база присутствует в конфигурации как резервная (backup) для read-пула.

  • Health-check должен проверять доступность СУБД, а не только сетевую связность.

  • При деградации реплика исключается из пула до восстановления.

Логирование HAProxy#

Для логирования HAProxy необходимо установить rsyslog

sudo apt install -y rsyslog

После создать файл конфигурации /etc/rsyslog.d/haproxy.conf

С содержимым:

# Collect log with UDP
$ModLoad imudp
$UDPServerAddress 127.0.0.1
$UDPServerRun 514

# Creating separate log files based on the severity
local0.* /var/log/haproxy-traffic.log
local0.notice /var/log/haproxy-admin.log

Перезапустить сервис rsyslog

В конфигурации HAProxy прописан

log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"

R

Переменная

Описание

Тип

%o

специальная переменная, применить флаги ко всем следующим переменным

%B

прочитано байт (от сервера клиенту)

числовой

H

%CC

захваченный cookie запроса

строка

H

%CS

захваченный cookie ответа

строка

%H

имя хоста

строка

H

%HM

метод HTTP (например, POST)

строка

H

%HP

URI HTTP-запроса без строки запроса (путь)

строка

H

%HQ

строка запроса URI HTTP-запроса (например, ?bar=baz)

строка

H

%HU

URI HTTP-запроса (например, /foo?bar=baz)

строка

H

%HV

версия HTTP (например, HTTP/1.0)

строка

%ID

уникальный идентификатор

строка

%ST

код состояния

числовой

%T

дата/время по Гринвичу (GMT)

дата

%Ta

активное время запроса (от TR до конца)

числовой

%Tc

Tc

числовой

%Td

Td = Tt - (Tq + Tw + Tc + Tr)

числовой

%Tl

местное дата/время

дата

%Th

время рукопожатия соединения (SSL, протокол PROXY)

числовой

H

%Ti

время простоя перед HTTP-запросом

числовой

H

%Tq

Th + Ti + TR

числовой

H

%TR

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

числовой

H

%Tr

Tr (время ответа)

числовой

%Ts

временная метка (timestamp)

числовой

%Tt

Tt

числовой

%Tw

Tw

числовой

%U

отправлено байт (от клиента серверу)

числовой

%ac

текущие соединения (actconn)

числовой

%b

имя бэкенда

строка

%bc

beconn (конкурентные соединения бэкенда)

числовой

%bi

IP-адрес источника бэкенда (адрес подключения)

IP

%bp

порт источника бэкенда (адрес подключения)

числовой

%bq

очередь бэкенда

числовой

%ci

IP-адрес клиента (принятый адрес)

IP

%cp

порт клиента (принятый адрес)

числовой

%f

имя фронтенда

строка

%fc

feconn (конкурентные соединения фронтенда)

числовой

%fi

IP-адрес фронтенда (принимающий адрес)

IP

%fp

порт фронтенда (принимающий адрес)

числовой

%ft

имя транспорта фронтенда (суффикс „~“ для SSL)

строка

%lc

счётчик лога фронтенда

числовой

%hr

захваченные заголовки запроса, стиль по умолчанию

строка

%hrl

захваченные заголовки запроса, стиль CLF

список строк

%hs

захваченные заголовки ответа, стиль по умолчанию

строка

%hsl

захваченные заголовки ответа, стиль CLF

список строк

%ms

миллисекунды даты принятия (дополненные нулями слева)

числовой

%pid

PID

числовой

H

%r

HTTP-запрос

строка

%rc

повторные попытки

числовой

%rt

счётчик запросов (HTTP-запрос или TCP-сессия)

числовой

%s

имя сервера

строка

%sc

srv_conn (конкурентные соединения сервера)

числовой

%si

IP-адрес сервера (целевой адрес)

IP

%sp

порт сервера (целевой адрес)

числовой

%sq

очередь сервера

числовой

S

%sslc

шифры SSL (например, AES-SHA)

строка

S

%sslv

версия SSL (например, TLSv1)

строка

%t

дата/время (с миллисекундным разрешением)

дата

H

%tr

дата/время HTTP-запроса

дата

H

%trg

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

дата

H

%trl

местное дата/время начала HTTP-запроса

дата

%ts

состояние завершения

строка

H

%tsc

состояние завершения со статусом cookie

строка

R = Restrictions : H = mode http only ; S = SSL only

Пример конфигурации HAProxy#

Конфигурационный файл HAProxy (обычно располагается в /etc/haproxy/haproxy.cfg):

global
    log 127.0.0.1:514 local0
    chroot /var/lib/haproxy
    maxconn 6000
    user _haproxy
    group _haproxy
    daemon

defaults
    log global
    mode tcp
    option tcplog
    option dontlognull
    timeout connect 10s
    timeout client  24h
    timeout server  600s
    option tcpka
    retries 3

# Статистика и администрирование HAProxy
frontend stats
    mode http
    bind :8404
    stats enable
    stats refresh 30s
    stats uri /stats
    stats show-modules
    stats admin if TRUE
    log-format "%ci:%cp [%t] %ft %b/%s %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"

# Фронтенд для запросов на запись (обычно идут на мастер)
frontend db_rw_frontend
    bind *:6434
    mode tcp
    log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
    default_backend db_rw_backend

# Фронтенд для запросов на чтение (распределяются по репликам)
frontend db_ro_frontend
    bind *:6433
    mode tcp
    log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
    default_backend db_ro_backend

# Бэкенд для записи — только мастера (обычно два мастера в режиме активный/пассивный)
backend db_rw_backend
    mode tcp
    balance roundrobin
    option tcp-check
    server v00011 <IP_мастера_1>:6432 check agent-check agent-port 9001 inter 10s fall 2 rise 2
    server v00012 <IP_мастера_2>:6432 check agent-check agent-port 9001 inter 10s fall 2 rise 2

# Бэкенд для чтения — реплики + мастера как backup
backend db_ro_backend
    mode tcp
    balance roundrobin
    option tcp-check
    server v0013 <IP_реплики_1>:6432 check agent-check agent-port 9001 inter 10s fall 2 rise 2
    server v0014 <IP_реплики_2>:6432 check agent-check agent-port 9001 inter 10s fall 2 rise 2
    server v0015 <IP_реплики_3>:6432 check agent-check agent-port 9001 inter 10s fall 2 rise 2
    server v00011 <IP_мастера_1>:6432 backup check agent-check agent-port 9001 inter 10s fall 2 rise 2
    server v00012 <IP_мастера_2>:6432 backup check agent-check agent-port 9001 inter 10s fall 2 rise 2

Пояснения:

  • Порт 6432 — типичный порт для подключения к PostgreSQL через пулеры (например, PgBouncer). В данном случае предполагается, что PostgreSQL слушает этот порт напрямую или через промежуточный слой.

  • Директива agent-check и agent-port 9001 включает использование внешнего агента для более интеллектуальной проверки состояния сервера (например, определение роли: мастер или реплика, отставание реплики).

  • Параметры inter 10s fall 2 rise 2 задают интервал проверок и количество успешных/неудачных попыток для перевода сервера в статус доступен/недоступен.

  • В бэкенде db_ro_backend мастера указаны как backup, поэтому они будут использоваться только в случае, если ни одна реплика не доступна.

Агенты проверки состояния PostgreSQL#

Для корректной работы agent-check необходимо развернуть на каждом сервере PostgreSQL скрипты-агенты, которые через сокет или порт сообщают HAProxy статус сервера (up, down, maint и т.д.). Агент запускается как systemd-сервис под управлением socket-activated.

Скрипт для мастера (потенциальная мастер-нода)#

Разместите на сервере, который может быть мастером, скрипт /usr/local/bin/pg_master_agent.sh:

#!/bin/bash
set -uo pipefail

export PGHOST=127.0.0.1
export PGPORT=5432
export PGUSER=postgres
export PGDATABASE=postgres

# Ожидание входных данных от HAProxy (по протоколу agent-check)
read -t 1 || true

# Проверка, является ли текущий сервер мастером (не в recovery)
IS_PRIMARY=$(psql -Atq -c "SELECT NOT pg_is_in_recovery();")

if [[ "$IS_PRIMARY" == "t" ]]; then
    echo "up"
else
    echo "down"
fi

Скрипт для реплики (слейв-нода)#

На репликах используйте скрипт /usr/local/bin/pg_replica_agent.sh (пример с проверкой отставания):

#!/bin/bash
set -uo pipefail

export PGHOST=127.0.0.1
export PGPORT=5432
export PGUSER=postgres
export PGDATABASE=postgres

MAX_LAG=100  # максимально допустимое отставание в секундах

read -t 1 || true

# Получаем информацию о recovery и текущем отставании
RESULT=$(psql -Atq 2>/dev/null <<'SQL'
SELECT
  pg_is_in_recovery(),
  CASE WHEN pg_is_in_recovery() THEN EXTRACT(EPOCH FROM now() - pg_last_xact_replay_timestamp()) ELSE 0 END AS lag;
SQL
)

if [[ -z "$RESULT" ]]; then
    echo "down"
    exit 0
fi

IS_RECOVERY="${RESULT%%|*}"
LAG="${RESULT##*|}"

# Если сервер не в recovery (то есть мастер) — down, так как для read-пула нужны реплики
if [[ "$IS_RECOVERY" != "t" ]]; then
    echo "down"
    exit 0
fi

# Проверка отставания (lag может быть пустым или дробным)
if (( $(echo "$LAG > $MAX_LAG" | bc -l 2>/dev/null) )); then
    echo "down"
    exit 0
fi

echo "up"

Примечание: если отставание не критично, проверку $LAG можно опустить. При отсутствии bc можно заменить сравнение на целочисленное: if [ ${LAG%.*} -gt $MAX_LAG ]; then.

Оба скрипта должны быть исполняемыми:

chmod +x /usr/local/bin/pg_master_agent.sh
chmod +x /usr/local/bin/pg_replica_agent.sh

Настройка systemd для запуска агентов#

Для каждого экземпляра агента требуется сокет-активация. Создайте следующие файлы.

Важно

Название сокета и сервиса должны совпадать!

Файл сокета /etc/systemd/system/pg-agent.socket#

[Unit]
Description=PostgreSQL HAProxy Agent Socket

[Socket]
ListenStream=9001
Accept=yes
MaxConnections=16

[Install]
WantedBy=sockets.target

Файл сервиса (шаблон) /etc/systemd/system/pg-agent@.service#

Этот шаблон будет запускать соответствующий скрипт в зависимости от роли сервера. Для мастера:

[Unit]
Description=PostgreSQL HAProxy Agent Service (Master)

[Service]
User=postgres
Group=postgres
ExecStart=/usr/local/bin/pg_master_agent.sh
StandardInput=socket
StandardOutput=socket

Для реплики измените путь к скрипту (или создайте отдельный шаблон, например pg-agent-replica@.service). Проще всего использовать один шаблон и симлинки, либо разные файлы сервисов. Ниже пример для реплики:

[Unit]
Description=PostgreSQL HAProxy Agent Service (Replica)

[Service]
User=postgres
Group=postgres
ExecStart=/usr/local/bin/pg_replica_agent.sh
StandardInput=socket
StandardOutput=socket

Сохраните его как /etc/systemd/system/pg-agent-replica@.service.

После создания файлов выполните:

systemctl daemon-reload
systemctl enable --now pg-agent.socket

Сокет будет слушать порт 9001 и при подключении запускать соответствующий сервис (экземпляр с номером, совпадающим с номером сокета, если используется шаблон). Поскольку используется Accept=yes, каждый входящий запрос создаёт отдельный экземпляр сервиса с уникальным идентификатором.

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