Redrock Postgres 搜索 英文
版本: 14 / 15 / 16 / 17

32.5. 管道模式 #

32.5.1. 使用管道模式
32.5.2. 与管道模式相关联的函数
32.5.3. 何时使用管道模式

libpq 管道模式允许应用程序发送查询,而不必读取先前发送的查询的结果。利用管道模式,客户端等待服务器的时间更少,因为多个查询/结果可以在单次网络事务中发送/接收。

管道模式提供了显著的性能提升,但是使用管道模式编写客户端更为复杂,因为它涉及管理挂起的查询队列并查找队列中哪个结果与哪个查询相对应。

管道模式通常也会在客户端和服务器上消耗更多内存,尽管可以仔细而积极地管理发送/接收队列来缓解此问题。无论连接处于阻塞模式还是非阻塞模式,此问题都适用。

尽管 libpq 的管道 API 在 PostgreSQL 14 中引入,但它是客户端特性,不需要特殊的服务器支持,并且可以在支持 v3 扩展查询协议的任何服务器上运行。更多信息,请参见 第 53.2.4 节

32.5.1. 使用管道模式 #

要发出管道,应用程序必须将连接切换到管道模式,这是通过 PQenterPipelineMode 完成的。可以使用 PQpipelineStatus 来测试管道模式是否处于活动状态。在管道模式中,只允许利用扩展查询协议的 异步操作,不允许包含多个 SQL 命令的命令字符串,也不允许使用 COPY。使用 PQfnPQexecPQexecParamsPQpreparePQexecPreparedPQdescribePreparedPQdescribePortalPQclosePreparedPQclosePortal 等同步命令执行函数是错误条件。PQsendQuery 也不允许,因为它使用的是简单的查询协议。一旦所有已分派的命令的结果都被处理完毕,并且管道结束结果已被消耗,应用程序可以使用 PQexitPipelineMode 返回非管道模式。

提示

最好在 libpq 中与 非阻塞模式 一起使用管道模式。如果在阻塞模式下使用,可能会发生客户端/服务器死锁。[15]

32.5.1.1. 发出查询 #

在进入管道模式后,应用程序使用 PQsendQueryParams 或它的预处理查询同级元素 PQsendQueryPrepared 来分派请求。这些请求在客户端排队,直至刷新到服务器;当使用 PQpipelineSync 在管道中建立一个同步点,或当调用 PQflush 时发生这种情况。函数 PQsendPreparePQsendDescribePreparedPQsendDescribePortalPQsendClosePreparedPQsendClosePortal 也适用于管道模式。结果处理在下文中描述。

服务器按客户端发送的顺序执行语句并返回结果。服务器将立即执行管道中的命令,而不等待管道结束。请注意,结果在服务器端进行缓冲;当通过 PQpipelineSyncPQsendPipelineSync 建立同步点时,服务器将刷新该缓冲,或当调用 PQsendFlushRequest 时。如果任何语句遇到错误,服务器将中止当前事务,并在下一个同步点之前不会执行队列中的任何后续命令;对于每个此类命令都会生成一个 PGRES_PIPELINE_ABORTED 结果。(即使管道中的命令会回滚事务,仍为 true。)在同步点之后将恢复查询处理。

一个操作依赖于先前操作的结果是可以的;例如,一个查询可能会定义一个表,而同一管道中的下一个查询会使用该表。类似地,应用程序可以创建一个已命名的预处理语句,并使用同一管道中的后面语句来执行它。

32.5.1.2. 处理结果 #

若要处理管道中某个查询的结果,应用程序重复调用 PQgetResult 并处理各结果,直至 PQgetResult 返回 null。然后可以使用 PQgetResult 再次检索管道中下一个查询的结果,并重复该循环。应用程序按正常方式处理各个语句的结果。当管道中所有查询的结果都返回后,PQgetResult 返回包含状态值 PGRES_PIPELINE_SYNC 的结果

客户端可以选择延迟结果处理直到整个管道发送完毕,或者在管道中发送进一步的查询时交织进行结果处理;请参见 第 32.5.1.4 节

PQgetResult 的行为与正常异步处理时相同,但它可能包含新的 PGresult 类型 PGRES_PIPELINE_SYNCPGRES_PIPELINE_ABORTED。对于管道中相应点上的每个 PQpipelineSyncPQsendPipelineSync,都会报告 PGRES_PIPELINE_SYNC 一次。PGRES_PIPELINE_ABORTED 在第一个错误及其所有后续结果的正常查询结果位置发出,直至下一个 PGRES_PIPELINE_SYNC;请参见 第 32.5.1.3 节

