31.5. Режим конвейера#

31.5. Режим конвейера

31.5. Режим конвейера #

libpq режим конвейера позволяет приложениям отправлять запрос без необходимости читать результат предыдущего отправленного запроса. Используя режим конвейера, клиент будет меньше ждать сервера, так как несколько запросов/результатов могут быть отправлены/получены в одной сетевой транзакции.

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

Режим конвейера обычно также потребляет больше памяти как на клиенте, так и на сервере, хотя грамотное и строгое управление очередью отправки/приема может контролировать потребление памяти. Это относится как к блокирующему, так и к неблокирующему режиму соединения.

В то время как конвейерный API libpq был введен в Tantor SE 14, это клиентская функция, которая не требует специальной поддержки сервера и работает на любом сервере, поддерживающем расширенный протокол запросов v3. Дополнительную информацию см. в разделе Раздел 53.2.4.

31.5.1. Использование режима конвейера #

Чтобы использовать конвейеры, приложение должно переключить соединение в режим конвейера, что выполняется с помощью PQenterPipelineMode. PQpipelineStatus может быть использован для проверки, активен ли режим конвейера. В режиме конвейера разрешены только асинхронные операции, которые используют расширенный протокол запросов, строки команд, содержащие несколько SQL-команд, запрещены, как и COPY. Использование функций синхронного выполнения команд, таких как PQfn, PQexec, PQexecParams, PQprepare, PQexecPrepared, PQdescribePrepared, PQdescribePortal, PQclosePrepared, PQclosePortal, является ошибочным условием. PQsendQuery также запрещен, так как он использует простой протокол запросов. Как только все отправленные команды обработаны, и конечный результат конвейера получен, приложение может вернуться в режим без конвейера с помощью PQexitPipelineMode.

Примечание

Лучше всего использовать режим конвейера с libpq в неблокирующем режиме. Если используется в блокирующем режиме, возможна блокировка клиента/сервера. [15]

31.5.1.1. Выполнение запросов #

После входа в режим конвейера приложение отправляет запросы, используя PQsendQueryParams или его аналог для подготовленных запросов PQsendQueryPrepared. Эти запросы ставятся в очередь на стороне клиента до тех пор, пока не будут отправлены на сервер; это происходит, когда используется PQpipelineSync для установления точки синхронизации в конвейере, или когда вызывается PQflush. Функции PQsendPrepare, PQsendDescribePrepared, PQsendDescribePortal, PQsendClosePrepared, и PQsendClosePortal также работают в режиме конвейера. Обработка результатов описана ниже.

Сервер выполняет инструкции и возвращает результаты в том порядке, в котором клиент их отправляет. Сервер начнет выполнять команды в конвейере немедленно, не дожидаясь конца конвейера. Обратите внимание, что результаты буферизуются на стороне сервера; сервер очищает этот буфер, когда устанавливается точка синхронизации с помощью либо PQpipelineSync, либо PQsendPipelineSync, или когда вызывается PQsendFlushRequest. Если какая-либо инструкция вызывает ошибку, сервер прерывает текущую транзакцию и не выполняет никакую последующую команду в очереди до следующей точки синхронизации; для каждой такой команды создается результат PGRES_PIPELINE_ABORTED. (Это остается верным, даже если команды в конвейере откатят транзакцию.) Обработка запросов возобновляется после точки синхронизации.

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

31.5.1.2. Результаты обработки #

Для обработки результата одного запроса в конвейере приложение вызывает функцию PQgetResult повторно и обрабатывает каждый результат, пока PQgetResult не вернет null. Результат следующего запроса в конвейере может быть получен снова с помощью функции PQgetResult, и цикл повторяется. Приложение обрабатывает отдельные результаты запросов как обычно. Когда результаты всех запросов в конвейере были получены, PQgetResult возвращает результат, содержащий значение статуса PGRES_PIPELINE_SYNC.

Клиент может выбрать отложенную обработку результата до тех пор, пока не будет отправлена полная конвейерная линия, или вклинить это в отправку дополнительных запросов в конвейере; см. Раздел 31.5.1.4.

PQgetResult ведет себя так же, как и при обычной асинхронной обработке, за исключением того, что он может содержать новые типы PGresult PGRES_PIPELINE_SYNC и PGRES_PIPELINE_ABORTED. PGRES_PIPELINE_SYNC сообщается ровно один раз для каждого PQpipelineSync или PQsendPipelineSync в соответствующей точке в конвейере. PGRES_PIPELINE_ABORTED выдается вместо нормального результата запроса для первой ошибки и всех последующих результатов до следующего PGRES_PIPELINE_SYNC; см. Раздел 31.5.1.3.

