32.5. Конвейерный режим #
Конвейерный режим libpq даёт приложению отправить запрос, не получая результат предыдущего. Используя преимущества конвейерного режима, клиент будет меньше ждать сервер, поскольку несколько запросов/результатов могут быть отправлены/получены в одной сетевой транзакции.
Хотя конвейерный режим обеспечивает значительный прирост производительности, разрабатывать клиентские приложения, использующие этот режим, гораздо сложнее, так как требуется реализовывать очереди ожидающих запросов и сопоставлять результаты с запросами в очереди.
Для конвейерного режима также обычно требуется больше памяти как на стороне клиента, так и на стороне сервера, хотя тщательное и агрессивное управление очередью отправки/получения может это нивелировать. Это не зависит от того, в каком режиме — блокирующем или нет — устанавливается подключение.
Хотя конвейерный API в libpq появился с выходом PostgreSQL 14, это клиентская функциональность, которая не требует специальной поддержки на стороне сервера и работает с любым сервером, поддерживающем 3-ю версию расширенного протокола запросов. За дополнительными сведениями обратитесь к Подразделу 53.2.4.
32.5.1. Использование конвейерного режима #
Для запуска конвейеров приложение должно переключить соединение в конвейерный режим посредством функции PQenterPipelineMode
. Можно проверить, включён ли данный режим, используя функцию PQpipelineStatus
. В конвейерном режиме разрешены только асинхронные операции, использующие расширенный протокол запросов, а строки команд, содержащие несколько SQL-команд, и команда COPY
запрещены. Использовать функции синхронного выполнения команд, такие как PQfn
, PQexec
, PQexecParams
, PQprepare
, PQexecPrepared
, PQdescribePrepared
, PQdescribePortal
, PQclosePrepared
, PQclosePortal
в этом режиме нельзя. Функцию PQsendQuery
применять также нельзя, так как она использует простой протокол запросов. Обработав результаты всех отправленных команд и итоговый результат конвейера, приложение может вернуться в обычный режим, вызвав PQexitPipelineMode
.
Примечание
Конвейерный режим рекомендуется использовать при работе libpq в неблокирующем режиме. В блокирующем режиме возможны взаимоблокировки на уровне клиент-сервер. [15]
32.5.1.1. Отправка запросов #
Перейдя в конвейерный режим, приложение отправляет запросы, вызывая PQsendQueryParams
или родственную ей функцию PQsendQueryPrepared
, работающую с подготовленными запросами. Данные запросы ставятся в очередь на стороне клиента, а затем сбрасываются на сервер; это происходит, когда вызывается PQpipelineSync
, устанавливающая точку синхронизации в конвейере, или когда вызывается PQflush
. В конвейерном режиме также работают функции PQsendPrepare
, PQsendDescribePrepared
, PQsendDescribePortal
, PQsendClosePrepared
и PQsendClosePortal
. Обработка результатов описана ниже.
Сервер выполняет операторы и в порядке их поступления от клиента возвращает результаты. Сервер начнёт выполнять команды в конвейере немедленно, не дожидаясь конца конвейера. Обратите внимание, что результаты буферизуются на стороне сервера; сервер сбрасывает этот буфер, когда функцией PQpipelineSync
или PQsendPipelineSync
устанавливается точка синхронизации или когда вызывается функция PQsendFlushRequest
. Если при выполнении какого-либо оператора возникает ошибка, сервер прерывает текущую транзакцию и не выполняет никакую следующую команду в очереди до следующей точки синхронизации; для каждой такой команды выдаётся результат PGRES_PIPELINE_ABORTED
. (Это справедливо и тогда, когда в конвейере передаются команды, которые могли бы откатить транзакцию.) Обработка запроса возобновляется после точки синхронизации.
Одна операция вполне может зависеть от результатов предыдущей; например, в одном запросе может создаваться таблица, которую будет использовать следующий запрос в том же конвейере. Точно так же приложение может создать именованный подготовленный оператор и выполнить его с последующими операторами в том же конвейере.
32.5.1.2. Обработка результатов #
Чтобы обработать результат одного запроса в конвейере, приложение многократно вызывает PQgetResult
и обрабатывает каждый её результат, пока PQgetResult
не выдаст NULL. Затем может быть получен результат следующего запроса в конвейере, также с помощью PQgetResult
, и весь цикл повторяется. Результаты отдельных запросов приложение обрабатывает обычным образом. После того, как будут выданы результаты всех запросов в конвейере, PQgetResult
выдаёт результат со значением статуса PGRES_PIPELINE_SYNC
.
Клиент может отложить обработку результатов до тех пор, пока весь конвейер не будет отправлен, или чередовать её с отправкой дальнейших запросов в конвейере; см. Подраздел 32.5.1.4.
Функция PQgetResult
работает так же, как и при обычной асинхронной обработке, но может дополнительно выдавать результаты новых типов PGRES_PIPELINE_SYNC
и PGRES_PIPELINE_ABORTED
. PGRES_PIPELINE_SYNC
выдаётся ровно один раз для каждого вызова PQpipelineSync
или PQsendPipelineSync
в соответствующей точке конвейера. 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
или PQsendPipelineSync
выдаёт результат 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, если соединение не находится в конвейерном режиме или сообщение синхронизации отправить не удалось.
PQsendPipelineSync
#Отмечает точку синхронизации в конвейере, отправляя сообщение синхронизации, но не очищая буфер отправки. Эта точка служит ограничителем неявной транзакции и точкой восстановления после ошибки; см. Подраздел 32.5.1.3.
int PQsendPipelineSync(PGconn *conn);
Возвращает 1 в случае успеха. Возвращает 0, если соединение не находится в конвейерном режиме или сообщение синхронизации отправить не удалось. Обратите внимание, что сообщение не сохраняется на сервере автоматически. При необходимости используйте функцию
PQflush
.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] Клиент может заблокироваться, пытаясь передать запросы серверу, а сервер заблокируется, пытаясь выдать клиенту результаты уже выполненных запросов. Это возможно, только когда клиент передаёт так много запросов, что заполняет и свой выходной буфер, и входной буфер сервера, и только затем переключается на обработку передаваемых сервером результатов, но предсказать, когда точно это произойдёт, сложно.