32.5. 管道模式 #
libpq 的管道模式允许应用程序在尚未读取先前查询结果时继续发送查询。多个查询及其结果可以在一次网络交互中发送和接收,从而减少客户端等待服务器的时间。
管道模式可以显著提升性能,但使用它编写客户端也更复杂,因为需要管理待处理查询队列,并确定每个结果对应队列中的哪个查询。
管道模式通常也会增加客户端和服务器的内存消耗,不过仔细、主动地管理发送和接收队列可以缓解这一问题。无论连接处于阻塞还是非阻塞模式,都是如此。
虽然 libpq 的管道 API 是在 PostgreSQL 14 中引入的,但它是一个客户端特性,不需要特殊的服务器支持,并且适用于支持 v3 扩展查询协议的任何服务器。欲了解更多信息,请参阅第 54.2.4 节。
32.5.1. 使用管道模式 #
要使用管道,应用程序必须通过 PQenterPipelineMode 将连接切换到管道模式。可用 PQpipelineStatus 检查管道模式是否已启用。在管道模式下,只允许使用扩展查询协议的异步操作,不允许命令字符串包含多个 SQL 命令,也不允许 COPY。调用同步命令执行函数,例如 PQfn、PQexec、PQexecParams、PQprepare、PQexecPrepared、PQdescribePrepared、PQdescribePortal、PQclosePrepared、PQclosePortal,会导致错误。也不允许使用 PQsendQuery,因为它使用简单查询协议。所有已发送命令的结果及管道结束结果都读取处理完毕后,应用程序便可通过 PQexitPipelineMode 返回非管道模式。
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 的结果。
客户端可以等到整条管道发送完毕后再处理结果,也可以在继续发送管道中的查询时交错处理结果;参见第 32.5.1.4 节。
PQgetResult 的行为与普通异步处理相同,只是返回结果可能具有新的 PGresult 类型 PGRES_PIPELINE_SYNC 和 PGRES_PIPELINE_ABORTED。对于每次 PQpipelineSync 或 PQsendPipelineSync 调用,都会在管道中的对应位置恰好报告一次 PGRES_PIPELINE_SYNC。对于第一个错误及其后的所有结果,会用 PGRES_PIPELINE_ABORTED 代替正常查询结果,直到下一个 PGRES_PIPELINE_SYNC;参见第 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 将事务恢复为正常状态。
注意
客户端绝对不可以在发送 COMMIT 时就假设工作已经提交,只有收到相应结果、确认提交完成时,才能这样认为。因为错误是异步到达的,如果出现错误,应用需要能够从最后一个已收到确认的已提交更改重新开始,并重新发送在该点之后完成的工作。
32.5.1.4. 交错处理结果与发送查询 #
为避免大型管道发生死锁,客户端应围绕非阻塞事件循环组织,使用 select、poll、WaitForMultipleObjectEx 等操作系统机制。
客户端应用程序通常应维护两个队列:一个存放尚未发送的工作,另一个存放已经发送但尚未处理结果的工作。套接字可写时,应继续发送工作;套接字可读时,应读取并处理结果,将其与相应结果队列中的下一个条目匹配。应根据可用内存情况,频繁地从套接字读取结果,无需等到管道结束。每个管道应对应一个逻辑工作单元,通常是一个事务,但并非必须如此。管道之间无需退出再重新进入管道模式,也无需等待一个管道完成后才发送下一个。
PostgreSQL 源代码发行版的 src/test/modules/libpq_pipeline/libpq_pipeline.c 中提供了一个示例,使用 select() 和简单状态机跟踪已发送和已接收的工作。
32.5.2. 与管道模式关联的函数 #
PQpipelineStatus#返回 libpq 连接的当前管道模式状态。
PGpipelineStatus PQpipelineStatus(const PGconn *conn);
PQpipelineStatus可以返回下列值中的一个: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 毫秒的服务器上执行一个包含 100 条语句的操作,不使用管道时,仅网络延迟就需要 30 秒;使用管道时,等待服务器结果的时间可能低至 0.3 秒。
如果应用程序需要执行大量小型 INSERT、UPDATE 和 DELETE 操作,而这些操作又难以转换为集合操作或 COPY 操作,就可以使用管道命令。
如果客户端必须获得前一个操作的信息,才能生成下一个操作,管道模式就没有帮助。在这种情况下,客户端必须引入同步点,并等待一次完整的客户端与服务器往返,才能获得所需结果。不过,通常可以调整客户端设计,让所需信息在服务器端交换。读取、修改、写入的循环尤其适合这样改进。例如:
BEGIN; SELECT x FROM mytable WHERE id = 42 FOR UPDATE; -- 结果:x=2 -- 客户端将 x 加 1: UPDATE mytable SET x = 3 WHERE id = 42; COMMIT;
可以改写为以下效率更高的操作:
UPDATE mytable SET x = x + 1 WHERE id = 42;
当单个管道包含多个事务时,使用管道的收益较小,复杂度也更高(见第 32.5.1.3 节)。
[15] 客户端在尝试向服务器发送查询时阻塞,而服务器在尝试将已处理查询的结果发送给客户端时阻塞。只有当客户端在转而处理服务器输入之前,发送了足够多的查询,填满自身的输出缓冲区和服务器的接收缓冲区,才会发生这种情况;但很难准确预测何时会发生。