PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

32.5. 流水线管道模式 #

32.5.1. 使用流水线模式
32.5.2. 流水线模式相关函数
32.5.3. 何时使用流水线模式

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

虽然管道模式带来了显著的性能提升, 但使用管道模式编写客户端更复杂, 因为它涉及管理一个待处理查询的队列, 并找出队列中哪个结果对应哪个查询。

管道模式通常还会在客户端和服务器上消耗更多内存, 尽管对发送/接收队列进行仔细且积极的管理可以缓解这一点。 无论连接处于阻塞还是非阻塞模式, 这一点都适用。

虽然 libpq 的管道 API 是在 PostgreSQL 14 中引入的, 它是一个客户端侧特性, 不需要特殊的服务器支持, 并且可以在任何支持 v3 扩展查询协议的服务器上工作。 更多信息请参见 Section 54.2.4。

32.5.1. 使用流水线模式 #

要发出管道, 应用程序必须把连接切换到管道模式, 这通过 PQenterPipelineMode 完成。 PQpipelineStatus 可用于测试管道模式是否处于活动状态。 在管道模式中, 只允许使用扩展查询协议的 异步操作, 包含多条 SQL 命令的命令字符串是不允许的, COPY 也不允许。 使用同步命令执行函数 (例如 PQfn、 PQexec、 PQexecParams、 PQprepare、 PQexecPrepared、 PQdescribePrepared、 PQdescribePortal、 PQclosePrepared、 PQclosePortal) 是一种错误条件。 PQsendQuery 也是不允许的, 因为它使用简单查询协议。 一旦所有被分派的命令的结果都得到了处理, 并且结束管道的结果已被消费, 应用程序就可以用 PQexitPipelineMode 返回非管道模式。

Note

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

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 返回空。 然后可以再次使用 PQgetResult 来检索管道中下一条查询的结果, 并重复这一循环。 应用程序按照正常方式处理单个语句的结果。 当管道中所有查询的结果都已被返回时, PQgetResult 会返回一个包含状态值 PGRES_PIPELINE_SYNC 的结果

客户端可以选择把结果处理推迟到完整的管道被发送之后, 或者把它与在管道中发送更多查询交错进行; 参见 Section 32.5.1.4。

PQgetResult 的行为与普通异步处理相同, 只是它可能包含新的 PGresult 类型 PGRES_PIPELINE_SYNC 和 PGRES_PIPELINE_ABORTED。 管道中每个 PQpipelineSync 或 PQsendPipelineSync 的相应位置上, 恰好会报告一次 PGRES_PIPELINE_SYNC。 对于第一个错误以及直到下一个 PGRES_PIPELINE_SYNC 的所有后续结果, 都会以 PGRES_PIPELINE_ABORTED 代替正常的查询结果发出; 参见 Section 32.5.1.3。

在处理管道结果时, PQisBusy、 PQconsumeInput 等照常运作。 特别是, 在管道中间调用一次 PQisBusy 时, 如果到目前为止发出的所有查询的结果都已被消费, 它会返回 0。

对于当前正在处理的查询, libpq 不会向应用程序提供任何信息 (唯一的例外是 PQgetResult 返回空, 表示我们开始返回下一个查询的结果)。 应用程序必须自己跟踪它发送查询的顺序, 以便把它们与相应的结果关联起来。 应用程序通常会为此使用一个状态机或一个 FIFO 队列。

32.5.1.3. 错误处理 #

从客户端的角度来看, 在 PQresultStatus 返回 PGRES_FATAL_ERROR 之后, 管道会被标记为已中止。 对于被中止的管道中剩余的每个排队操作, PQresultStatus 都会报告一个 PGRES_PIPELINE_ABORTED 结果。 PQpipelineSync 或 PQsendPipelineSync 的结果会被报告为 PGRES_PIPELINE_SYNC, 以指示被中止管道的结束以及正常结果处理的恢复。

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

