如果在 pgbouncer 具有 pool_mode=transaction 时运行单个查询,最坏的情况是什么?

What's the worst case scenario if one runs individual queries when pgbouncer has pool_mode=transaction?

让我们假设 pgbouncer 的配置如下:

pool_mode=transaction
reset_query=discard all
reset_query_always=0

如果我有这样的连接

begin transaction
...
commit transaction

那么就很简单了,因为在事务打开期间,pgbouncer 将只为这个客户端连接保留后端连接。

但是,如果客户端应用改为发送

select select pg_advisory_lock(123);
begin transaction isolation level serializable
...
commit transaction
select select pg_advisory_unlock(123);

是否有可能在查询之间切换后端连接,以便后端连接 #1 获取锁,连接 #2 执行事务,连接 #3 尝试解锁建议锁定并明显失败?

(建议锁定将用作高负载情况的优化,在这种情况下,由于大量回滚事务,序列化事务之间的冲突会导致后端数据库服务器上的 CPU 负载很高。通常情况下,冲突很少发生,以至于可序列化的事务导致比使用显式锁定更低的延迟。)

这是我能找到的唯一相关问题: How does pgbouncer behave when transaction pooling is enabled and a single statement is issued? – 但这并不能解决我的问题。阅读答案表明,如果自上次查询以来未超过超时时咨询锁为 taken/released,则上述内容应该有效,但我不知道这是否可信。

我使用以下 pgbouncer 配置对此进行了测试:

[pgbouncer]
pool_mode = transaction
server_reset_query = DISCARD ALL
server_check_query = select 1
server_reset_query_always = 0

max_client_conn = 2000
default_pool_size = 1
min_pool_size = 1

reserve_pool_size = 0
reserve_pool_timeout = 5

max_db_connections = 1

和两个通过 pgbouncer 到同一个数据库的并行连接。以下使用 P1 ja P2 作为每个进程发送的查询的标识符。如果做一个简单的测试,例如:

P1: set application_name=p1;
P1: select pg_advisory_lock(42);
P2: set application_name=p2;
P1: show application_name;
    p1
P2: select pg_advisory_lock(42);
P2. show application_name;
    p2

... 似乎两个连接都能够获得相同的独占咨询锁,即使它们具有唯一的 application_name 值,所以每个连接都应该是唯一的!实际上,所有命令都已使用单个连接发送到 postgres,因此该单个连接已两次获得咨询锁 42,如果稍后释放相同的锁两次(逻辑上每个 P1 和 P2 连接一次),一切都很好。发生这种情况是因为排他锁可以被同一个所有者多次获取,并且在任何其他进程可以获得相同的锁之前必须释放同样多的次数。

因此,也可能出现以下情况:

P1: select pg_advisory_lock(42);
P2: select pg_advisory_lock(42);
P1: begin transaction isolation level serializable;
P1: select 1;
P1: commit;
P2: begin transaction isolation level serializable;
P2: select 1;
P2: commit;
P1: select pg_advisory_unlock(42);
P2: select pg_advisory_unlock(42);

想象一下多个查询和数据库更改而不是上面的 select 1

请注意,两个数据库客户端应用程序在执行可序列化事务之前都获得了独占锁,但仍然无法正常工作。

显然这种情况是不可能的,除非P1 和P2 实际上共享同一个数据库连接。通常一个人会通过 pgbouncer 允许多个并行数据库连接,所以这种代码是活泼的。实际上,它会一直工作,直到您达到高服务器负载,然后您会遇到随机失败,具体取决于哪个查询与来自另一个客户端的另一个查询一起执行。

因此,如果 pool_mode=transaction 处于活动状态,则在事务外进行任何查询绝对不安全。选项 server_reset_query_always=1 不能真正解决问题,永远不要使用。如果您觉得需要 server_reset_query_always=1,则需要使用 pool_mode=session,否则您将面临随机数据损坏的风险。

此外,pgbouncer 似乎足够聪明来伪造一些 连接特定数据。例如,当 P1 在上面的第一个示例中设置其 application_name 并在 P2 已经在同一连接上设置其应用程序名称后查询它时,P1 将获得预期的结果。但是,如果您在发生这种情况时监视 pg_stat_activity,则每次 P1 或 P2 发送查询时,与 postgres 的唯一活动连接都会更改其 application_name 值。这使得显得这种混用pool_mode=transaction是可以的。

最后,在事务外设置 application_name 应该是安全的,但实际上在 postgres 级别上实现的任何功能都是不安全的。除非您绝对确定 pgbouncer 可以模拟您需要的功能,否则不要对 pool_mode=transaction 中从 pgbouncer 获取的数据库连接发出任何查询,begin ... 除外,commit ...rollback。一旦事务处于活动状态,连接就会为您保留,并且一切正常,就像与 postgres 的真正直接连接一样,直到您执行 commitrollback.

如果设置了 pool_mode=transaction,我真的希望 pgbouncer 总是 return 每当客户端尝试在事务外进行任何查询时出错。不幸的是,这不是我们生活的现实,pgbouncer 客户必须小心。