Асинхронный пул соединений QCP
Асинхронный пул соединений (Quantum Connection Pool, QCP) обеспечивает подключение и поддержку связи с удаленными клиентами.
Общий принцип работы
QCP принимает входящие подключения от удаленных клиентов по адресу, указанному в listening_address (по умолчанию — 0.0.0.0:8080), и перенаправляет их трафик серверам баз данных, перечисленных в разделе servers. Соединения с серверами устанавливаются (и завершаются) автоматически, по мере необходимости.
В зависимости от режима работы, задаваемого параметром relay_mode, перенаправление трафика данных от клиентов к серверам осуществляется одним из следующих способов:
- relay_mode: Session: при первом обращении к серверу клиенту выделяется уникальное соединение с базой данных, которое возвращается в пул только при отключении клиента.
- relay_mode: Smart: при обращении к серверу клиенту выделяется уникальное соединение с базой данных, которое возвращается в пул тогда и только тогда, когда в ответе от сервера будет стоять флаг Idle (см. раздел Форматы сообщений, сообщение ReadyForQuery).
С полным описанием настроек можно ознакомиться в примере конфигурационного файла qcp/config-example.yaml.
Ограничения по использованию QCP
Применение QCP в режиме Smart может вызвать проблемы при использовании следующих функций:
- настройка/сброс до начальных установок;
- прослушивание;
- курсоры WITH HOLD;
- подготовка/высвобождение памяти;
- сохранение/удаление строк во временных таблицах;
- загрузка операторов;
- рекомендательные блокировки уровня сеанса.
Запуск и работа
Запуск осуществляется с помощью утилиты qcp (qcp --help для списка
параметров), остановка — утилитой qcp-ctrl (qcp-ctrl --help для списка
параметров). Например, чтобы остановить запущенный экземпляр QCP, необходимо
выполнить команду qcp-ctrl quit.
Вывод записей журнала в процессе работы QCP контролируется параметром log_output (см. qcp/config-example.yaml), при этом уровень протоколирования задается параметром log_level (подробнее про уровни протоколирования см. на справочной странице LevelFilter на соответствующем сайте). Например, чтобы выводить записи журнала уровня Info и выше в файл /tmp/qcp.log, необходимо задать следующие параметры в файле конфигурации:
log_level: Info
log_output:
file: /tmp/qcp.log
Потребление памяти
При запуске программы единовременно выделяется количество памяти, указанное в разделе arena файла конфигурации:
# Конфигурация памяти (необязательно)
arena:
chunk_size: 65 KB # Размер одного блока памяти; поддерживаются суффиксы B, KB, MB, GB
# Общее количество потребляемой памяти можно указать, либо используя параметр
# «количество блоков памяти»:
chunks_count: 3150 # Количество таких блоков
# либо указав общее количество напрямую:
total_size: 3.1 GB # Поддерживаются суффиксы B, KB, MB, GB
Подключение к СУБД
QCP может подключаться либо к одиночному серверу СУБД, либо к кластеру СУБД, управляемому Patroni.
Для подключения к серверу СУБД в разделе server необходимо указать его адрес, например:
server:
# Одиночный сервер
address: # Тип и адрес подключения (TCP или сокет домена Unix)
Tcp: "127.0.0.1:1234"
# Unix: "/home/mexus/devel/optim/qhb-with-rust/build/dbsockets/.s.PGSQL.5432"
Для подключения к кластеру СУБД, управляемому Patroni:
server:
# Кластер Patroni
patroni:
# Сервера (узлы) из кластера
nodes:
- dbms_address: # Адрес СУБД
Tcp: "127.0.0.1:1234"
rest_url: "http://127.0.0.1:8090" # URL REST API patroni
- dbms_address: # Адрес СУБД
Unix: "/путь/к/сокету/unix"
rest_url: "http://127.0.0.2:8090" # URL REST API patroni
instance_name: my_cluster # Имя кластера (используется как префикс для метрик).
single_try_timeout: 5 sec # Необязательный параметр: максимальное ограничение времени на
# одну попытку узнать, какой из серверов кластера мастер.
master_check_interval: 1500 ms # Необязательный параметр: как часто
# опрашивать кластер. По умолчанию — 1500
# миллисекунд.
Необходимо перечислить список всех узлов кластера, указывая адрес СУБД и адрес REST API Patroni для каждого узла. Так же необходимо задать имя кластера с помощью параметра instance_name.
При таком подключении QCP будет регулярно опрашивать Patroni на всех узлах с целью определения текущего мастера. Если мастер изменяется, то QCP прерывает все текущие запросы от подключенных клиентов и подключается к новому мастеру.
Также можно указать дополнительные параметры:
- single_try_timeout: ограничение по времени на каждый REST запрос,
- master_check_interval: как часто опрашивать кластер с целью определения текущего мастера.
Независимо от того, осуществляется ли подключение к кластеру или к одной СУБД, для описания подключений используются следующие необязательные параметры:
- inactive_timeout: временной интервал, после которого неактивное соединение с сервером будет закрыто при условии наличия минимального количества подключений.
- lifetime: примерное максимальное время жизни соединения к серверу, по истечении которого оно будет закрыто.
- heart_beat_interval: интервал проверки допустимости соединения с сервером в период, когда подключение не ассоциировано ни с одним клиентом.
Кластерный режим с QHB Agent
QCP поддерживает управление кластером QHB через агент qhb-agent. В этом режиме QCP отслеживает состояние узлов кластера и отдает команды qhb-agent для переключения узлов QHB в режим реплики. Таким образом, QCP выступает координатором кластера, а qhb-agent — исполнителем команд на каждом узле.
qhb-agent устанавливается на каждый узел кластера QHB из репозитория Quantom. После установки необходимо создать конфигурационный файл /etc/qhb-agent/config.yaml:
listen: <ip-адрес-узла>:<порт> # Адрес, на котором агент принимает команды от QCP
qhb:
path: /usr/local/qhb # Путь к бинарным файлам QHB
port: 5432 # Порт QHB
pgdata: /qhb/data # Каталог данных QHB
replica-user: <пользователь> # Пользователь для репликации
replica-pwd: <пароль>
admin-user: <пользователь> # Административный пользователь
db-name: <имя БД>
application-name: <имя узла> # Уникальное имя узла в кластере
Запуск и управление осуществляются через systemd:
systemctl enable --now qhb-agent
Проверка работоспособности агента — через HTTP API:
curl http://<адрес>:<порт>/health
Для подключения QCP к кластеру через qhb-agent в разделе server используется секция qluster:
server:
qluster:
cluster-name: qluster # Имя кластера
name: <имя-этого-узла-qcp> # Идентификатор QCP в кластере
readonly-statements-poll-period: 15s
api-listen: 0.0.0.0:<api-порт> # Порт API самого QCP
qhb:
db-user: <пользователь>
db-name: <имя БД>
qhb-timeout: 2s # Таймаут операций с QHB
connection-lifetime: 1m # Время жизни соединения
agent-timeout: 30s # Таймаут команд к qhb-agent
promote-wait: 1s # Ожидание после повышения
raft:
vote-timeout: 12s
accident-mode-timeout: 20s
max-delay: 30s
healthcheck-interval: 5s
servers:
- address:
Tcp: "<адрес-узла-1>:<порт>"
- address:
Tcp: "<адрес-узла-2>:<порт>"
В секции servers перечисляются все узлы кластера QHB. QCP самостоятельно определяет текущий мастер и направляет клиентские соединения на него. При смене мастера QCP прерывает текущие соединения, переключает затронутые узлы в режим реплики через qhb-agent и подключается к новому мастеру.
Секция raft задает параметры консенсусного протокола, используемого для координации между узлами кластера:
- vote-timeout — таймаут голосования при выборе лидера;
- accident-mode-timeout — время ожидания перед входом в аварийный режим;
- max-delay — максимальная задержка операций репликации;
- healthcheck-interval — интервал проверки состояния узлов.
Секция connections при использовании qluster задается так же, как и в других режимах (см. раздел «Многопользовательский режим» ниже).
Процедура периодической проверки состояния кластера
Назначение
qcp_healthcheck — хранимая процедура на стороне qhb, которую периодически вызывает узел qcp.
Процедура выполняет четыре задачи:
- участвует в выборе текущего лидера qcp;
- управляет временем жизни аварийного режима;
- собирает и возвращает данные gossip от узлов qcp;
- возвращает локальное состояние узла qhb: lsn и источник репликации.
Реализация делится на независимые блоки. Все состояние хранится только в памяти процесса и защищается общим взаимным исключением (мьютексом).
API
Таблица 1. Входные параметры
| Параметр | Тип | Описание |
|---|---|---|
| qcp_name | text | Имя узла qcp, который вызывает процедуру. |
| term | bigint | Номер выборов / логическое время. |
| timeout | int | Таймаут в миллисекундах. Используется для срока жизни лидерства и записи gossip. |
| participate_in_elections | bool | Управляет участием вызывающего узла qcp в выборах. |
| accident_mode_timeout | int | Таймаут аварийного режима в миллисекундах. Значение больше 0 включает или продлевает режим. |
| gossip | bytea | Бинарные данные, которые сохраняются для qcp_name и возвращаются другим вызывающим. |
Таблица 2. Выходные поля
Процедура возвращает одну строку.
| Поле | Тип | Описание |
|---|---|---|
| qcp_leader | text not null | Текущий лидер qcp по мнению данного узла qhb. Пустая строка означает отсутствие лидера. |
| qcp_leader_expired_at | int not null | Временная метка Unix (в миллисекундах), когда истекают полномочия текущего лидера. |
| term | bigint not null | Текущий сохраненный номер выборов. |
| accident_mode_expired_at | int not null | Временная метка Unix (в миллисекундах), когда истекает аварийный режим. |
| lsn | bigint not null | Текущий lsn базы данных. |
| qhb_replication_source | text not null | host:port мастера, с которого реплицируется текущий узел qhb. Для мастера возвращается пустая строка. |
| gossip_names | text[] | Имена узлов qcp, для которых есть актуальные записи gossip. |
| gossip_values | bytea[] | Значения gossip. Индекс соответствует тому же индексу в gossip_names. |
Внутреннее состояние
Таблица 3. Выборы лидера qcp
| Поле | Описание |
|---|---|
| qcp_leader | Текущий лидер qcp. По умолчанию пустая строка. |
| qcp_leader_expired_at | Временная метка Unix (в миллисекундах), когда закончатся полномочия текущего лидера. |
| current_term | Номер выборов. По умолчанию 0. |
Таблица 4. Аварийный режим
| Поле | Описание |
|---|---|
| accident_mode_expired_at | Временная метка Unix (в миллисекундах), до которой действует аварийный режим. |
Gossip
Состояние хранится в хеш-таблице:
qcp_name -> (gossip, expired_at)
где:
- qcp_name — имя узла qcp;
- gossip — бинарные данные из входного параметра;
- expired_at — Временная метка Unix (в миллисекундах), после которой запись считается устаревшей.
Логика работы
Выборы лидера qcp
Если participate_in_elections = true, то вызывающий узел qcp участвует в выборах.
Алгоритм:
-
Если qcp_leader — пустая строка, процедура запоминает нового лидера:
qcp_leader = qcp_name current_term = term qcp_leader_expired_at = now() + timeout -
Если qcp_leader == qcp_name, процедура продлевает срок лидерства:
qcp_leader_expired_at = now() + timeout -
Если term > current_term, процедура принимает новый term и запоминает нового лидера:
qcp_leader = qcp_name current_term = term qcp_leader_expired_at = now() + timeout -
Если qcp_leader_expired_at истек, процедура запоминает нового лидера:
qcp_leader = qcp_name current_term = term qcp_leader_expired_at = now() + timeout
Если participate_in_elections = false, то вызывающий узел qcp не пытается стать лидером.
Алгоритм:
-
Если текущий лидер совпадает с qcp_name, лидерство сбрасывается:
qcp_leader = "" current_term = 0 qcp_leader_expired_at = 0 -
Если qcp_leader_expired_at истек, лидерство также сбрасывается:
qcp_leader = "" current_term = 0 qcp_leader_expired_at = 0
Аварийный режим
Состояние:
accident_mode_expired_at
Если на вход передан accident_mode_timeout > 0, процедура устанавливает:
accident_mode_expired_at = now() + accident_mode_timeout
Сбор данных gossip по узлам qcp
Если qcp_name != "", процедура сохраняет переданный gossip в хеш-таблице:
gossip_map[qcp_name] = (gossip, now() + timeout)
После этого процедура проходит по всем значениям хеш-таблицы и удаляет записи, у которых истек expired_at.
На выходе актуальные записи возвращаются двумя массивами:
gossip_names = [qcp_name_1, qcp_name_2, ...]
gossip_values = [gossip_1, gossip_2, ...]
gossip_names[i] соответствует gossip_values[i].
Возврат lsn и источника репликации
Процедура возвращает текущий lsn базы данных.
Если текущий узел qhb является репликой, в qhb_replication_source возвращается host:port мастера, с которого он реплицируется.
Если текущий узел qhb не является репликой, qhb_replication_source возвращается как пустая строка.
Многопользовательский режим
QCP поддерживает возможность подключения к кластеру СУБД, используя различные пары пользователь-БД.
Для каждой из таких пар создается отдельный пул подключений. Когда клиент подключается к QCP, он указывает логин пользователя и имя БД, и, в соответствии с этими параметрами, QCP помещает его в нужный пул.
При попытке подключения к QCP с парой пользователь-БД, которая отсутствует в конфигурации, клиент получит ошибку:
FATAL: Unknown user name, database name, or invalid combination of user and database
-- КРИТИЧНО: Неизвестное имя пользователя, имя базы данных или недопустимое сочетание пользователя и базы данных
При этом в логах QCP появится примерно следующее сообщение:
[qcp::relay_helper] [WARN] Client ... startup sequence failed: Unknown user-database pair: user "user", db "db"
-- (сбой в последовательности запуска: неизвестная пара пользователь-БД:
-- пользователь "user", БД "db")
Пары пользователь-БД перечисляются в секции connections раздела server, например:
# Первый пул подключений.
- username: user # Имя пользователя
password: something # Необязательный параметр: пароль для аутентификации
database: utilities # Необязательный параметр: имя базы данных.
# Если не указана, в качестве имени базы будет
# использоваться имя пользователя.
backends: # Количество подключений с данным именем пользователя и базой
initial: 1 # .. при старте пула
min: 1 # .. минимальное
max: 3 # .. максимальное
# Необязательная секция. Описывает то, как клиент должен быть
# аутентифицирован на QCP. Если секция не задана, то для авторизации
# пользователя с данным именем пользователя для данной базы на QCP будет
# использоваться тот же пароль, какой задан для сервера, если он задан.
client_auth:
# Метод аутентификации: trust или md5.
method: md5
# Пароль для авторизации. Если не задан, будет использоваться тот
# же, что и для подключения QCP к базе.
password: lol2
options: # Необязательный параметр: дополнительные параметры подключения,
# передаваемые базе данных
option_name: option_value
# Второй пул подключений.
- username: qhb # Имя пользователя
backends: # Количество подключений с данным именем пользователя и базой
initial: 2 # .. при старте пула
min: 2 # .. минимальное
max: 5 # .. максимальное
Для описания каждого пула подключений для пары пользователь-БД требуется указать имя пользователя (username) и имя БД (database). Поле database можно пропустить, тогда оно примет то же значение, что и username.
Помимо этого необходимо указать ограничения на количество подключений к СУБД в данном пуле, в секции backends:
- initial указывает, сколько будет открыто подключений к СУБД с данной парой пользователь-БД при старте QCP.
- min — ограничение на минимальное количество подключений к СУБД, может быть меньше, чем initial.
- max — максимальное количество подключений к СУБД. Должно быть не меньше, чем initial и min.
Если для подключения к СУБД с данными пользователем и БД требуется пароль, то его можно указать в поле password.
Также доступна секция options, позволяющая передать произвольные аргументы командной строки для сервера в виде имя: значение.
Авторизация клиентов на QCP
По умолчанию для каждой пары пользователь-БД для авторизации пользователей QCP использует такие же настройки, как и для авторизации QCP на СУБД: если СУБД требует пароль, то и QCP будет требовать от пользователя его же, если СУБД не требует пароль, то и QCP не будет.
Данное поведение можно переопределить, используя секцию client_auth:
- поле method позволяет переопределить метод авторизации,
- поле password позволяет задать пароль, отличный от пароля, используемого для подключения к СУБД.
Применение параметров без перезапуска QCP
Некоторые параметры конфигурации можно изменить, не перезапуская QCP. Такой способ называется «перезагрузка файла конфигурации».
Для этого необходимо изменить требуемые параметры в конфигурационном файле, после чего выполнить
sudo systemctl reload qcp
Если QCP работает не под управлением systemd, то необходимо определить идентификатор процесса QCP (обратитесь к системному администратору, чтобы узнать, как это сделать на вашей ОС), после чего выполнить следующую команду, подставляя найденный идентификатор вместо <pid>:
sudo kill -USR1 <pid>
Для того чтобы обновленная конфигурация применилась, необходимо, чтобы конфигурационный файл был допустимым. В противном случае в журнале QCP отобразится невозможность применения обновленной конфигурации, и QCP продолжит работать в прежнем режиме. Частичное применение конфигурации в таком случае исключено.
Если применение возможно, в журнале появится соответствующая запись:
[INFO] Reloaded configuration file /путь/к/config.yaml
-- [ИНФОРМАЦИЯ] перезагружен файл конфигурации /путь/к/config.yaml
Обратите внимание, что для применения обновленной конфигурации требуется некоторое время, которое может достигать нескольких секунд при высокой нагрузке.
Применение параметров подключения для каждой пары пользователь-БД сопровождается записями в журнале:
[INFO] Updating connection configuration for user "qhb", db "qhb"
[INFO] Applied new connection parameters for user "qhb", db "qhb"
-- [ИНФОРМАЦИЯ] Обновление конфигурации подключения для пользователя "qhb", БД "qhb"
-- [ИНФОРМАЦИЯ] Добавлены новые параметры подключения для пользователя "qhb", БД "qhb"
Применение параметров входящих подключений сопровождается записью в журнале:
[INFO] Incoming parameters updated
-- [ИНФОРМАЦИЯ] Обновлены параметры входящих подключений
Изменение следующих параметров не может быть осуществлено без перезапуска QCP, поэтому они будут проигнорированы QCP при перезагрузке файла конфигурации:
- arena
- log_level
- log_output
- log_colouring
- log_timestamps
- worker_threads
Следующие параметры будут обновлены во время следующего их использования:
- accept_clients_no_backends: параметр будет обновлен при подключении следующего клиента, либо же когда появится доступное подключение к СУБД, если прием новых подключений от клиентов был приостановлен.
- в секции servers_retry_back_off параметры min_wait, max_wait и max_wait_at будут обновлены при следующей попытке установить подключение к СУБД.
- в секции server параметры inactive_timeout, lifetime, check_interval и heart_beat_interval будут применены к подключениям к СУБД, установленным после перезагрузки файла конфигурации.
Изменение типа подключения к СУБД (одиночный сервер, кластер под управлением Patroni) приведет к отключению всех текущих клиентов и разрыву всех текущих подключений к СУБД.
Удаление подключения из секции connections раздела server приведет к отключению всех клиентов, использующих такие подключения, а также к разрыву всех таких подключений к СУБД.
Изменение имени пользователя (username), имени БД (database), пароля (password) или параметров командной строки сервера (options) у подключения приведет к отключению всех клиентов, использующих такие подключения, а также к разрыву всех таких подключений к СУБД и последующему переподключению к СУБД с обновленными параметрами.
Изменение поля client_auth у подключения будет применено для всех последующих подключений к QCP.