如果管道使用了一个隐式事务, 那么已经执行的操作会被回滚, 排队在失败操作之后执行的操作会被完全跳过。 如果管道开始并提交单个显式事务 (即第一条语句是 BEGIN, 最后一条是 COMMIT), 也会出现相同的行为, 只不过在管道结束时, 会话仍处于被中止的事务状态。 如果一个管道包含 多个显式事务, 所有在错误之前提交的事务都保持已提交, 当前正在进行的事务被中止, 而所有后续操作 (包括后续的事务) 都会被完全跳过。 如果在一个显式事务块处于被中止状态时发生了一次管道同步点, 那么除非下一条命令用 ROLLBACK 把事务置回正常模式, 否则下一个管道将立即变为被中止。

Note

客户端不得在它发送一条 COMMIT 时就假定工作已被提交 — 只有当收到相应的结果以确认提交完成时才能这样假定。 由于错误是异步到达的, 应用程序需要能够从最后一次 收到的已提交更改处重启, 并在出现问题的时候重发在那之后完成的工作。

32.5.1.4. 交错结果处理与查询调度 #

为了避免大型管道上的死锁, 客户端应当围绕一个使用操作系统设施 (例如 select、 poll、 WaitForMultipleObjectEx 等) 的非阻塞事件循环来构造。

客户端应用程序通常应当维护两个队列: 一个是尚待分派的工作的队列, 一个是已被分派但结果尚未被处理的工作的队列。 当套接字可写时, 它应当分派更多工作。 当套接字可读时, 它应当读取结果并处理它们, 把它们与相应结果队列中的下一个条目匹配起来。 根据可用内存的情况, 应当频繁地从套接字读取结果: 没有必要等到管道结束才读取结果。 管道应当以逻辑工作单元为界, 通常 (但不一定) 是每个管道一个事务。 没有必要在管道之间退出管道模式再重新进入, 也没有必要等待一个管道完成之后才发送下一个。

一个使用 select() 和一个简单的状态机来跟踪已发送和已接收工作的示例, 可以在 PostgreSQL 源代码发行版的 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 #

通过发送一条 同步消息 并刷出发送缓冲区, 在管道中标记一个同步点。 它充当一个隐式事务的分界符和一个错误恢复点; 参见 Section 32.5.1.3。

int PQpipelineSync(PGconn *conn);

成功时返回 1。 如果连接不在管道模式中, 或者发送一条 同步消息 失败, 则返回 0。

PQsendPipelineSync #

通过发送一条 同步消息 而不刷出发送缓冲区, 在管道中标记一个同步点。 它充当一个隐式事务的分界符和一个错误恢复点; 参见 Section 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 毫秒往返时间之外的服务器上运行的一个 100 条语句的操作, 在不使用流水线的情况下仅网络延迟就要花费 30 秒; 使用流水线之后, 它等待来自服务器的结果的时间可能只要 0.3 秒。

当你的应用程序执行大量无法轻松被转换为集合操作或 COPY 操作的小型 INSERT、 UPDATE 和 DELETE 操作时, 请使用流水线命令。

当客户端需要某个操作的信息来产生下一个操作时, 管道模式没有用处。 在这样的情况下, 客户端将不得不引入一个同步点, 并等待一个完整的客户端/服务器往返来获取它需要的结果。 然而, 通常可以通过调整客户端设计来改为在服务器侧交换所需的信息。 读-改-写循环是特别好的候选者; 例如:

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;

当单个管道包含多个事务时 (参见 Section 32.5.1.3), 流水线的用处更小, 并且更复杂。



[10] 客户端将阻塞在尝试向服务器发送查询上, 而服务器将阻塞在尝试把它已处理的查询的结果发送给客户端上。 只有当客户端在切换到处理来自服务器的输入之前, 发送了足以填满它自己的输出缓冲区和服务器接收缓冲区的查询时, 这种情况才会发生, 但很难预测它究竟会在何时发生。