跳至内容
22.7 实战:写入、只读与管理三类接入

22.7 实战:写入、只读与管理三类接入

本节把前六节变成一个可重放验收:

baseline gate
  -> declared identity + private service material
      -> four endpoint semantics
          -> two-slot queue
              -> transaction-session counterexample
                  -> prepared-statement matrix
                      -> async visibility sample
                          -> exact pool rollback
                              -> forward/restore planned switch
                                  -> role-aware pool refresh
                                      -> token reconciliation
                                          -> postflight + adversarial review

它在本地 Pigsty nonproduction sandbox 执行 L1/L2 动作。不要把 guard 改掉后 指向生产。

22.7.1 为 pg36_shop 配置端点和连接预算

先写生产设计,后映射 sandbox

pg36_shop 的概念设计:

logical service用途role/pathsessionfreshness
pg36_shop_rwAPI/worker 短写事务primary pooledtransactionprimary
pg36_shop_rocatalog/非因果读replica pooledtransaction声明 staleness
pg36_shop_adminmigration/诊断primary directfull sessionprimary
pg36_shop_olap报表/ETLoffline direct/受控 poolworkload-specific可陈旧

本章不创建真实 pg36_shop database,而把它映射到保留沙箱:

logical service    pg36_shop
sandbox database   test
declared user      test, pgbouncer=true
fixture schema     pg36_ch22
fixture table      route_probe

为什么不临时创建一个 LOGIN:

PostgreSQL role exists
  != Pigsty/PgBouncer authentication surface delivered

正式 runner 从 private reviewed Pigsty inventory 读取既有 test credential, 写入 mode 0600 的临时 libpq service file,结束后删除;credential 不打印、 不 hash 到报告、不进入 Git。

生产 identity 应如何声明

生产应在 reviewed Pigsty inventory/secret workflow 中声明:

pg_databases:
  - name: pg36_shop

pg_users:
  - name: pg36_shop_app
    password: <approved secret material/reference>
    pgbouncer: true

字段与 secret 语法按当前 Pigsty 版本确认。还要设置:

  • owner/group role 与 login role 分离;
  • least privilege;
  • connection limit;
  • default privilege;
  • role/database timeout;
  • TLS/HBA;
  • rotation;
  • application_name;
  • direct admin role;
  • PgBouncer per-user/database budget。

不要把书中 placeholder 作为可用 secret。

libpq service file

概念结构:

[pg36-shop-rw]
host=pg36-shop.example
port=5433
dbname=pg36_shop
user=pg36_shop_app
sslmode=verify-full
target_session_attrs=read-write
connect_timeout=2

[pg36-shop-ro]
host=pg36-shop.example
port=5434
dbname=pg36_shop
user=pg36_shop_app
sslmode=verify-full
target_session_attrs=read-only
connect_timeout=2

[pg36-shop-admin]
host=pg36-shop.example
port=5436
dbname=pg36_shop
user=pg36_shop_migrate
sslmode=verify-full
target_session_attrs=read-write
connect_timeout=2

密码应来自 .pgpass、secret manager 或受控 service material。文件权限:

directory 0700
service/pgpass 0600
no symlink
no stdout/log

本章沙箱 PgBouncer client TLS 是 disable,使用 sslmode=prefer 只为匹配 事实,并保留 EX20-CLIENT-PROXY-NO-TLS;生产必须另做 TLS 验收。

预算草案

假设:

API pods                  12
API client pool           12 each
worker pods                6
worker client pool         6 each
read pods                 12
read client pool           8 each
migration/admin            2

客户端上限:

rw clients      12×12 + 6×6 = 180
ro clients      12×8         =  96
admin direct                   =   2

不是 278 个 backend。一个候选 server budget:

rw app/worker server pool       40 + reserve 8
ro per eligible replica         20
admin direct                      2
monitor/platform/incident        separately reserved

需要在生产规模压测后定稿。

fixture 合同

setup.sql 创建:

CREATE SCHEMA pg36_ch22 AUTHORIZATION postgres;

CREATE TABLE pg36_ch22.route_probe (
    run_id         uuid        NOT NULL,
    worker_no      integer     NOT NULL,
    attempt_no     integer     NOT NULL,
    token          text        NOT NULL UNIQUE,
    client_sent_at timestamptz NOT NULL,
    committed_at   timestamptz NOT NULL DEFAULT clock_timestamp(),
    PRIMARY KEY (run_id, worker_no, attempt_no)
);

它验证:

  • existing schema owner/comment;
  • exact columns/type/nullability;
  • primary/unique constraints;
  • declared login safe attributes;
  • grants only USAGE/SELECT/INSERT;
  • role 未被 runner 创建、修改或接管。

