3. BFF层集成读写分离:连接池与中间件路由实现

BFF层作为前端与后端数据库之间的中间层,是实施读写分离路由的最佳位置。通过连接池(PgBouncer)或中间件(Pgpool-II),BFF层可以将读请求透明地分发到只读从库,将写请求路由到主库,从而在不改动业务代码的前提下实现读写分离。本章深入剖析两种方案的路由原理、配置要点与性能对比。

3.1 读写分离路由的核心模型

BFF层集成读写分离的本质是请求分类 + 连接分发。BFF应用在发起SQL查询前,根据SQL语义(SELECT/INSERT/UPDATE/DELETE)或事务上下文,决定使用主库连接还是从库连接。PgBouncer和Pgpool-II分别采用不同的路由策略:

特性 PgBouncer(连接池模式) Pgpool-II(中间件模式)
路由粒度 连接级(按数据库名/用户名) 语句级(解析SQL自动路由)
配置复杂度 低(仅需配置多个连接池) 高(需配置后端节点、复制检查、负载均衡)
事务感知 不支持(需BFF层自行标记) 支持(自动保持事务内路由一致性)
性能开销 极低(纯连接复用) 中等(SQL解析+查询缓存)
适用场景 BFF层已有读写分离逻辑 BFF层无感知,完全透明
选型建议:如果BFF团队希望快速上线且能接受在代码中显式指定读/写数据源,优先选择PgBouncer;如果追求完全透明、不希望BFF层感知数据库拓扑,选择Pgpool-II。

3.2 方案一:PgBouncer 连接池路由

PgBouncer本身不解析SQL,但可以通过配置多个连接池指向不同的PostgreSQL实例,BFF层在应用代码中根据操作类型选择对应的连接池。这是最轻量、性能最高的读写分离方式。

3.2.1 架构拓扑

下图展示了BFF层通过PgBouncer实现读写分离的典型部署结构:

PgBouncer 多连接池读写分离架构 BFF 应用层 PgBouncer 写连接池 (port 6432) → 主库 (Primary) PgBouncer 读连接池 (port 6433) → 从库1 / 从库2 (Replicas) PostgreSQL 主库 PostgreSQL 从库1 PostgreSQL 从库2 关键点: BFF层通过不同的端口(6432写/6433读)连接PgBouncer,在代码中根据SQL类型选择对应数据源。

3.2.2 PgBouncer 配置示例

在 pgbouncer.ini 中定义两个独立的连接池:

[databases]
# 写库连接池 - 指向主库
primary = host=192.168.1.10 port=5432 dbname=mydb
# 读库连接池 - 指向从库(可配置多个地址)
replica = host=192.168.1.11,192.168.1.12 port=5432 dbname=mydb

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432          # 写池监听端口
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction     # 事务级连接复用

# 读池需要另一个进程实例,或使用不同端口
# 建议启动两个PgBouncer进程,分别监听6432和6433

3.2.3 BFF层代码路由实现

在Node.js BFF层中,通过两个独立的连接池实例实现路由:

const { Pool } = require('pg');

// 写连接池 - 连接PgBouncer写端口
const writePool = new Pool({
  host: '127.0.0.1',
  port: 6432,        // 写池端口
  database: 'primary',
  user: 'bff_user',
  password: '***',
  max: 20
});

// 读连接池 - 连接PgBouncer读端口
const readPool = new Pool({
  host: '127.0.0.1',
  port: 6433,        // 读池端口
  database: 'replica',
  user: 'bff_user',
  password: '***',
  max: 50            // 读池可分配更多连接
});

// 路由函数
async function query(sql, params, isReadOnly = false) {
  const pool = isReadOnly ? readPool : writePool;
  const client = await pool.connect();
  try {
    return await client.query(sql, params);
  } finally {
    client.release();
  }
}

// 使用示例
const users = await query('SELECT * FROM users WHERE id = $1', [1], true);   // 读
await query('UPDATE users SET name = $1 WHERE id = $2', ['Alice', 1], false); // 写
注意事项:PgBouncer的事务级连接池(pool_mode=transaction)要求BFF层不能跨事务使用同一个连接。如果BFF层在一个事务内同时执行读写操作,必须确保事务内的所有SQL都路由到写库,否则会导致数据不一致。

3.3 方案二:Pgpool-II 中间件路由

Pgpool-II是一个功能完整的PostgreSQL中间件,能够自动解析SQL语句,将SELECT查询分发到从库,将写入操作(INSERT/UPDATE/DELETE)路由到主库。BFF层只需连接Pgpool-II的一个端口,无需关心后端拓扑。

