# Pigsty 服务接入层

LLMS 索引： [llms.txt](/llms.txt)

---

Pigsty 不发明 PostgreSQL 的主库、副本或 session 语义。它把：

```text
inventory intent
  -> Patroni role API
      -> HAProxy service
          -> PgBouncer/PostgreSQL destination
              -> DNS/VIP/client service material
                  -> metrics and administration
```

组合成可交付实现。

理解 Pigsty 服务层的关键不是记命令，而是能把任何观察反向映射到原生组件。

## 22.6.1 服务定义、角色选择与端口 {#item-22-6-1}

### 默认变量

本章参考实现的关键声明形态：

```yaml
pgbouncer_enabled: true
pgbouncer_port: 6432
pgbouncer_poolmode: transaction
pgbouncer_sslmode: disable

pg_service_provider: ''
pg_default_service_dest: pgbouncer
pg_default_services:
  - { name: primary, port: 5433, dest: default,
      check: /primary, selector: "[]" }
  - { name: replica, port: 5434, dest: default,
      check: /read-only, selector: "[]",
      backup: "[? pg_role == `primary` || pg_role == `offline` ]" }
  - { name: default, port: 5436, dest: postgres,
      check: /primary, selector: "[]" }
  - { name: offline, port: 5438, dest: postgres,
      check: /replica,
      selector: "[? pg_role == `offline` || pg_offline_query ]",
      backup: "[? pg_role == `replica` && !pg_offline_query]" }
```

版本和自定义配置可能不同。读取实际 inventory、role defaults 与 rendered
file，不要把这段当成跨版本常量。

### `dest`

服务 destination 可以表达：

```text
default     使用 pg_default_service_dest
postgres    PostgreSQL pg_port，常见 5432
pgbouncer   PgBouncer pgbouncer_port，常见 6432
number      指定端口
```

因此：

```text
primary/replica dest=default
```

在本章 `pg_default_service_dest=pgbouncer` 时走连接池；如果用户改成
`postgres`，同一个 5433/5434 就绕过池。

端口名不能替实际路径。

### `check`

`check` 是 Patroni REST health path：

```text
/primary
/replica
/read-only
```

HAProxy 对成员的 `8008` 检查，数据流则去 `dest`。角色判断来自 Patroni，
不是 HAProxy 解析 PostgreSQL protocol。

### `selector`

selector 从 cluster member inventory 中选普通 backend。

```yaml
selector: "[]"
```

表示全集。

offline 示例只选择：

```text
pg_role == offline OR pg_offline_query
```

这让平台能把特定 replica 标为重查询目标。selector 是期望集合，运行时健康
检查仍可能摘除不合格成员。

### `backup`

backup selector 形成 HAProxy backup server。它决定正常集合不可用时是否
降级。

要把 backup 语义写进服务合同：

- replica 回 primary 是否允许；
- offline 回普通 replica 是否允许；
- backup 激活是否告警；
- 目标是否有容量；
- client `target_session_attrs` 会接受还是拒绝。

### `pg_service_provider`

默认空值通常在每个 PostgreSQL node 上交付 local HAProxy service。

也可指定专用 HAProxy node group。此时要重新设计：

- provider 高可用；
- provider 到数据库网络；
- DNS/VIP/multi-host；
- config rollout；
- source IP/HBA；
- stats/metrics；
- 故障域。

把 HAProxy 从数据库节点移出，不自动获得入口 HA。

### VIP 与 DNS

相关声明包括：

```yaml
pg_vip_enabled: false
pg_vip_address: 127.0.0.1/24
pg_vip_interface: auto
pg_dns_suffix: ''
pg_dns_target: auto
```

这是交付入口的机制选择。启用前要按第 22.1.3 节验证网络、仲裁、DNS cache
与证书，不能因为变量存在就宣称通过。

### 自定义业务服务

可以在 `pg_services` 添加服务，而不是修改默认列表。例如概念上：

```yaml
pg_services:
  - name: shop-ro
    port: 5444
    dest: pgbouncer
    check: /read-only
    selector: "[? pg_role == `replica` && !pg_offline_query]"
```

生产声明还应补：

- backup/fail-closed；
- maxconn；
- balance；
- options/rise/fall；
- owner 和用途；
- TLS/网络；
- driver endpoint。

不要为每个应用随意开端口；只有语义或资源/失败域不同才需要新服务。

## 22.6.2 PgBouncer、HAProxy 与数据库的证据链 {#item-22-6-2}

### 第一步：声明证据

从 reviewed inventory 提取 secret-free projection：

```text
cluster/member addresses
pg_role/pg_offline_query
pg_services/default services
default destination
PgBouncer mode/port/budget
VIP/DNS/provider
declared users and pgbouncer participation
```

凭据值不进入报告，只记录：

```text
present
source class
rotation owner
```

### 第二步：rendered HAProxy

本章 `/etc/haproxy/pg-test-*.cfg` 投影：

```text
primary  :5433 -> all members :6432, /primary
replica  :5434 -> members :6432, /read-only, primary backup
default  :5436 -> all members :5432, /primary
offline  :5438 -> pg-test-3 :5432, pg-test-2 backup, /replica
```