在处理管道结果时,PQisBusyPQconsumeInput 等正常运行。特别是,如果结果针对迄今为止发布的所有查询均已使用,则在管道中间对 PQisBusy 的调用将返回 0。

libpq 不向应用程序提供任何有关当前正在处理的查询的信息(除了 PQgetResult 返回 null 表示我们开始返回后续查询的结果)。该应用程序必须跟踪其发送查询的顺序,以将其与相应的结果关联。应用程序通常对此使用状态机或 FIFO 队列。

32.5.1.3. 错误处理 #

从客户端的角度来看,在 PQresultStatus 返回 PGRES_FATAL_ERROR 后,管道标记为已中止。对于中止管道中剩余的每个排队操作,PQresultStatus 将报告 PGRES_PIPELINE_ABORTED 结果。对于 PQpipelineSyncPQsendPipelineSync 的结果,将报告为 PGRES_PIPELINE_SYNC,以表示中止的管道结束和正常结果处理恢复。

客户端必须在错误恢复期间使用 PQgetResult 处理结果。

如果该管道使用了隐式事务,则已经执行的操作将回滚,而排队以遵循失败操作的操作将完全跳过。如果该管道启动并提交单个显式事务(即第一个语句是 BEGIN 而最后一个是 COMMIT),则将保留相同的行为,除了在管道结束时该会话保持中止事务状态。如果一个管道包含多个显式事务,则在错误之前提交的所有事务保持提交,当前进行中的事务将中止,且所有后续操作将被完全跳过,包括后续事务。如果在中止状态下出现管道同步点和显式事务块,则下一个管道将立即中止,除非下一条命令使用 ROLLBACK 将事务置于正常模式。

提示

当客户端发送 COMMIT 时,它一定不能假定工作已提交,仅在收到相应结果以确认提交已完成时假定工作已提交。由于错误异步到达,因此如果出现问题,应用程序需要能够从上一个接收的已提交更改重新开始,并重新发送在那之后完成的工作。

32.5.1.4. 结果处理和查询分发交错 #

为了避免在大型管道上发生死锁,客户端应围绕一个非阻塞事件循环进行构建,该事件循环使用诸如selectpollWaitForMultipleObjectEx等操作系统设施。

客户端应用程序通常应维护一个剩余待分配的工作队列和一个已分配但尚未处理其结果的工作队列。当套接字可写时,它应分配更多工作。当套接字可读时,它应读取结果并对其进行处理,将其与相应结果队列中的下一个条目匹配。根据可用内存,应频繁地读取来自套接字的结果:无需等到管道结束再来读取结果。管道应限定于工作的逻辑单元,通常(但不一定)每个管道一个事务。无需在管道之间退出管道模式并重新进入该模式,或者无需等待一个管道完成再发送下一个管道。

一个使用select()和一个简单状态机来跟踪已发送和接收的工作的示例位于 Postgre SQL 源分发包中的src/test/modules/libpq_pipeline/libpq_pipeline.c中。

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 时间)较高),并且连续执行许多小操作时,管道模式最有用。当每个查询在执行时占用很多客户端/服务器往返时间倍数时,使用管道化命令通常好处较少。在往返时间为 300 ms 的服务器上运行 100 条语句的操作,如果没有管道,仅网络延迟就需要 30 秒;使用管道时,等待服务器结果的时间可能短至 0.3 秒。

在应用程序执行大量不易转换为集合操作或 COPY 操作的小 INSERTUPDATEDELETE 操作时,使用管道化命令。

当客户端需要在进行一项操作后获取信息才能执行下一项操作时,管道模式不可用。在这种情况下,客户端必须引入同步点并等待一个完整的客户端/服务器往返才能获取所需结果。但是,通常可以调整客户端设计以在服务器端交换所需信息。读改写周期是做得很好的候选操作,例如

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] 客户端将被阻止向服务器发送查询,但服务器将被阻止向客户端发送它已处理查询的结果。这仅在客户端发送足够的查询以填充其输出缓冲区和服务器的接收缓冲区后才会出现,然后客户端会切换到处理来自服务器的输入,但是很难准确预测何时会发生这种情况。