Маршрутизация запросов чтения на реплики#
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, каждый входящий запрос создаёт отдельный экземпляр сервиса с уникальным идентификатором.
Важно: на мастер-сервере должен быть запущен сервис, использующий скрипт мастера; на репликах — скрипт реплики. Для этого можно либо скопировать шаблон с разными именами, либо воспользоваться переменными окружения в скрипте, определяющими роль автоматически.