{PQisBusy, PQconsumeInput, и т.д. работают как обычно при обработке результатов конвейера. В частности, вызов PQisBusy в середине конвейера возвращает 0, если результаты всех до сих пор выпущенных запросов были обработаны.

libpq не предоставляет приложению никакой информации о текущем выполняемом запросе (за исключением того, что PQgetResult возвращает null, чтобы указать, что мы начинаем возвращать результаты следующего запроса). Приложение должно отслеживать порядок, в котором оно отправляет запросы, чтобы связать их с соответствующими результатами. Обычно приложения используют конечный автомат или очередь FIFO для этого.

31.5.1.3. Обработка ошибок #

С точки зрения клиента, после того как PQresultStatus возвращает PGRES_FATAL_ERROR, конвейер помечается как прерванный. PQresultStatus будет сообщать PGRES_PIPELINE_ABORTED результат для каждой оставшейся в очереди операции в прерванном конвейере. Результат для PQpipelineSync или PQsendPipelineSync сообщается как PGRES_PIPELINE_SYNC для обозначения конца прерванного конвейера и возобновления нормальной обработки результатов.

Клиент должен обрабатывать результаты с помощью PQgetResult во время восстановления после ошибки.

Если конвейер использовал неявную транзакцию, то операции, которые уже выполнены, откатываются, а операции, которые были запланированы после неудачной операции, полностью пропускаются. То же самое происходит, если конвейер запускается и коммитит одну явную транзакцию (т.е. первый оператор - BEGIN, а последний - COMMIT), за исключением того, что сессия остается в состоянии прерванной транзакции в конце конвейера. Если конвейер содержит несколько явных транзакций, все транзакции, которые были подтверждены до ошибки, остаются зафиксированными, текущая выполняющаяся транзакция прерывается, и все последующие операции полностью пропускаются, включая последующие транзакции. Если точка синхронизации конвейера происходит с блоком явной транзакции в состоянии прерванной, следующий конвейер немедленно становится прерванным, если следующая команда не переводит транзакцию в нормальный режим с помощью ROLLBACK.

Примечание

Клиент не должен предполагать, что работа подтверждена, когда он отправляет команду COMMIT - только когда получает соответствующий результат, подтверждающий завершение коммита. Поскольку ошибки приходят асинхронно, приложение должно иметь возможность перезапуститься с последнего полученного зафиксированного изменения и повторно отправить выполненную работу после этого момента, если что-то пойдет не так.

31.5.1.4. Обработка результатов и диспетчеризация запросов взаимно переплетены #

Чтобы избежать блокировок в больших конвейерах, клиент должен быть структурирован вокруг неблокирующего цикла событий, используя средства операционной системы, такие как select, poll, WaitForMultipleObjectEx и т. д.

Клиентское приложение обычно должно поддерживать очередь оставшейся работы для отправки и очередь работы, которая была отправлена, но еще не была обработана. Когда сокет доступен для записи, приложение должно отправлять больше работы. Когда сокет доступен для чтения, оно должно читать результаты и обрабатывать их, сопоставляя их с следующей записью в соответствующей очереди результатов. В зависимости от доступной памяти результаты с сокета должны читаться часто: нет необходимости ждать до конца конвейера для чтения результатов. Конвейеры должны быть ограничены логическими единицами работы, обычно (но не обязательно) одна транзакция на конвейер. Нет необходимости выходить из режима конвейера и снова входить в него между конвейерами или ждать, пока один конвейер не закончится, прежде чем отправить следующий.

Пример использования функции select() и простого конечного автомата для отслеживания отправленной и полученной работы находится в файле src/test/modules/libpq_pipeline/libpq_pipeline.c в дистрибутиве исходного кода PostgreSQL.

31.5.2. Функции, связанные с режимом конвейера #

PQpipelineStatus #

Возвращает текущий статус режима конвейера соединения libpq.

PGpipelineStatus PQpipelineStatus(const PGconn *conn);

PQpipelineStatus может возвращать одно из следующих значений:

PQ_PIPELINE_ON

Соединение libpq находится в режиме конвейера.

PQ_PIPELINE_OFF

Соединение libpq не находится в режиме pipeline.

PQ_PIPELINE_ABORTED

Соединение libpq находится в режиме конвейера, и произошла ошибка при обработке текущего конвейера. Флаг "отменено" сбрасывается, когда функция PQgetResult возвращает результат типа PGRES_PIPELINE_SYNC.

PQenterPipelineMode #

Заставляет соединение перейти в режим конвейера, если оно в настоящее время находится в режиме простоя или уже находится в режиме конвейера.

int PQenterPipelineMode(PGconn *conn);

Возвращает 1 в случае успеха. Возвращает 0 и не имеет эффекта, если соединение в данный момент не является простаивающим, то есть имеет готовый результат или ожидает дополнительного ввода от сервера и т. д. Эта функция фактически ничего не отправляет на сервер, она только изменяет состояние соединения libpq.

PQexitPipelineMode #

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

int PQexitPipelineMode(PGconn *conn);

Возвращает 1 в случае успеха. Возвращает 1 и не выполняет никаких действий, если не находится в режиме конвейера. Если текущий оператор еще не завершил обработку или функция PQgetResult не была вызвана для сбора результатов всех ранее отправленных запросов, возвращает 0 (в этом случае используйте PQerrorMessage для получения дополнительной информации об ошибке).

PQpipelineSync #

Отмечает точку синхронизации в конвейере, отправляя сообщение синхронизации и очищая буфер отправки. Это служит разделителем неявной транзакции и точкой восстановления после ошибки; см. Раздел 31.5.1.3.

int PQpipelineSync(PGconn *conn);

Возвращает 1 в случае успеха. Возвращает 0, если соединение не находится в режиме конвейера или отправка сообщения sync не удалась.

PQsendPipelineSync #

Отмечает точку синхронизации в конвейере, отправляя сообщение синхронизации без очистки буфера отправки. Это служит разделителем неявной транзакции и точкой восстановления после ошибки; см. Раздел 31.5.1.3.

int PQsendPipelineSync(PGconn *conn);

Возвращает 1 в случае успеха. Возвращает 0, если соединение не находится в режиме конвейера или отправка синхронизирующего сообщения не удалась. Обратите внимание, что сообщение само по себе не отправляется на сервер автоматически; используйте PQflush при необходимости.

PQsendFlushRequest #

Отправляет запрос серверу для сброса его буфера вывода.

int PQsendFlushRequest(PGconn *conn);

Возвращает 1 в случае успеха. Возвращает 0 в случае любой ошибки.

Сервер автоматически очищает свой выходной буфер в результате вызова PQpipelineSync, или по любому запросу, когда не используется режим конвейера; эта функция полезна для того, чтобы заставить сервер очистить свой выходной буфер в режиме конвейера без установления точки синхронизации. Обратите внимание, что сам запрос не отправляется на сервер автоматически; используйте PQflush, если необходимо.

31.5.3. Когда использовать режим конвейера #

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

Режим конвейера наиболее полезен, когда сервер находится на большом расстоянии, то есть задержка в сети (время отклика ping) высокая, а также когда выполняется много маленьких операций одна за другой в быстром темпе. Обычно нет большой выгоды в использовании команд конвейера, когда каждый запрос занимает много кратное время обмена данными между клиентом и сервером. Операция с 100 запросами, запущенная на сервере, находящемся на расстоянии 300 мс времени обмена данными, займет 30 секунд только на задержку в сети без использования конвейера; с использованием конвейера она может занять всего 0,3 секунды ожидания результатов от сервера.

Используйте команды с конвейером, когда ваше приложение выполняет множество маленьких операций INSERT, UPDATE и DELETE, которые не могут быть легко преобразованы в операции над множествами или в операцию COPY.

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

BEGIN;
SELECT x FROM mytable WHERE id = 42 FOR UPDATE;
-- result: x=2
-- client adds 1 to x:
UPDATE mytable SET x = 3 WHERE id = 42;
COMMIT;

может быть сделано намного более эффективно с помощью:

UPDATE mytable SET x = x + 1 WHERE id = 42;

Pipelining менее полезен и более сложен, когда одна конвейер содержит несколько транзакций (см. Раздел 31.5.1.3).



[15] Клиент будет блокироваться при попытке отправить запросы на сервер, но сервер будет блокироваться при попытке отправить результаты клиенту от уже обработанных запросов. Это происходит только тогда, когда клиент отправляет достаточное количество запросов, чтобы заполнить как свой буфер вывода, так и буфер приема сервера, прежде чем переключиться на обработку ввода от сервера, но точно предсказать, когда это произойдет, сложно.