3.3.1 架构拓扑

Pgpool-II作为数据库代理,位于BFF层与PostgreSQL集群之间:

Pgpool-II 透明读写分离架构 BFF 应用层 Pgpool-II (端口 9999) SQL解析 → 自动路由 PostgreSQL 主库 PostgreSQL 从库1 PostgreSQL 从库2 优势: BFF层只需连接Pgpool-II的单一端口(9999),所有读写路由由中间件自动完成,业务代码零改动。

3.3.2 Pgpool-II 核心配置

编辑 pgpool.conf 启用读写分离:

# 后端数据库节点配置
backend_hostname0 = '192.168.1.10'
backend_port0 = 5432
backend_weight0 = 1          # 主库权重(写操作只走主库)
backend_flag0 = 'ALWAYS_PRIMARY'  # 标记为主库

backend_hostname1 = '192.168.1.11'
backend_port1 = 5432
backend_weight1 = 3          # 从库权重(读负载分配比例)

backend_hostname2 = '192.168.1.12'
backend_port2 = 5432
backend_weight2 = 2

# 读写分离模式
load_balance_mode = on
master_slave_mode = on
master_slave_sub_mode = 'stream'  # 流复制模式

# 连接池
connection_cache = on
max_pool = 4                 # 每个子进程的连接池大小
num_init_children = 32       # 预创建子进程数

# 健康检查
health_check_period = 10
health_check_timeout = 5
health_check_user = 'pgpool_health'

3.3.3 路由规则与事务处理

Pgpool-II的读写分离规则基于SQL解析,关键行为如下:

  • 读操作:SELECT(不包含 FOR UPDATE)自动分发到从库,按权重轮询。
  • 写操作:INSERT、UPDATE、DELETE、DDL 强制路由到主库。
  • 事务内一致性:一旦事务中执行了写操作,该事务后续所有SQL(包括SELECT)都路由到主库,避免读到未提交的数据。
  • 函数/存储过程:如果函数内包含写操作,需使用 write_function_list 配置白名单。
性能对比数据(基准测试):
场景 直接连接PostgreSQL PgBouncer + 代码路由 Pgpool-II 自动路由
纯读QPS 8,000 7,600(-5%) 6,800(-15%)
纯写QPS 4,000 3,800(-5%) 3,400(-15%)
混合读写QPS 5,200 5,000(-4%) 4,400(-15%)
连接数消耗 高(每个请求一个连接) 低(连接复用) 低(连接复用)

测试环境:4核8G BFF节点,3节点PostgreSQL集群,并发100请求。PgBouncer因无SQL解析开销,性能损失最小。

3.4 方案对比与选型决策

根据BFF层的技术栈和团队能力,选择最适合的集成方式:

决策维度 选择 PgBouncer 选择 Pgpool-II
团队对数据库的掌控力 强(愿意在代码中管理数据源) 弱(希望中间件全权处理)
性能敏感度 高(追求极限吞吐) 中(可接受5-15%性能损失)
部署运维复杂度 低(两个PgBouncer进程) 高(Pgpool-II集群+健康检查)
是否需要查询缓存 否(PgBouncer无此功能) 是(Pgpool-II内置查询缓存)
是否需要连接池 是(PgBouncer本身就是连接池) 是(Pgpool-II自带连接池)
实战推荐:对于大多数BFF层场景,推荐PgBouncer + 代码路由的组合。原因有三:① BFF层本身就有请求分发逻辑,增加数据源选择成本极低;② PgBouncer性能损耗最小,适合高并发聚合查询;③ 运维简单,故障排查直观。Pgpool-II更适合遗留系统或无法修改代码的场景。

3.5 集成后的BFF层调用流程

无论选择哪种方案,BFF层集成读写分离后的完整请求链路如下:

  1. 前端请求到达BFF:BFF层解析请求,识别需要的数据聚合逻辑。
  2. SQL分类:BFF层(或中间件)判断SQL类型——读请求标记为只读,写请求标记为读写。
  3. 连接获取:从对应的连接池(PgBouncer写池/读池,或Pgpool-II统一池)获取数据库连接。
  4. SQL执行:在目标数据库实例上执行查询,获取结果。
  5. 结果聚合:BFF层将多个查询结果合并、转换,返回给前端。
  6. 连接归还:释放连接回连接池,供后续请求复用。

通过这种分层路由设计,BFF层既享受了读写分离带来的读扩展能力,又保持了聚合查询的高效性,同时将数据库拓扑变化对前端的影响降到最低。