fixture 是 synthetic data,drill 不自动删除它。

四端点预期

primary  5433 -> writable pg-test-1 through PgBouncer
replica  5434 -> read-only pg-test-2/3 through PgBouncer
default  5436 -> writable pg-test-1 direct
offline  5438 -> read-only pg-test-3 direct

SQL 同时记录 postmaster start time,与直连三成员的基线映射,解决 PgBouncer local Unix backend 下 inet_server_addr() 可能为空的问题。

22.7.2 验证会话状态、预备语句与只读一致性

风险分级

动作风险改动
captureL0只读快照
verify/review/allL0重验既有证据
schema setupL1synthetic schema/table
pool overrideL1一个 PgBouncer process runtime 值
queue/session/prepare/visibilityL1synthetic connection/row
planned switch + restoreL2Patroni role/timeline
reset:fixtureL3删除 synthetic schema

all 从不:

创建连接
SET pool config
RECONNECT
写行
切换
删除

preflight

在任何 mutation 前,第 19 章 gate 验证:

exact target pg36-l2-vagrant
Pigsty v4.4.0 declaration
PostgreSQL 18
four distinct hosts
pg-test-1 primary
pg-test-2/3 replicas
required exceptions accepted
production approval false

第 22 章 capture 再验证:

timeline/member/lag
package versions
listeners
four rendered services
all three PgBouncer configs
all three PostgreSQL connection settings

任何 drift 先停。

pool role-state baseline

在第一个应用 probe 前:

RECONNECT test;

在三台 PgBouncer 分别执行,清除 earlier role cycle 的 retained server connection state。

这一步来自真实失败发现:

pg-test-2 direct PostgreSQL was read-only
but its pooled target_session_attrs check rejected
RECONNECT test restored repeated checks

它只影响 sandbox teaching database,且是 evidence-bearing action。

临时两槽 pool

先 snapshot:

default_pool_size       50
reserve_pool_size       30
reserve_pool_timeout     1
query_wait_timeout     120

再 runtime SET:

default_pool_size        2
reserve_pool_size        0
reserve_pool_timeout     1
query_wait_timeout       5

为什么 runtime:

  • 让 12-client queue 低噪声可观察;
  • 避免为实验 saturate 50+30;
  • 不修改 rendered file;
  • exact finally rollback。

生产 pool policy 必须回到 Pigsty declaration,不照抄 runtime SET。

endpoint probe

每个 service 连接后:

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();

同时三节点 SHOW POOLS 证明 test/test pooled path。

saturation probe

12 个 client 同时:

SELECT pg_sleep(0.25),
       pg_backend_pid(),
       current_setting('transaction_read_only')::boolean;

管理 console 周期采样:

SHOW POOLS;

验收:

completed=12
max sv_active<=2
max cl_waiting>=1
unique backend PID<=2
all transactions read-write

正式:

max sv_active=2
max cl_waiting=10
unique PID=2
fastest=254.451ms
slowest=1519.423ms

session counterexample

为确定性分配:

  1. RECONNECT test
  2. A 在 backend X SET search_path=pg_catalog,commit;
  3. B 借到 X 并保持 transaction;
  4. A 被迫借 backend Y;
  5. 比较 PID 与 search_path;
  6. 关闭 client,RECONNECT test 清理实验状态。

正式:

A first       PID 65057, pg_catalog
B borrowed    PID 65057, pg_catalog
A reassigned  PID 65058, "$user", public

既证明 state leakage,也证明 state loss。

protocol prepared

Psycopg:

prepare_threshold=1
12 parameterized executions
hold first backend
force same client to second backend

正式:

PIDs        65171, 65172
results     12/12 correct

接受范围:

PgBouncer 1.25.2
max_prepared_statements=256
psycopg 3.2.9
transaction pooling
exact tested query

SQL PREPARE negative

PREPARE pg36_ch22_sql(integer) AS SELECT $1 + 1;
COMMIT;

占住创建 backend,再:

EXECUTE pg36_ch22_sql(41);

正式:

prepared PID  65285
execute PID   65286
SQLSTATE      26000
class         InvalidSqlStatementName

这是必须出现的失败。

replica visibility

写端点插入 unique token,commit 后取得主库 LSN;只读端点轮询 exact token, 记录:

selected member
recovery/read-only
replay LSN
elapsed
polls/connection rejections

正式:

member        pg-test-2
visible       true
delay         11.092 ms
polls         1
rejections    0

