34.5. 管道模式#
libpq管道模式允许应用程序发送查询,而不必读取先前发送查询的结果。利用管道模式,客户端将减少等待服务器的时间,因为可以在单个网络事务中发送/接收多个查询/结果。
虽然管道模式提供了显著的性能提升,但使用管道模式编写客户端更加复杂,因为它涉及管理挂起的查询队列,以及查找队列中哪个结果对应于哪个查询。
管道模式通常还会在客户端和服务器上消耗更多内存,尽管可以通过仔细且积极地管理发送/接收队列来缓解这种情况。无论连接处于阻塞模式还是非阻塞模式,这一点都适用。
虽然libpq的管道 API 在PostgreSQL14 中引入,但它是一个客户端特性,不需要特殊的服务器支持,并且可以在支持 v3 扩展查询协议的任何服务器上工作。有关更多信息,请参阅第 55.2.4 节。
34.5.1. 使用管道模式#
要发出管道,应用程序必须将连接切换到管道模式,这可以通过PQenterPipelineMode
来完成。可以使用PQpipelineStatus
来测试管道模式是否处于活动状态。在管道模式下,仅允许使用利用扩展查询协议的异步操作,不允许包含多个 SQL 命令的命令字符串,COPY
也不允许。使用同步命令执行函数(例如PQfn
、PQexec
、PQexecParams
、PQprepare
、PQexecPrepared
、PQdescribePrepared
、PQdescribePortal
)是一种错误情况。PQsendQuery
也不允许,因为它使用简单查询协议。一旦所有已发送命令的结果都已处理完毕,并且最终管道结果已被使用,应用程序可以使用PQexitPipelineMode
返回到非管道模式。
34.5.1.1. 发出查询#
进入管道模式后,应用程序使用PQsendQueryParams
或其预处理查询同级PQsendQueryPrepared
调度请求。这些请求在客户端排队,直到刷新到服务器;当使用PQpipelineSync
在管道中建立一个同步点,或调用PQflush
时,就会发生这种情况。函数PQsendPrepare
、PQsendDescribePrepared
和PQsendDescribePortal
也适用于管道模式。结果处理如下所述。
服务器执行语句并按客户端发送的顺序返回结果。服务器将立即开始执行管道中的命令,而不会等待管道结束。请注意,结果在服务器端缓冲;当使用PQpipelineSync
建立同步点,或调用PQsendFlushRequest
时,服务器会刷新该缓冲区。如果任何语句遇到错误,服务器会中止当前事务,并且在下一个同步点之前不会执行队列中的任何后续命令;对于每个此类命令,都会产生一个PGRES_PIPELINE_ABORTED
结果。(即使管道中的命令会回滚事务,这一点仍然成立。)查询处理在同步点之后恢复。
一个操作依赖于前一个操作的结果是可以的;例如,一个查询可以定义一个表,而同一管道中的下一个查询使用该表。同样,一个应用程序可以创建一个命名预处理语句,并在同一管道中的后续语句中执行它。
34.5.1.2. 处理结果#
若要处理管道中一个查询的结果,应用程序会反复调用PQgetResult
,并处理每个结果,直至PQgetResult
返回 null。然后可以使用PQgetResult
再次检索管道中下一个查询的结果,并重复该循环。应用程序会像通常一样处理各个语句结果。当管道中所有查询的结果都已返回时,PQgetResult
会返回包含状态值PGRES_PIPELINE_SYNC
的结果
客户端可以选择推迟结果处理,直到发送整个管道,或者将其与在管道中发送进一步查询交错;请参见第 34.5.1.4 节。
若要进入单行模式,请在使用PQgetResult
检索结果之前调用PQsetSingleRowMode
。此模式选择仅对当前正在处理的查询有效。有关PQsetSingleRowMode
用法的更多信息,请参阅第 34.6 节。
PQgetResult
的行为与正常异步处理相同,但它可能包含新的PGresult
类型PGRES_PIPELINE_SYNC
和PGRES_PIPELINE_ABORTED
。对于管道中相应点处的每个PQpipelineSync
,PGRES_PIPELINE_SYNC
会报告一次。对于第一个错误及其所有后续结果,PGRES_PIPELINE_ABORTED
会代替正常查询结果发出,直至下一个PGRES_PIPELINE_SYNC
;请参见第 34.5.1.3 节。
在处理管道结果时,PQisBusy
、PQconsumeInput
等会像通常一样运行。特别是,如果在管道中间调用PQisBusy
,则如果已使用到目前为止发出的所有查询的结果,则会返回 0。
libpq不会向应用程序提供有关当前正在处理的查询的任何信息(除了PQgetResult
返回 null 以指示我们开始返回下一个查询的结果)。应用程序必须跟踪发送查询的顺序,以将其与相应的结果相关联。应用程序通常会为此使用状态机或 FIFO 队列。
34.5.1.3. 错误处理#
从客户端的角度来看,在PQresultStatus
返回PGRES_FATAL_ERROR
之后,管道被标记为已中止。对于中止的管道中每个剩余的排队操作,PQresultStatus
将报告一个PGRES_PIPELINE_ABORTED
结果。PQpipelineSync
的结果报告为PGRES_PIPELINE_SYNC
,以表示中止的管道结束和正常结果处理恢复。
客户端必须在错误恢复期间使用PQgetResult
处理结果。
如果管道使用隐式事务,则已执行的操作将回滚,而排队在失败操作之后的操作将完全跳过。如果管道启动并提交单个显式事务(即第一个语句是BEGIN
,最后一个是COMMIT
),则相同的行为仍然成立,除了会话在管道结束时仍处于中止的事务状态。如果管道包含多个显式事务,则在错误之前提交的所有事务仍保持提交状态,当前正在进行的事务将中止,所有后续操作都将完全跳过,包括后续事务。如果管道同步点在中止状态下发生显式事务块,则下一个管道将立即中止,除非下一个命令使用ROLLBACK
将事务置于正常模式。
注意
客户端不能假设在发送COMMIT
时工作已提交,而只能在收到相应结果确认提交已完成时才能假设工作已提交。由于错误异步到达,因此如果出现问题,应用程序需要能够从最后接收到的已提交更改重新启动,并重新发送此后完成的工作。
34.5.1.4. 交错结果处理和查询分派#
为了避免大管道上的死锁,客户端应围绕非阻塞事件循环构建,使用操作系统设施,例如select
、poll
、WaitForMultipleObjectEx
等。
客户端应用程序通常应维护一个待分派的工作队列和一个已分派但尚未处理其结果的工作队列。当套接字可写时,它应分派更多工作。当套接字可读时,它应读取结果并对其进行处理,将其与相应结果队列中的下一个条目匹配。根据可用内存,应频繁读取套接字中的结果:无需等到管道结束才读取结果。管道应限定为逻辑工作单元,通常(但不一定)每个管道一个事务。无需在管道之间退出管道模式并重新进入管道模式,或无需等待一个管道完成再发送下一个管道。
使用select()
和简单状态机跟踪已发送和已接收工作的示例位于 PostgreSQL 源代码分发中的src/test/modules/libpq_pipeline/libpq_pipeline.c
中。
34.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
#通过发送 同步消息 并刷新发送缓冲区,在管道中标记一个同步点。这充当隐式事务的分隔符和错误恢复点;请参阅 第 34.5.1.3 节。
int PQpipelineSync(PGconn *conn);
成功时返回 1。如果连接未处于管道模式或发送 同步消息 失败,则返回 0。
PQsendFlushRequest
#发送请求以使服务器刷新其输出缓冲区。
int PQsendFlushRequest(PGconn *conn);
成功时返回 1。在任何故障时返回 0。
服务器自动刷新其输出缓冲区,这是由于调用
PQpipelineSync
或在非管道模式下的任何请求;此函数可用于在管道模式下使服务器刷新其输出缓冲区,而无需建立同步点。请注意,请求本身不会自动刷新到服务器;如果需要,请使用PQflush
。
34.5.3. 何时使用管道模式#
与异步查询模式非常类似,使用管道模式时没有有意义的性能开销。它增加了客户端应用程序的复杂性,并且需要格外小心以防止客户端/服务器死锁,但管道模式可以提供相当大的性能改进,以换取因长时间保留状态而增加的内存使用量。
当服务器距离较远(即网络延迟(“ping 时间”)较高)时,管道模式最有用,而且在快速连续执行许多小操作时也是如此。当每个查询执行需要花费客户端/服务器往返时间很多倍时,使用管道命令通常好处较少。在往返时间为 300 毫秒的服务器上运行 100 个语句操作,如果没有管道,仅网络延迟就需要 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;
当单个管道包含多个事务时,管道处理效率较低,而且更复杂(请参见第 34.5.1.3 节)。
[15]客户端将阻塞,尝试向服务器发送查询,但服务器将阻塞,尝试向客户端发送已处理查询的结果。这仅在客户端发送的查询足以填满其输出缓冲区和服务器的接收缓冲区时才会发生,然后它才会切换到处理来自服务器的输入,但很难准确预测何时会发生这种情况。