Асинхронный пул соединений QCP

Асинхронный пул соединений (Quantum Connection Pool, QCP) обеспечивает подключение и поддержку связи с удаленными клиентами.



Общий принцип работы

QCP принимает входящие подключения от удаленных клиентов по адресу, указанному в listening_address (по умолчанию — 0.0.0.0:8080), и перенаправляет их трафик серверам баз данных, перечисленных в разделе servers. Соединения с серверами устанавливаются (и завершаются) автоматически, по мере необходимости.

В зависимости от режима работы, задаваемого параметром relay_mode, перенаправление трафика данных от клиентов к серверам осуществляется одним из следующих способов:

  1. relay_mode: Session: при первом обращении к серверу клиенту выделяется уникальное соединение с базой данных, которое возвращается в пул только при отключении клиента.
  2. 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.

Процедура выполняет четыре задачи:

  1. участвует в выборе текущего лидера qcp;
  2. управляет временем жизни аварийного режима;
  3. собирает и возвращает данные gossip от узлов qcp;
  4. возвращает локальное состояние узла qhb: lsn и источник репликации.

Реализация делится на независимые блоки. Все состояние хранится только в памяти процесса и защищается общим взаимным исключением (мьютексом).


API

Таблица 1. Входные параметры

ПараметрТипОписание
qcp_nametextИмя узла qcp, который вызывает процедуру.
termbigintНомер выборов / логическое время.
timeoutintТаймаут в миллисекундах. Используется для срока жизни лидерства и записи gossip.
participate_in_electionsboolУправляет участием вызывающего узла qcp в выборах.
accident_mode_timeoutintТаймаут аварийного режима в миллисекундах. Значение больше 0 включает или продлевает режим.
gossipbyteaБинарные данные, которые сохраняются для qcp_name и возвращаются другим вызывающим.

Таблица 2. Выходные поля

Процедура возвращает одну строку.

ПолеТипОписание
qcp_leadertext not nullТекущий лидер qcp по мнению данного узла qhb. Пустая строка означает отсутствие лидера.
qcp_leader_expired_atint not nullВременная метка Unix (в миллисекундах), когда истекают полномочия текущего лидера.
termbigint not nullТекущий сохраненный номер выборов.
accident_mode_expired_atint not nullВременная метка Unix (в миллисекундах), когда истекает аварийный режим.
lsnbigint not nullТекущий lsn базы данных.
qhb_replication_sourcetext not nullhost:port мастера, с которого реплицируется текущий узел qhb. Для мастера возвращается пустая строка.
gossip_namestext[]Имена узлов qcp, для которых есть актуальные записи gossip.
gossip_valuesbytea[]Значения 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 участвует в выборах.

Алгоритм:

  1. Если qcp_leader — пустая строка, процедура запоминает нового лидера:

    qcp_leader = qcp_name
    current_term = term
    qcp_leader_expired_at = now() + timeout
    
  2. Если qcp_leader == qcp_name, процедура продлевает срок лидерства:

    qcp_leader_expired_at = now() + timeout
    
  3. Если term > current_term, процедура принимает новый term и запоминает нового лидера:

    qcp_leader = qcp_name
    current_term = term
    qcp_leader_expired_at = now() + timeout
    
  4. Если qcp_leader_expired_at истек, процедура запоминает нового лидера:

    qcp_leader = qcp_name
    current_term = term
    qcp_leader_expired_at = now() + timeout
    

Если participate_in_elections = false, то вызывающий узел qcp не пытается стать лидером.

Алгоритм:

  1. Если текущий лидер совпадает с qcp_name, лидерство сбрасывается:

    qcp_leader = ""
    current_term = 0
    qcp_leader_expired_at = 0
    
  2. Если 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.