验收只要求在 5 秒 sandbox window 内看见,不形成 freshness SLO。

pool rollback gate

以上任一步成功或失败,finally 恢复:

50 / 30 / 1 / 120

读取 SHOW CONFIG exact compare。只有:

restored_before_switch=true

才允许 L2 切换。

22.7.3 注入切换与连接风暴,观察退避和恢复

这里“注入”的边界

本节只执行:

healthy planned switchover
pg-test-1 -> pg-test-2 -> pg-test-1

不注入 process/network/storage/DCS failure。标题中的“连接风暴”是小型 6-worker 重连探针,不是生产规模压力。

exact guards

需要两份 private input:

PG36_CH19_INVENTORY
  第 19 章 exact baseline gate 使用,mode 0600

PG36_CH22_CREDENTIAL_INVENTORY
  包含已声明 test/pgbouncer user credential,mode 0600

在普通环境两者可以来自同一 reviewed inventory 的安全副本。本书 local sandbox 的已部署 v4.4 baseline 与当前工作目录声明版本不同,因此 formal run 明确分离,避免用新声明冒充旧部署。

执行:

export PG36_EVIDENCE_DIR=/absolute/private/path/to/new-empty/ch22-run
export PG36_CH19_INVENTORY=/absolute/private/path/to/baseline.yml
export PG36_CH22_CREDENTIAL_INVENTORY=/absolute/private/path/to/credential.yml

export PG36_CH22_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH22_NONPRODUCTION=true
export PG36_CH22_PRODUCTION_DATA=false
export PG36_CH22_PRODUCTION_TRAFFIC=false
export PG36_CH22_CONFIRM=POOL_ROUTE_SWITCH_AND_RESTORE_CH22

static/labs/ch22/task.sh drill:service

所有值 exact match。output 非空、inventory 缺失/权限错误、topology drift 都会拒绝。

client workload

六个 worker,24 秒:

INSERT INTO pg36_ch22.route_probe
  (run_id, worker_no, attempt_no, token, client_sent_at)
VALUES (...)
RETURNING committed_at, pg_backend_pid(), pg_postmaster_start_time();

每 attempt 短连接,service:

host=10.10.10.11
port=5433
target_session_attrs=read-write
connect_timeout=2

失败:

same token is not blindly resubmitted
outcome recorded unknown
worker uses capped exponential backoff + jitter

forward

exact executor:

patronictl -c /etc/patroni/patroni.yml \
  switchover pg-test \
  --leader pg-test-1 \
  --candidate pg-test-2 \
  --force

完成条件:

pg-test-2 sole primary/running
pg-test-1/3 replica/streaming
all timeline 10
lag within gate

然后三节点:

RECONNECT test;

必须出现一次 refresh 之后的 acknowledged write,才能进入回切。

restore

patronictl ... switchover pg-test \
  --leader pg-test-2 \
  --candidate pg-test-1 \
  --force

完成:

pg-test-1 sole primary/running
pg-test-2/3 replica/streaming
all timeline 11

再次刷新三节点 pool,并要求首笔确认。

pool refresh evidence

正向三成员 action:

pg-test-1  172.318 ms
pg-test-2  276.016 ms
pg-test-3  175.812 ms
first acknowledged after final refresh action  140.283 ms

回切:

pg-test-1  176.102 ms
pg-test-2  167.016 ms
pg-test-3  173.996 ms
first acknowledged after final refresh action  1697.326 ms

这些 action time 只是管理命令耗时;write gap 还包括 topology、health、 server login 和 client backoff。

reconcile

结束后查询本 run 的所有 worker row:

SELECT worker_no, attempt_no, token, committed_at
FROM pg36_ch22.route_probe
WHERE run_id = $1
  AND worker_no > 0;

分类:

acknowledged token -> must exist
unknown token      -> lookup says committed or absent
duplicate token    -> must be zero

正式:

events                        387
acknowledged                  339
unknown                        48
persisted                     339
acknowledged missing            0
unknown committed               0
unknown absent                 48
duplicate                       0
unreconciled                    0
distinct postmaster generations 3

unknown_absent=48 不是失败;它们已被确定分类。若 unknown committed > 0, 也可以通过,只要 token lookup 明确且业务不重复执行。真正不允许的是 unreconciled。

时间口径

forward command                    2.774 s
forward conservative write gap     6.995 s
restore command                    2.766 s
restore conservative write gap     8.510 s
maximum adjacent ack gap           7.653 s

conservative gap:

last ack before action start
  -> first ack after stable topology and pool refresh

它包含 probe interval、connection attempt 和 backoff,不是纯数据库 promotion 时间,也不是 production RTO。

