32.5. Конвейерный режим

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

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

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

Хотя конвейерный API появился с выходом PostgreSQL 14, это клиентская функциональность, которая не требует специальной поддержки на стороне сервера и работает с любым сервером, поддерживающем 3-ю версию протокола расширенных запросов.

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

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

Примечание

Конвейерный режим рекомендуется использовать при работе libpq в неблокирующем режиме. В блокирующем режиме возможны взаимоблокировки на уровне клиент-сервер. [15]

32.5.1.1. Отправка запросов

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

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

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

32.5.1.2. Обработка результатов

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

Клиент может отложить обработку результатов до тех пор, пока весь конвейер не будет отправлен, или чередовать её с отправкой дальнейших запросов в конвейере; см. Подраздел 32.5.1.4.

Чтобы войти в однострочный режим, вызовите PQsetSingleRowMode перед получением результатов от PQgetResult. Выбранный режим будет действовать только для текущего обрабатываемого запроса. Для получения дополнительной информации об использовании PQsetSingleRowMode обратитесь к Разделу 32.6.

Функция PQgetResult работает так же, как и при обычной асинхронной обработке, но может дополнительно выдавать результаты новых типов PGRES_PIPELINE_SYNC и PGRES_PIPELINE_ABORTED. PGRES_PIPELINE_SYNC выдаётся ровно один раз для каждого вызова PQpipelineSync в соответствующей точке конвейера. PGRES_PIPELINE_ABORTED выдаётся вместо обычного результата запроса для первой ошибки и всех последующих результатов до следующего PGRES_PIPELINE_SYNC; см. Подраздел 32.5.1.3.

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

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

32.5.1.3. Обработка ошибок

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

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

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

Примечание

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

32.5.1.4. Чередование обработки результатов и отправки запросов

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

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

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

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

PQpipelineStatus

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

PGpipelineStatus PQpipelineStatus(const PGconn *conn);

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

PQ_PIPELINE_ON

Подключение libpq находится в конвейерном режиме.

PQ_PIPELINE_OFF

Подключение libpq не находится в конвейерном режиме.

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

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

int PQpipelineSync(PGconn *conn);

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

PQsendFlushRequest

Отправляет серверу команду сбросить его буфер вывода.

int PQsendFlushRequest(PGconn *conn);

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

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

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

Как и в случае с асинхронным режимом запросов, при использовании конвейерного режима нет значительных издержек производительности. Использование конвейерного режима увеличивает сложность клиентского приложения и требует дополнительной осторожности во избежание взаимоблокировок клиент-сервер, но может предложить значительное улучшение производительности в обмен на увеличение объёма используемой памяти из-за более длительного выхода из состояния.

Конвейерный режим наиболее полезен, когда сервер находится на большом расстоянии от клиента, т. е. когда сетевая задержка («ping time») велика, а также когда много небольших операций выполняются в быстрой последовательности. Как правило, использование конвейерных команд даёт меньше преимуществ, когда выполнение каждого запроса занимает в несколько раз больше времени, чем передача данных клиент-сервер и обратно. Операция из 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;

Конвейеризация менее полезна и более сложна, когда один конвейер содержит несколько транзакций (см. Подраздел 32.5.1.3).



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