22.1 服务端点的语义
数据库连接串经常被当成部署细节:
postgresql://user@host:port/database但每个字段都在选择行为:
host/port 入口和发现机制
database/user catalog、身份和 pool key
target_session_attrs 可接受的服务端角色
sslmode/certificate 传输与身份验证
connect_timeout 一次建立连接最多占用多久
options/startup params 会话初始语义所以 URI 是应用与数据平台之间的 API。修改它可能改变一致性、会话、性能、 安全与故障行为,不能当作无语义的运维替换。
22.1.1 主写、只读、同步只读与直连管理端点
从业务动作定义端点
先列动作,再列端口:
| 动作 | 必要语义 | 不必要或有害的假设 |
|---|---|---|
| 创建订单 | 当前可写主库、提交结果可判定 | 固定机器 |
| 查看刚下的订单 | read-your-writes | 任意异步副本 |
| 浏览商品目录 | 可接受少量陈旧、只读 | 必须占用主库 |
| 生成日报 | 大查询、资源隔离、允许陈旧 | 普通 OLTP 副本 |
| 执行迁移 | 跟随主库、稳定 backend、完整会话能力 | 事务池 |
| 调查故障 | 保留管理槽、短超时、可审计 | 与应用共享无界池 |
由此可以得到几类服务,而不是几个机器别名。
主写服务
主写服务至少承诺:
new connection
-> currently accepted writable role
-> transaction_read_only = off
-> write identity and permissions valid它不承诺:
existing connection survives promotion/demotion
in-flight transaction transparently migrates
connection success means the next commit cannot fail
network error means the last commit did not happen客户端可用 libpq 的:
target_session_attrs=read-write作为最后一道角色检查。它不能选主,也不能修复代理;它只是在连接建立后拒绝 不满足属性的 session。
对写端点还要定义:
- 事务被断开后是否自动重试;
- 哪些动作有 idempotency key;
- 提交结果未知时如何查询;
- 是否允许 session pooling;
- 应用侧 pool acquire/connect/statement timeout;
- 最大并发和排队位置。
只读服务
只读端点有两个不同维度:
role semantics
当前 session 不能写
freshness semantics
数据最多允许落后多少transaction_read_only=on 只证明第一项,不证明第二项。一个停止 replay
数小时的副本仍然只读。
客户端可以组合:
target_session_attrs=read-only以及事务级护栏:
BEGIN READ ONLY;
SELECT ...;
COMMIT;前者防错误落点,后者把意图交给 PostgreSQL。二者都不能自动提供 bounded staleness。
只读服务还要明确降级:
replica unavailable
-> fail closed
-> fall back to primary as read-only workload
-> return cached/stale result
-> shed optional trafficPigsty 默认 replica service 把 primary 作为 backup。因此它表达 “副本优先”,而不是“永远只去副本”。如果业务必须隔离主库,声明与客户端 都要 fail closed,不能只相信服务名称。
同步只读不是“副本端口”的同义词
同步复制控制提交确认条件。例如:
primary commit waits until chosen standby has durable WAL这仍不自动表示:
任意只读副本已经 replay 到该提交
客户端下一次连接一定选择那个同步副本
同步副本没有降级或被替换
查询开始前 replay 已越过业务 token真正的“同步只读服务”至少要同时约束:
- 哪些 standby 进入同步集合;
- commit 等待到 write、flush 还是 apply;
- 端点只选择哪个集合;
- 同步状态丢失时 fail closed 还是降级;
- 客户端如何携带上次写入边界。
若 synchronous_commit=remote_apply 且读取端点只选择参与确认的已 apply
副本,可以收紧窗口,但切换、负载均衡和事务起点仍需单独证明。
离线/分析服务
“replica”表示复制角色,“offline”通常表达调度意图:
普通 OLTP 查询不选它
重型 OLAP/ETL 优先选它
资源参数、索引或延迟容忍可能不同Pigsty 的 offline service 使用 /replica 检查,并通过 inventory selector
优先选 pg_offline_query 成员。它仍会 replay WAL,长查询、临时文件与 I/O
可能拖慢 replay;“离线”不表示与主集群完全隔离。
应额外写:
- 最大可接受 replay lag;
- 查询并发、statement timeout、temp file 限额;
- 是否允许 hot standby conflict 取消查询;
- 普通副本能否作为 backup;
- replay 明显落后时是否摘除。
直连管理端点
直连管理并不一定是固定主机。更实用的是:
client -> semantic primary proxy -> PostgreSQL 5432它跟随当前主库,但绕过事务池,适合:
- schema migration;
- 需要 session advisory lock 的部署工具;
LISTEN/NOTIFYconsumer;- 大型
COPY或特殊驱动工作流; - 需要稳定 backend 的诊断;
- CDC/逻辑复制管理,在完成额外评审后。
绕过池不等于无限连接。管理端点通常应有更小、更受控的来源网段、身份、连接 预算与审计。
Pigsty 默认 default service 是这种路径:HAProxy 仍用 /primary
选择主库,但目标是 PostgreSQL 5432 而不是 PgBouncer 6432。
写一份端点合同
一个最小合同可以写成:
service: pg36_shop_rw
purpose: short OLTP read-write transactions
role: primary
path: haproxy -> pgbouncer(transaction) -> postgres
client_guard: target_session_attrs=read-write
consistency: primary transaction semantics
session_features:
allowed: [SET LOCAL, protocol_prepare_tested]
forbidden: [LISTEN, session_advisory_lock, persistent_temp_state]
budget:
app_instances: 8
app_pool_per_instance: 12
pgbouncer_server_pool: 40
database_reserved_for_platform: 60
timeouts:
acquire: 200ms
connect: 2s
statement: 3s
failure:
retry: capped exponential backoff with jitter
write_outcome: reconcile by idempotency token
security:
tls: verify-full
role: pg36_shop_app
owner: shop-platform端口只是这个合同的实现字段之一。
22.1.2 复制延迟、一致性与 read-your-writes
“刚写完却读不到”为什么完全正常
异步流复制的路径是:
primary commit
-> WAL generated/flushed
-> sender transmits
-> standby receives/writes/flushes
-> startup process replays
-> read query starts with a snapshot写请求收到成功,只表示其提交满足当前 synchronous_commit 规则。若规则不
等待目标副本 apply,随后的 replica query 可能先到。
形式化地,令:
L_commit = 写事务之后主库的提交边界
L_replay = 读取开始前目标副本的 replay LSN要让该副本具备读取边界,至少需要:
[ L_{\text{replay}} \ge L_{\text{commit}} ]
这仍不代表业务一定读到目标行:查询条件、事务 snapshot、权限、分区、 soft delete 和应用 cache 都可能改变结果。
四种常用策略
策略一:写后粘主
write success
-> same request/session reads primary
-> or tenant/user sticks to primary for bounded time优点是简单;缺点是时间窗口不是因果证明,可能过长浪费主库,也可能过短。
适合:
- 用户刚提交后查看详情;
- 一次 HTTP request 内写后读;
- 没有 LSN token 基础设施的普通应用。
策略二:携带一致性 token
写事务完成后,从主库取得一个 WAL 边界:
SELECT pg_current_wal_flush_lsn();客户端把它作为 opaque token 传给后续读取。读取端点在副本上观察:
SELECT pg_last_wal_replay_lsn();只有 replay 越过 token 才查询,否则:
short wait
-> primary fallback
-> fail with retryable freshness error注意:
- LSN 必须来自提交之后;事务内提前读取可能落在 commit record 之前;
- timeline 改变后,不能只做字符串大小比较;
- token 协议要绑定 cluster identity;
- 等待必须受 request deadline 限制;
- connection pool 可能在两次语句间换 backend;
- 读取事务 snapshot 必须在 replay 达标之后建立。
事务池场景最好把“等待边界 + 业务查询”放在同一只读事务或服务端函数里, 避免检查和查询落到不同副本。
策略三:同步 apply 与限定路由
可以让提交等待同步副本 replay,再让读取只去被确认的集合。它增加写延迟, 并把副本可用性纳入提交路径。
必须定义降级时:
block writes
reduce synchronous quorum
route reads back to primary
declare consistency downgrade没有明确降级合同的“同步”会在故障时悄悄变成另一个语义。
策略四:业务版本/事件边界
有些系统不用裸 LSN,而用:
aggregate version
event sequence
updated_at + unique version
outbox offset副本读到至少该业务版本才返回。这更贴近业务,但底层仍需可靠映射和超时。
延迟指标的几个阶段
副本延迟不是一个数字:
| 阶段 | PostgreSQL 观察 | 能推出什么 |
|---|---|---|
| sent | primary sent_lsn | WAL 已发到何处 |
| write | primary write_lsn | standby OS 收到/写入 |
| flush | primary flush_lsn | standby 持久化 |
| replay | primary replay_lsn / standby replay LSN | 查询可见边界接近何处 |
| query | 业务 token 查询 | 目标事实是否可见 |
replay_lag=0 是某次采样,不是未来保证。低流量时 timestamp lag 也可能显得
陈旧,LSN gap 与 wall-clock delay 要结合读。
本章的一个样本
正式实验:
write path 10.10.10.11:5433 -> primary PgBouncer
read path 10.10.10.11:5434 -> replica PgBouncer
selected replica pg-test-2
token visible 11.092 ms
poll count 1它只证明:
在这个时刻、这个 token、这个小型异步沙箱和当前负载下,副本在约 11.092 ms 后返回了该行。
它不能证明:
P99 replica lag
切换期间延迟
高 WAL 吞吐下延迟
网络抖动下延迟
所有读取 read-your-writes因此 evidence 把 EX22-ASYNC-READ-OBSERVATION 作为必需例外。
读端点失败也是语义
本章准备实验时曾观察到:
Patroni topology correct
direct PostgreSQL state recovery=true, read_only=true
HAProxy health UP
target_session_attrs rejects one pooled path根因位于 PgBouncer server pool 的角色状态,而不是 PostgreSQL 复制本身。 执行受控的:
RECONNECT test;让空闲/完成的服务端连接重新建立后,端点属性恢复。
这说明一致性证据必须从客户端走完整路径。只在副本上执行
SELECT pg_is_in_recovery(),不能证明应用的 5434 连接会得到同样 session。
22.1.3 DNS、VIP、代理与客户端发现
四种入口解决不同问题
固定节点地址
host=10.10.10.11- 优点:简单、可诊断。
- 缺点:节点故障就是入口故障;即使 HAProxy 能路由数据库角色,客户端也到 不了它。
本章 formal run 只验证这种单入口,因此明确保留例外。
DNS
DNS 可以:
- 把名称指向 VIP/代理;
- 返回多个 A/AAAA 记录;
- 在切换时修改地址;
- 为不同服务提供稳定名称。
但 DNS 不迁移既有 TCP 连接。还要考虑:
authoritative TTL
resolver/client cache
JVM/driver cache policy
negative caching
record order and happy-eyeballs
DNS control-plane availability“TTL 5 秒”不等于 5 秒内所有应用都换地址。
VIP
VIP 把一个网络地址移动到健康入口。它解决入口地址连续性,不决定数据库主库。
需要证明:
- 谁持有 VIP,使用什么仲裁;
- split brain 时是否可能双持;
- gratuitous ARP/NDP 与交换网络收敛;
- 跨网段/跨 AZ 是否可达;
- VIP manager 与 Patroni 状态如何组合;
- 入口节点的 HAProxy/PgBouncer 是否健康。
数据库主库、VIP owner 和 HAProxy backend 是三个状态机,不能假设天然一致。
客户端多 host
libpq 连接串可以列多个 host:
host=proxy-a,proxy-b
port=5433,5433
target_session_attrs=read-write
connect_timeout=2客户端按规则尝试,target_session_attrs 拒绝错误角色。这减少单一入口依赖,
但每个应用/驱动的:
- host 顺序;
- DNS 展开;
- 并行或串行尝试;
- overall deadline;
- pool 的 address refresh;
- TLS hostname verification;
都要实测。
专用代理/服务发现
外部 HAProxy、云负载均衡、Kubernetes Service 或服务网格可以提供入口。 仍要验证:
L4 vs L7 protocol handling
idle timeout and TCP keepalive
health check semantics
connection draining
source IP and HBA
TLS termination/passthrough
backend queue
control-plane blast radius一个完整发现链
application logical name
-> resolver / service discovery
-> one or more reachable proxy addresses
-> role-aware health check
-> PgBouncer or PostgreSQL destination
-> SQL role assertion每一步都有独立的 stale state:
DNS cache stale
VIP owner stale
HAProxy health stale
PgBouncer backend state stale
application pool holds old TCP connection切换测试必须穿过整个链,而不是只验证最末端的数据库角色。
target_session_attrs 的位置
它是客户端接受条件:
any 任意 session
read-write 必须可写
read-only 必须只读
primary 不是 hot standby
standby 是 hot standby
prefer-standby 优先 standby,必要时接受其他具体取值以当前 libpq 官方文档为准。它不能:
- 限制副本延迟;
- 保证连接经过或绕过 PgBouncer;
- 替代 TLS hostname 检查;
- 让失败事务自动迁移;
- 识别业务上的“正确集群”。
因此连接串还需要正确的 host、database、user、证书和 cluster identity 治理。
入口验收矩阵
| 场景 | 要执行的动作 | 通过条件 |
|---|---|---|
| 正常主库 | 从每个入口连接写服务 | 都选同一可写 leader |
| 正常副本 | 连接只读服务 | 只读,成员在允许集合 |
| 一个入口故障 | 停止/隔离入口 | 客户端转向另一入口 |
| DNS 变化 | 修改记录并保留旧连接 | 新连接收敛,旧连接行为已定义 |
| 数据库切换 | planned role change | 入口不变,新连接到新主库 |
| proxy reload | 配置校验后 reload | 现有/新连接行为符合 draining 合同 |
| stale pool | 角色变化后保留 server pool | 能检测并安全 refresh |
| TLS rotation | 轮换 CA/cert | 新旧窗口与 hostname 验证通过 |
本章只执行正常四端点和数据库 planned switch。入口节点故障、DNS/VIP 和 TLS 留给生产准入矩阵,不能由一次成功连接替代。
本节检查表
[ ] 每个服务按业务动作命名,而不是按主机命名
[ ] role、freshness、session、path、budget、failure 全部成文
[ ] 写端点有 target role 与 outcome-unknown 策略
[ ] 读端点写清 primary fallback 和 staleness 边界
[ ] 分析端点有资源与 replay 保护
[ ] 管理端点跟随主库但有独立预算/权限
[ ] DNS/VIP/multi-host 的失败域经过测试
[ ] SQL 观察走完整客户端路径
[ ] 角色变化后重新验证池化 session参考资料
- PostgreSQL 18:libpq connection parameters
- PostgreSQL 18:hot standby
- PostgreSQL 18:warm standby settings
- Pigsty:PostgreSQL Service
返回本章目录 · 下一节:连接的服务端成本 · 查看全书目录 · 查看索引中心