暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

使用pgcat实现EDB EPAS事务池化

新智锦绣 2025-12-12
42

点击蓝字关注我们



EDB EPAS/PostgreSQL为每个连接的客户端分配⼀个独⽴的进程,称为客户端后端。出于以下⼏个⽅⾯的考虑,通常会将最⼤客户端连接数限制在⼏百个:
1)资源竞争;
2)延迟;
3)两者兼⽽有之。
随之,引⼊连接池⼯具如[pgbouncer]或pgcat。
这些池化⼯具通过将多个客户端连接透明地多路复⽤到单个EPAS/PostgreSQL客户端后端上,从⽽能够处理更多连接(并降低延迟)。本⽂介绍通过pgcat连接edb epas的池化功能。


pgcat 和 EDB EPAS


EPAS安装部署已经在本号前⾯⽂章⾥介绍过多次,这⾥不再赘述。环境是EDB AS17,端⼝5444,访问数据库设为edb。
对于pgcat,edb默认没有提供扩展包,我们从GitHub下载。
获取并构建pgcat:
    $ git clone https://github.com/postgresml/pgcat.git
    $ cd pgcat

    pgcat包默认会创建一个postgresql,本例我只需要pgcat,不需要创建postgresql,所以修改docker-compose.yml如下:

      $ cat docker-compose.yml
      version: "3"
      services:
        pgcat:
          build: .
          network_mode: host  ## epas和pgcat在同一主机,所以配置docker使用host网络
          command:
            - "pgcat"
            - "/etc/pgcat/pgcat.toml"
          volumes:
            - "${PWD}/examples/docker/pgcat.toml:/etc/pgcat/pgcat.toml"
          ports:
            - "9930:9930"
            - "6432:6432"

      pgcat需要一个配置文件,但仓库中自带的配置文件涉及太多功能,这里为了演示,简化配置。docker启动时会读./examples/docker/pgcat.toml配置文件内容,pool_mode默认transaction事务模式内容如下:

        $  cat examples/docker/pgcat.toml
        [general]
        port = 6432
        idle_timeout = 60000
        worker_threads = 8
        wait_time_before_ban = 0
        admin_port = 9930
        admin_username = "admin"
        admin_password = "admin"


        [pools.edb]


          # shard 0
          [pools.edb.shards.0]
          database = "edb"


            [[pools.edb.shards.0.servers]]
            host = "host.containers.internal"
            port = 5444
            role = "primary"


          [[pools.edb.databases]]
          database = "edb"
          shard = 0


          [pools.edb.users.0]
          username = "enterprisedb"
          password = "enterprisedb"
          pool_size = 1

        告诉pgcat如何连接以及在哪里连接,并且pgcat本身应该在6432端口运行。本文后续部分唯一会调整的配置就是pool_size。

        启动docker容器pgcat:

          $ sudo docker compose up

          如何不出意外,这时pgcat容器应该正常启动,终端显示一些信息:

            [pgcat] | 2025-12-08T02:46:51.928418Z  INFO ThreadId(01) pgcat::pool: [pool: edb][user: enterprisedb] creating new pool    
            [pgcat] | 2025-12-08T02:46:51.928468Z  INFO ThreadId(01) pgcat: Config autoreloader: disabled    
            [pgcat] | 2025-12-08T02:46:51.928481Z  INFO ThreadId(01) pgcat: Waiting for clients    
            [pgcat] | 2025-12-08T02:46:51.928637Z  INFO ThreadId(04) pgcat::pool: Creating a new server connection Address { id: 0, host: "host.containers.internal", port: 5444, shard: 0, database: "edb", role: Primary, replica_number: 0, address_index: 0, username: "enterprisedb", pool_name: "edb", mirrors: [], stats: AddressStats { total: AddressStatFields { xact_count: 0, query_count: 0, bytes_received: 0, bytes_sent: 0, xact_time: 0, query_time: 0, wait_time: 0, errors: 0 }, current: AddressStatFields { xact_count: 0, query_count: 0, bytes_received: 0, bytes_sent: 0, xact_time: 0, query_time: 0, wait_time: 0, errors: 0 }, averages: AddressStatFields { xact_count: 0, query_count: 0, bytes_received: 0, bytes_sent: 0, xact_time: 0, query_time: 0, wait_time: 0, errors: 0 }, averages_updated: false }, error_count: 0 }    
            [pgcat] | 2025-12-08T02:46:51.928855Z  INFO ThreadId(06) pgcat::stats: Events reporter started    
            [pgcat] | 2025-12-08T02:47:19.895497Z  WARN ThreadId(05) pgcat: Client disconnected with error ClientSocketError("password code", ClientIdentifier { application_name: "psql", username: "enterprisedb", pool_name: "edb" })    
            [pgcat] | 2025-12-08T02:48:21.931056Z  INFO ThreadId(05) pgcat::server: Server connection closed Address { id: 0, host: "host.containers.internal", port: 5444, shard: 0, database: "edb", role: Primary, replica_number: 0, address_index: 0, username: "enterprisedb", pool_name: "edb", mirrors: [], stats: AddressStats { total: AddressStatFields { xact_count: 0, query_count: 0, bytes_received: 0, bytes_sent: 0, xact_time: 0, query_time: 0, wait_time: 0, errors: 0 }, current: AddressStatFields { xact_count: 0, query_count: 0, bytes_received: 0, bytes_sent: 0, xact_time: 0, query_time: 0, wait_time: 0, errors: 0 }, averages: AddressStatFields { xact_count: 0, query_count: 0, bytes_received: 0, bytes_sent: 0, xact_time: 0, query_time: 0, wait_time: 0, errors: 0 }, averages_updated: false }, error_count: 0 }, session duration: 0d 00:01:29.990    
            [nuser@epas-1 ~]$ sudo docker ps 
            Emulate Docker CLI using podman. Create etc/containers/nodocker to quiet msg.
            CONTAINER ID  IMAGE                         COMMAND               CREATED      STATUS        PORTS       NAMES
            aee70d4a1cd9  localhost/pgcat_pgcat:latest  pgcat etc/pgcat/...  8 hours ago  Up 5 minutes              pgcat_pgcat_1


            共享的理解


            现在EPAS运行着,pgcat也运行着。用psql连接到pgcat:

              $ [nuser@epas-1 ~]$ psql -h 192.168.31.101 -U enterprisedb -p 6432 edb
              ⽤户 enterprisedb 的⼝令:
              psql (17.6.0)
              输⼊ "help" 来获取帮助信息.
              edb=#
              edb=#
              edb=#

              pgcat正在拦截客户端(psql)和epas客户端后端之间的所有交互。但从客户端本身很难判断连接的是池化工具还是直接连接到epas。

              编辑这个psql会话的提示符,以便在添加更多连接时更容易区分:

                edb=# \set PROMPT1 '[client1] %/%R%x%# '
                [client1] edb=# \set PROMPT2 '[client1] %/%R%x%# '
                [client1] edb=#

                现在运行一个简单的查询:

                  [client1] edb=# SELECT 'hello world';
                    ?column?
                  -------------
                  hello world
                  (1 row)

                  检查一下查询实际在哪个PostgreSQL客户端后端PID上执行:

                    [client1] edb=# SELECT pg_backend_pid();
                     pg_backend_pid
                    ----------------
                              89583
                    (1 row)

                    现在做一些会话本地的事情,比如修改时区。假设在纽约,所以时区是Asia/Shanghai:

                      [client1] edb=# SHOW timezone;
                           TimeZone
                      ------------------
                       Asia/Shanghai
                      (1 row)

                      现在把它改成America/Los_Angeles:

                        [client1] edb=# SET timezone TO 'America/Los_Angeles';
                        SET
                        [client1] edb=# SHOW timezone;
                              TimeZone
                        ---------------------
                         America/Los_Angeles
                        (1 row)

                        保持这个psql会话打开,在另一个终端中再打开一个psql会话:

                          $ psql -h localhost -U enterprisedb -p 6432 edb
                          ⽤户 enterprisedb 的⼝令:
                          psql (17.6.0)
                          输⼊ "help" 来获取帮助信息.
                          edb=#

                          也设置一下这个会话的提示符以便区分:

                            edb=# \set PROMPT1 '[client2] %/%R%x%# '
                            [client2] edb=# \set PROMPT2 '[client2] %/%R%x%# '
                            [client2] edb=#

                            在client2中时区应该是Asia/Shanghai,因为这是默认值:

                              [client2] postgres=# SHOW timezone;
                                   TimeZone
                              ------------------
                               Asia/Shanghai
                              (1 row)

                              现在检查epas客户端后端PID:

                                [client2] edb=# SELECT pg_backend_pid();
                                 pg_backend_pid
                                ----------------
                                          89583
                                (1 row)

                                在EPAS/PostgreSQL中,如果直接连接到数据库,这些PID应该是唯一的。一个客户端对应一个进程。但现在看到多个客户端之间共享同一个进程。

                                回到client1检查一下timezone设置,确保它没有被重置:

                                  [client1] postgres=# SHOW timezone;
                                        TimeZone
                                  ---------------------
                                   America/Los_Angeles
                                  (1 row)

                                  仍然是它自己的值。因此,池化工具看起来好像每个客户端都有自己的会话,就像每个客户端都有自己独立的EPAS/PostgreSQL客户端后端一样,即使有多个客户端共享同一个EPAS/PostgreSQL客户端后端。


                                  Pool_size=1


                                  这些池化工具的基本工作原理是维护一个打开的EPAS客户端后端连接池。在事务池化模式下,它会从池中获取一个客户端后端连接,并在事务结束时释放它。EPAS中的每个语句都是一个事务,因此每个客户端仅在语句执行期间持有池化连接。

                                  如果一个客户端运行一个执行时间较长的语句,它可能会让其他客户端无法访问池化连接。当设置pool_size = 1时,这种情况最容易观察到,因为只允许池化工具拥有单个打开的epas客户端后端。

                                  例如,在client2上调用pg_sleep()15秒。它会挂起15秒才会返回:

                                    [client2] edb=# SELECT pg_sleep(15);
                                     pg_sleep
                                    ----------


                                    (1 row)

                                    在client2上按下Enter键后,立即在client1上运行SELECT 1。client1会挂起几秒钟,然后返回错误:

                                      [client1] edb=# SELECT 1;
                                      FATAL:  could not get connection from the pool - AllServersDown

                                      但一旦client2的sleep完成并且语句执行完毕,client1就能够再次运行命令:

                                        [client1] edb=# SELECT 1;
                                        FATAL:  could not get connection from the pool - AllServersDown


                                        -- 稍等片刻后 --


                                        [client1] edb=# SELECT 1;
                                         ?column?
                                        ----------
                                        1
                                        (1 row)

                                        说明此时在共享同一个PostgreSQL客户端后端!

                                        接下来来试试交互式事务。

                                        在client1上用一个BEGIN启动交互式事务:

                                          [client1] edb=# BEGIN;
                                          BEGIN
                                          [client1] edb=*#

                                          在client2上尝试运行SELECT 1:

                                            [client2] edb=# select 1;
                                            FATAL:  could not get connection from the pool - AllServersDown

                                            它挂起并失败了。然后在client1的事务中运行一个查询,然后提交:

                                              [client1] edb=# BEGIN;
                                              BEGIN
                                              [client1] edb=*# SELECT 1;
                                               ?column?
                                              ----------
                                                      1
                                              (1 row)


                                              [client1] edb=*# COMMIT;
                                              COMMIT

                                              现在回到client2,一旦client1的事务完成,就可以再次访问PostgreSQL客户端后端:

                                                [client2] edb=# SELECT 1;
                                                FATAL:  could not get connection from the pool - AllServersDown


                                                -- client1提交后


                                                [client2] edb=# SELECT 1;
                                                 ?column?
                                                ----------
                                                        1
                                                (1 row)

                                                所以当只允许一个EPAS/PostgreSQL客户端后端时,可以观察到这种行为。现在2个客户端后端看看会发生什么。


                                                pool_size = 2


                                                将pool_size从1改为2。然后Ctrl-c停止pgcat服务器并重新运行它。关闭两个psql会话并启动两个新的会话。给它们同样的提示符前缀[client1]和[client2]。

                                                查看client1当前的EPAS客户端后端PID:

                                                  $ [nuser@epas-1 ~]$ psql -h 192.168.31.101 -U enterprisedb -p 6432 edb
                                                  ⽤户 enterprisedb 的⼝令:
                                                  psql (17.6.0)
                                                  输⼊ "help" 来获取帮助信息.
                                                  edb=# \set PROMPT1 '[client1] %/%R%x%# '
                                                  [client1] edb=# \set PROMPT2 '[client1] %/%R%x%# '
                                                  [client1] edb=#
                                                  [client1] edb=# SELECT pg_backend_pid();
                                                   pg_backend_pid
                                                  ----------------
                                                            91137
                                                  (1 row)

                                                  对client2做同样的操作:

                                                    $ psql -h 192.168.31.101 -U enterprisedb -p 6432 edb
                                                    ⽤户 enterprisedb 的⼝令:
                                                    psql (17.6.0)
                                                    输⼊ "help" 来获取帮助信息.
                                                    edb=# \set PROMPT1 '[client2] %/%R%x%# '
                                                    [client2] edb=# \set PROMPT2 '[client2] %/%R%x%# '
                                                    [client2] edb=# SELECT pg_backend_pid();
                                                     pg_backend_pid
                                                    ----------------
                                                              91137
                                                    (1 row)

                                                    可以看出,即使允许pgcat使用多个PostgreSQL客户端后端,它仍然只使用一个。

                                                    如果现在有一个阻塞的客户端连接,如果在client2上启动一个交互式事务:

                                                      [client2] edb=# BEGIN;
                                                      BEGIN
                                                      [client2] edb=*#

                                                      看看它的客户端后端PID是什么:

                                                        [client2] edb=# BEGIN;
                                                        BEGIN
                                                        [client2] edb=*# SELECT pg_backend_pid();
                                                         pg_backend_pid
                                                        ----------------
                                                                  91137
                                                        (1 row)

                                                        还是和之前一样的那个。让这个事务保持打开状态。

                                                        然后在client1上执行一个简单的SELECT 1:

                                                          [client1] edb=# SELECT 1;
                                                           ?column?
                                                          ----------
                                                                  1
                                                          (1 row)

                                                          看看client1运行在哪个客户端后端PID上:

                                                            [client1] edb=# SELECT pg_backend_pid();
                                                             pg_backend_pid
                                                            ----------------
                                                                      91250
                                                            (1 row)

                                                            和之前的不同!因此,由于一个客户端阻塞了连接的使用,而pool_size允许使用更多连接,pgcat就打开了一个新的EPAS客户端后端供客户端连接使用。

                                                            现在提交client2上的事务,并检查它在该事务之后的EPAS客户端后端PID:

                                                              [client2] postgres=# BEGIN;
                                                              BEGIN
                                                              [client2] postgres=*# SELECT pg_backend_pid();
                                                               pg_backend_pid
                                                              ----------------
                                                                        20809
                                                              (1 row)


                                                              [client2] postgres=*# COMMIT;
                                                              COMMIT
                                                              [client2] postgres=# SELECT pg_backend_pid();
                                                               pg_backend_pid
                                                              ----------------
                                                                        91250
                                                              (1 row)

                                                              再检查一次。再一次。再一次。

                                                                [client2] postgres=# [client2] postgres=# SELECT pg_backend_pid();
                                                                 pg_backend_pid
                                                                ----------------
                                                                          91137
                                                                (1 row)


                                                                [client2] postgres=# SELECT pg_backend_pid();
                                                                 pg_backend_pid
                                                                ----------------
                                                                          91250
                                                                (1 row)


                                                                [client2] postgres=# SELECT pg_backend_pid();
                                                                 pg_backend_pid
                                                                ----------------
                                                                          91137
                                                                (1 row)

                                                                client1也一样:

                                                                  [client1] postgres=# SELECT pg_backend_pid();
                                                                   pg_backend_pid
                                                                  ----------------
                                                                            91250
                                                                  (1 row)


                                                                  [client1] postgres=# SELECT pg_backend_pid();
                                                                   pg_backend_pid
                                                                  ----------------
                                                                            91137
                                                                  (1 row)


                                                                  [client1] postgres=# SELECT pg_backend_pid();
                                                                   pg_backend_pid
                                                                  ----------------
                                                                            91250
                                                                  (1 row)

                                                                  所以基本上,逻辑是客户端会随机地从现有连接池中获取连接,除非所有现有连接都被阻塞,否则不会创建新的连接。每个客户端可能在每次事务中都运行在完全不同的PostgreSQL客户端后端上。


                                                                  会话池化


                                                                  连接池工具可能支持几种池化模式,session会话模式,transaction事务模式和statement语句模式。

                                                                  在会话池化模式下,每个客户端连接都映射到一个EPAS/PostgreSQL客户端后端。就像直接连接到EPAS/PostgreSQL一样。这意味着像LISTEN/NOTIFY这样的功能可以毫无问题地工作。只是当客户端连接结束后,池化工具不会让EPAS/PostgreSQL客户端后端退出,而是保留它以便将来客户端连接复用。

                                                                  会话池化有助于避免建立连接的延迟,同时可以帮助扩展客户端连接数量,因为池化工具会将连接排队到可配置的超时时间,而PostgreSQL一旦达到max_connections[1]就会立即断开连接。对于扩展客户端连接数量的效果不如事务池化那么显著。

                                                                  基本上,池化工具默认使用事务池化,因为这种模式可以处理更多连接。当用户需要事务池化不支持的功能或行为时,就会切换到会话池化。

                                                                  关于会话池化,不在些做验证,读者可以自行验证学习。


                                                                  引用链接

                                                                  [1]max_connections:https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS



                                                                  关于公司

                                                                  感谢您关注新智锦绣科技(北京)有限公司!作为 Elastic 的 Elite 合作伙伴及 EnterpriseDB 在国内的唯一代理和服务合作伙伴,我们始终致力于技术创新和优质服务,帮助企业客户实现数据平台的高效构建与智能化管理。无论您是关注 Elastic 生态系统,还是需要 EnterpriseDB 的支持,我们都将为您提供专业的技术支持和量身定制的解决方案。


                                                                  欢迎关注我们,获取更多技术资讯和数字化转型方案,共创美好未来!

                                                                  Elastic 微信群

                                                                  EDB 微信群


                                                                  发现“分享”和“赞”了吗,戳我看看吧








                                                                  文章转载自新智锦绣,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                                  评论