同时核对：

```text
bind/mode/maxconn
balance
health method/path/status
inter/fastinter/downinter
rise/fall
shutdown-sessions
slowstart
backend maxconn/maxqueue
member address/destination/check port/backup
```

只核对文件 diff 仍不够；进程可能未 reload 或 runtime state 不同。

### 第三步：HAProxy runtime

从 stats socket/API 看：

```text
frontend OPEN
backend UP/DOWN
health code
last state change
sessions/queue
backup activation
```

敏感 stats user/password 不输出。使用 local protected socket 比把管理页面凭据
写入脚本更安全。

### 第四步：PgBouncer config

在 local Unix admin socket：

```sql
SHOW CONFIG;
SHOW DATABASES;
SHOW USERS;
SHOW POOLS;
SHOW STATS;
```

本章 safe projection：

```text
pool_mode=transaction
listen_addr=0.0.0.0
listen_port=6432
max_client_conn=20000
default_pool_size=50
reserve_pool_size=30
reserve_pool_timeout=1
query_wait_timeout=120
max_db_connections=100
max_user_connections=100
max_prepared_statements=256
server_reset_query=DISCARD ALL
server_reset_query_always=0
client_tls_sslmode=disable
unix_socket_dir=/run/postgresql
```

不要采集：

- password；
- SCRAM verifier；
- auth file 内容；
- inventory secret；
- admin credential。

### 数据库 LOGIN 不等于池化身份已交付

本章开发过程中故意撞到一个重要边界：

```text
CREATE ROLE pg36_ch22_app LOGIN PASSWORD ...
direct PostgreSQL auth works
PgBouncer auth fails
```

因为本章 Pigsty 默认：

```yaml
pgbouncer_auth_query: false
```

PgBouncer authentication surface 由声明式用户清单管理。只有数据库 catalog
里存在 role，不等于 pooler 的 auth file/query 已经认识它。

生产用户应在 Pigsty `pg_users` 中声明并明确：

```yaml
- name: pg36_shop_app
  password: <secret reference/material>
  pgbouncer: true
```

实际字段与 secret workflow 以当前版本文档和组织规范为准。不要手改
`userlist.txt` 制造不可追踪漂移。

本章 formal run 因此使用既有、Pigsty 已声明的 nonproduction `test` 用户，
只创建专属 schema/table；脚本永不修改或删除该 role。

若启用 `pgbouncer_auth_query`，还要评审：

- `auth_user` 与查询权限；
- query 在 replica/primary 的行为；
- password rotation；
- role expiration；
- auth database；
- failover；
- secret exposure。

### 第五步：PostgreSQL 原生状态

对每个 member：

```sql
SELECT pg_is_in_recovery(),
       current_setting('transaction_read_only'),
       current_setting('cluster_name'),
       current_setting('port'),
       pg_postmaster_start_time();
```

并观察连接预算：

```sql
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
  'max_connections',
  'superuser_reserved_connections',
  'reserved_connections',
  'idle_in_transaction_session_timeout',
  'statement_timeout',
  'max_locks_per_transaction',
  'work_mem',
  'temp_buffers'
);
```

`pg_postmaster_start_time()` 在本章用来把经过 local Unix socket 的 PgBouncer
session 映射回具体 member；`inet_server_addr()` 对 Unix backend 可能为空。

### 第六步：从 client 走完整路径

每个 service 用真实 database/user：

```sql
SELECT pg_is_in_recovery(),
       current_setting('transaction_read_only')::boolean,
       current_setting('cluster_name'),
       current_setting('port')::integer,
       pg_backend_pid(),
       pg_postmaster_start_time();
```

预期：

| service | recovery | read_only | path |
|---|---:|---:|---|
| primary 5433 | false | false | HAProxy → PgBouncer |
| replica 5434 | true | true | HAProxy → PgBouncer |
| default 5436 | false | false | HAProxy → PostgreSQL |
| offline 5438 | true | true | HAProxy → PostgreSQL |

再到每台 PgBouncer `SHOW POOLS`，证明 pooled endpoint 真正在对应 process
形成了 `database/user` pool。

### 第七步：行为证据

配置与角色通过后仍要测：

- 12-client/2-server queue；
- backend reassignment；
- session state 丢失/泄漏；
- protocol prepared；
- SQL PREPARE negative；
- async token visibility；
- planned switch/reconnect；
- final config/topology restore。

这才完成从声明到用户体验的链。

### 证据矩阵

| Claim | 声明 | 渲染 | runtime | SQL/client |
|---|---|---|---|---|
| 5433 主写 | service check/dest | primary cfg | backend status | writable |
| 5434 副本优先 | backup/selector | replica cfg | selected pool | read-only/member |
| 事务池 | pool mode | pgbouncer ini | SHOW CONFIG/POOLS | PID reassignment |
| 2 server cap | runtime override | N/A | sv_active ≤ 2 | 12 clients complete |
| prepared 支持 | max_prepared | SHOW CONFIG | two server PID | correct protocol results |
| switch recovery | Patroni/service | health config | topology/pool refresh | token reconcile |