postflight 和反例

第 19 章 postflight 再次通过。十五个 evidence mutation 必须被指定错误码拒绝:

production claim
primary routed read-only
replica routed writable
offline wrong member
sticky session claim
broken protocol prepare
SQL PREPARE cross-backend success
pool server cap exceeded
no waiter observed
pool config not restored
acknowledged write missing
unknown unreconciled
write gap over objective
wrong final leader
degraded source

反例不是额外单元测试装饰,它防止 validator 只检查“文件存在”。

evidence tree

ch22-run/
├── preflight-ch19/
├── drill/
│   ├── before.json
│   ├── endpoint-observations.json
│   ├── fixture.json
│   ├── pool-settings.json
│   ├── pool-saturation.json
│   ├── session-semantics.json
│   ├── prepared-statements.json
│   ├── replica-visibility.json
│   ├── phases/
│   │   ├── pre-switch.json
│   │   ├── after-forward.json
│   │   └── restored.json
│   ├── switch-forward.json
│   ├── switch-restore.json
│   ├── pool-refresh-actions.json
│   ├── client-events.jsonl
│   ├── reconciliation.json
│   ├── after.json
│   ├── drill-manifest.json
│   ├── validation-report.json
│   └── negative-report.json
├── postflight-ch19/
└── review.txt

完整证据含 token 和运行细节,应放 private evidence store,不提交 Git。 仓库只保留 secret-free 聚合 connection-run.json

read-only 重验

export PG36_EVIDENCE_DIR=/absolute/path/to/ch22-run
static/labs/ch22/task.sh all

应输出:

status=review-ok
endpoints=4
pool_active_max=2
waiters_max=10
acknowledged=339
unknown=48
missing=0
duplicates=0
unreconciled=0
counterexamples=15-rejected
production_ch22_gate=pending
mutation=none

reset

reset 与 drill 完全分离:

export PG36_CH22_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH22_NONPRODUCTION=true
export PG36_CH22_PRODUCTION_DATA=false
export PG36_CH22_PRODUCTION_TRAFFIC=false
export PG36_CH22_RESET_CONFIRM=DROP_CH22_SYNTHETIC_SCHEMA_AND_ROLE

static/labs/ch22/task.sh reset:fixture

确认 token 为兼容已发布的实验接口保留旧名称,但当前 reset 只:

terminate application_name like pg36_ch22_% for user test
DROP SCHEMA pg36_ch22 CASCADE
preserve declared role test

它不回滚 timeline、不清理 evidence、不改 pool。删除前仍应阅读脚本并确认 exact target。

失败时保守恢复

脚本:

  • pool override 已开始就尝试恢复 baseline;
  • switch 未开始则不触碰 topology;
  • pg-test-2 是唯一稳定 leader,允许计划切回;
  • topology ambiguous/degraded 时不猜、不 force;
  • 保留 failure manifest;
  • 不自动 drop fixture。

finally 能降低风险,不能替代 operator inspection。

生产准入差距

本章 sandbox contract:

accepted-with-exceptions

生产仍需:

  1. 在 reviewed Pigsty inventory 声明 database/user/service/budget;
  2. 验收 client/server TLS 与证书轮换;
  3. 证明 VIP、DNS 或 multi-host entry failover;
  4. 跑真实 driver/ORM/query-mode matrix;
  5. 在 production-class 资源做容量和 reconnect load test;
  6. 注入 unplanned failure、partial network 与 cancel;
  7. 为每个 replica workload 定义 consistency contract;
  8. 把 pool refresh 自动化、告警化并限定 blast radius;
  9. 把结果纳入 SLO/SOP/change review;
  10. 由业务 owner、安全与平台共同签署。

不要把本章 8.510 秒写进生产 SLO。

本章完成定义

读者应能独立解释并证明:

为什么应用连服务而不是机器
四个端点选择什么角色/路径
异步副本为何无天然 read-your-writes
连接预算如何跨应用/pool/database 相乘
transaction pooling 会丢失/泄漏什么状态
两类 prepared statement 为什么结论不同
SHOW POOLS 如何证明排队和 backend cap
HAProxy health 为什么不等于 SQL 可用
切换后 pool state 为什么必须重验
write outcome unknown 如何 reconcile
何时只能说 sandbox accepted-with-exceptions

若只能背端口和 RECONNECT 命令,本章还没有完成。

参考资料


上一节:Pigsty 服务接入层 · 返回本章目录 · 下一章:固若金汤:认证、授权与数据安全 · 查看全书目录 · 查看索引中心

最后更新于