## 22.6.3 配置变更、reload 与连接行为验证 {#item-22-6-3}

### 不要直接编辑 rendered file

错误流程：

```text
vim /etc/haproxy/pg-test-primary.cfg
systemctl reload haproxy
```

问题：

- inventory 不知道；
- 下次 automation 覆盖；
- 多节点不一致；
- review/rollback 不完整；
- secret/权限可能漂移。

正确流程：

```text
edit reviewed Pigsty declaration
  -> render diff/plan
      -> validate generated config
          -> staged reload
              -> runtime observation
                  -> client behavior test
                      -> commit evidence/rollback
```

### service tag

参考代码中服务生成/reload 由 `pg_service` 相关 task/tag 管理，典型调用形态：

```bash
./pgsql.yml -l pg-test -t pg_service
```

生产执行前必须按当前 Pigsty 版本查看 help/plan、限定 inventory 和 host。
不要从书中复制命令直接指向未知集群。

render task 会生成 service config，并在 reload 前运行 HAProxy config check。

### 配置校验

原生检查：

```bash
haproxy -f /etc/haproxy/haproxy.cfg -c -q
```

它证明语法/引用可加载，不证明路由语义正确。

PgBouncer reload：

```sql
RELOAD;
SHOW CONFIG;
```

不是所有配置都支持在线改变；某些需要 reconnect/restart。`SHOW CONFIG` 的
changeable 列与当前文档共同决定。

### reload 与现有连接

必须回答：

```text
old HAProxy process 是否 drain
existing TCP 是否保留
new connections 是否使用新 config
PgBouncer existing client/server pool 是否继承
authentication file rotation 对既有 session 是否影响
pool size 改变对已有 server connection 如何收敛
```

“reload 成功”不能替代这些答案。

### role change 与 config change 是两类变更

config change：

```text
declaration -> render -> reload
```

role change：

```text
Patroni/DCS -> health convergence -> pool/client refresh
```

二者可能同时发生，但 rollback 不同。故障切换时不应顺手修改持久配置，
否则难以分辨恢复来自哪项动作。

本章 runtime pool override 只用于实验，并在切换前恢复，正是为了隔离变量。

### staged rollout

多入口环境：

1. 选一个无生产或低流量 provider；
2. render/check；
3. reload；
4. direct health + client probe；
5. 观察 queue/error/session；
6. 扩到下一 provider；
7. 完整端点矩阵；
8. 保留旧配置与回滚。

若所有 provider 同时 reload，错误配置会同时摧毁入口冗余。

### 变更后的强制验证

```text
declaration projection equals reviewed intent
rendered files equal expected member/dest/check/backup
all proxy instances loaded intended config
runtime backend states make sense
PgBouncer config/pools within budget
primary endpoint writable
replica/offline endpoint readonly + allowed member
direct endpoint bypasses pool
session/prepared compatibility suite passes
old/new connection behavior matches change plan
```

如果变更涉及 role/promotion，再执行 pool role-state refresh 检查。

### 回滚

回滚不是把文件复制回去：

```text
restore declaration
render/check
staged reload
verify runtime
verify client path
close/refresh incompatible existing sessions if needed
record final state
```

如果数据库 role 已在期间改变，旧 rendered config 的成员角色仍由 health
check 动态判断，但 selector/backup/destination 可能不再合适，要重新评审。

### secret 与证据

服务变更会接触：

- inventory password；
- PgBouncer userlist/verifier；
- HAProxy stats auth；
- TLS key；
- HBA/identity。

证据只保留：

```text
hash/projection/presence
mode/owner
rotation metadata
behavioral result
```

不要把整个 inventory、auth file 或 config 原文无差别上传。正式 lab 对
临时 credential inventory 要求 mode `0600`，使用后删除副本，报告
`secret_values_exported=0`。

## 本节检查表

```text
[ ] 实际版本的 pg_default_services 已读取
[ ] dest/check/selector/backup 分别解释
[ ] local/dedicated service provider 的失败域明确
[ ] PostgreSQL LOGIN 与 PgBouncer auth delivery 分开验收
[ ] rendered HAProxy 与 runtime stats 都检查
[ ] SHOW CONFIG/POOLS 不导出敏感材料
[ ] client probe 映射到具体 member/role
[ ] 变更来自 inventory，不手改渲染产物
[ ] config check 只是语法门，不是完成条件
[ ] staged reload 验证 old/new connection
[ ] role change 后重新验证 pool state
[ ] rollback 恢复声明、runtime 与 client behavior
```

## 参考资料

- [Pigsty：PostgreSQL Service](https://pigsty.io/docs/pgsql/service/)
- [Pigsty：PgBouncer Administration](https://pigsty.io/docs/pgsql/admin/pgbouncer/)
- [PgBouncer：Configuration](https://www.pgbouncer.org/config.html)
- [HAProxy：Configuration tutorials](https://www.haproxy.com/documentation/haproxy-configuration-tutorials/)

---

[上一节：连接预算与过载边界](../05/) · [返回本章目录](../) · [下一节：实战：写入、只读与管理三类接入](../07/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
