# 平台服务目录与多租户

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

---

如果消费者只能申请“一台 PostgreSQL”，平台交付的是资源，不是服务。

一个可用目录应该让消费者在申请前就知道：

```text
得到什么
不保证什么
允许怎样使用
谁负责什么
怎样计量
何时复审
如何退出
```

## 18.4.1 服务等级、规格、版本与扩展套餐 {#item-18-4-1}

### offering 是完整合同，不是机器型号

本章的
[`service-catalog.json`](/labs/ch18/service-catalog.json)
为每个 offering 保存：

```text
id / status / environment
service owner
consumer owner requirement
topology
service objectives
isolation
backup class
allowed and exception extension bundles
quotas
lifecycle
```

机器 CPU/内存/磁盘规格当然重要，但它只是实现 offering 的一种资源配置。
同一 `pg-ha-standard` 可以在不同硬件代际上实现；只要服务合同和经过验证的
容量仍然成立，消费者不应绑定某台主机。

### `pg-dev`

定位：

```text
development-test
single PostgreSQL instance
no HA commitment
explicit expiry
best effort
```

它允许较广的试验 extension bundle，包括 `federation-lab`。这不意味着开发
环境可以无治理：

- 必须有 consumer owner；
- 连接、存储、temp、statement time 要声明；
- 必须有到期日；
- 敏感生产数据不能因为“只是 dev”就复制进去；
- 实验凭据不能进入 Git；
- 删除仍需精确目标和恢复需求确认。

开发 offering 的目标是加快安全实验，不是变成永不下线的影子生产。

### `pg-ha-standard`

这是 `pg36_shop` 的默认生产候选：

```text
primary + two replicas
reviewed failure domains
dedicated database by default
dedicated cluster when risk gate requires
continuous WAL + full/differential backup class
```

目录中：

```text
availability_target=99.9%-proposal
rpo_seconds=60
rto_minutes=30
```

这些是要进入第 20、21、24 章验证的目标。它们不能由“三节点”直接推出。

允许的 bundle：

```text
core
search-accepted
spatiotemporal-conditional
```

`vector-pilot` 只能走 exception；`federation-lab` 不允许。

### `pg-ha-critical`

critical 不是把 standard 的数字改得更漂亮。它意味着：

```text
dedicated cluster
explicit synchronous policy
explicit zero-RPO failure-domain scope
hard connection/overload budget
storage includes failure + upgrade headroom
quarterly objective/owner review
architecture and risk approval
```

目录提出：

```text
99.95%-proposal
RPO 0 in explicitly rehearsed failure domain
RTO 10 minutes
```

“RPO 0”必须带范围。同步副本若与 primary 共享电源/机房，不能自动覆盖整个
区域故障；若为可用性临时降级同步策略，也可能改变承诺。

### `pg-analytics-offline`

它不是第二个 source of truth：

```text
read-only dependent service
offline replica
separate analytical query SLO
freshness proposal 60 seconds
not read-your-writes
source cluster backup policy
bounded analytics pool
```

Pigsty 官方
[Cluster / Instance](https://pigsty.io/docs/pgsql/config/cluster/)
把 `pg_role: offline` 定位为慢查询、ETL、OLAP 与交互分析专用 read-only
replica。隔离的目的不是“让慢查询没人管”，而是不让它默认争用在线 replica。

### 规格要由容量曲线产生

不要先定义：

```text
small=4c16g
medium=8c32g
large=16c64g
```

再假设业务会自然匹配。更可靠的顺序：

1. 定义 workload class；
2. 固定数据量、并发和增长；
3. 测量容量曲线和饱和点；
4. 选定安全 headroom；
5. 映射到可采购/可部署资源；
6. 定义升降配条件。

硬件 SKU 可以变化，基准 workload 与目标不应随意变化。

### 版本是一组矩阵

平台版本不只一个 `postgresql=18`：

```text
OS
kernel / filesystem
PostgreSQL major + minor
extension packages
Patroni
etcd
HAProxy
PgBouncer
pgBackRest
exporters / monitoring
Pigsty release
client drivers
locale / collation
```

兼容矩阵至少回答：

- 哪组是当前支持；
- 哪组是下一升级候选；
- 哪组已弃用；
- package repository 能否重建；
- backup/restore/replica/upgrade 是否覆盖；
- 安全补丁时限；
- 谁批准例外。

### extension bundle 是服务的一部分

bundle 不只是安装清单：

```json
{
  "id": "vector-pilot",
  "status": "pilot",
  "extensions": [
    {
      "name": "vector",
      "version_policy": "0.8.4 on validated fixture",
      "lifecycle": "pilot"
    }
  ],
  "gates": [
    "model identity and dimension pinned",
    "exact and ANN quality pass",
    "backup, restore, replica, upgrade rehearsed"
  ]
}
```

offering 只引用已审阅 bundle，避免每个用户自行拼装无法升级的组合。

### 服务目标要分平台与消费者责任

平台可负责：

```text
cluster availability
service routing
backup execution and restore capability
database engine/extension lifecycle
platform monitoring
capacity envelope
incident coordination
```

消费者仍负责：

```text
SQL/schema quality
业务不变量
连接/重试/timeout
流量预测
数据分类
应用兼容测试
on-call contact
错误预算决策
```

责任边界要写 RACI，不能在事故时才争论“数据库没问题还是应用没问题”。

### 目录本身也需要版本与状态

本章目录：

```text
release=1.6-proposal
status=proposal
review_cycle_days=90
```

当下卷 evidence 完成后，应产生新 release，而不是原地把历史 proposal 改成
“一直就是通过”。目录是决策记录的一部分。

## 18.4.2 数据库、模式、实例和集群隔离 {#item-18-4-2}

### 四层隔离解决不同问题

| 层级 | 隔离了什么 | 没隔离什么 |
|---|---|---|
| schema | 名称、对象组织、部分权限 | database GUC/连接、WAL、资源、故障 |
| database | catalog、连接入口、部分设置 | instance CPU/I/O/WAL、superuser、升级 |
| instance | postmaster、内存、端口、WAL、配置 | host/kernel/storage/network |
| cluster/service | HA/恢复/生命周期边界 | 若共主机/控制平面仍可能相关 |

“tenant 一个 schema”与“tenant 一个 cluster”不只是成本不同，安全与爆炸半径
完全不同。

### schema 隔离

适合：

```text
同一信任域
同一生命周期
需要跨 schema 查询
共享 database 设置可接受
对象数量可控
```

风险：

```text
search_path 注入
误授权 CREATE
同名对象解析
共享 connection/GUC
owner/superuser 可见
跨 schema 依赖
升级/删除耦合
```

安全做法：

- 不给不可信运行时角色在公共解析路径中的 `CREATE`；
- 对安全定义函数固定安全 `search_path`；
- 使用显式 schema qualification；
- 分离 NOLOGIN owner 与 LOGIN runtime；
- 定期审计 ACL 与 dependencies。

### database 隔离

适合：

```text
同一 instance 中的中等信任边界
独立连接、schema/catalog、extension installation
不要求跨 database transaction/join
共享主机与实例故障可接受
```

优点是 namespace 和连接边界更清楚；代价是：

- PostgreSQL 原生跨 database 查询不透明；
- connection pool/监控/迁移对象增多；
- extension 每 database 管理；
- 仍共享 WAL、CPU、I/O、backup 与 major upgrade；
- instance superuser 仍可越界。

database 不是强资源隔离。

### instance 隔离

适合：

```text
不同 preload/GUC/major
独立 restart/upgrade
强一些的内存/连接/WAL边界
不同维护窗口
高风险扩展或 workload
```

同一 host 上多个 instance 仍共享：

```text
CPU
kernel
filesystem/storage
network
host administrator
power/failure
```

必须配 OS/cgroup/volume/port/backup 与监控隔离，否则只是多个 postmaster 抢
同一资源。

### cluster 隔离

适合：

```text
独立 HA and recovery objective
独立故障/升级窗口
高敏感或不互信 tenant
重 workload/noisy neighbor
特殊 extension/kernel
法规或数据驻留
独立成本归属
```

代价是节点、备份、监控、升级、值班和容量碎片化。不是所有小数据库都值得
一个三节点 cluster。

### 选择顺序从信任与故障开始

推荐问：

1. tenant 是否互信？
2. 数据分类是否允许共实例/共主机？
3. 是否允许同一个 superuser/平台团队访问？
4. workload 能否互相制造事故？
5. 是否需要不同 PostgreSQL/extension/preload？
6. 是否需要独立 failover/restore/upgrade？
7. 跨 tenant 查询/事务是否必要？
8. 成本和运营能力是否承受更高隔离？

安全硬边界优先于便利与成本。

### 多租户至少有三种模型

```text
shared tables + tenant_id
  最省对象；需要强 tenant predicate/RLS 与索引设计

schema per tenant
  对象分开；迁移、对象数量、search_path 复杂

database/cluster per tenant
  隔离更强；运营和容量成本更高
```

也可以混合：

```text
small trusted tenants -> shared
larger tenants        -> database
regulated/high-risk   -> dedicated cluster
```

但迁移路径要提前设计：如何从 shared 提升到 dedicated，identity、sequence、
外键、事件与投影如何移动。

### RLS 是纵深防御，不是唯一边界

Row-Level Security 可以根据角色/会话上下文限制行，但需要审计：

```text
table owner / BYPASSRLS
FORCE ROW LEVEL SECURITY
session tenant context injection
pooling mode
prepared statements
security definer functions
COPY / maintenance paths
foreign keys and side channels
backup/replica/admin access
```

第 23 章会用正负 tenant 测试验证。不能因为 `CREATE POLICY` 成功，就宣布
隔离完成。

### 隔离与可观测性必须同粒度

若配额按 tenant，指标却只能看整个 cluster，就无法：

- 识别 noisy neighbor；
- 归属成本；
- 证明公平性；
- 按 tenant 降级；
- 预测迁移。

需要在不暴露敏感 SQL/数据的前提下，保留 service/database/role/workload
class/tenant 等必要维度，并控制 cardinality。

## 18.4.3 成本归属、配额和生命周期 {#item-18-4-3}

### 没有成本归属，平台会奖励浪费

共享数据库中，使用者容易只看到：

```text
“多一张表”
“多一个索引”
“多跑一条报表”
```

平台承担的真实成本：

```text
compute
memory
primary + replica storage
WAL + archive
backup repository
network
monitoring retention
upgrade window
on-call labor
recovery time
capacity headroom
```

成本模型不一定要精确计费，但必须让 owner 看见边际影响。

### 用服务单位表达成本

可以按组织成熟度从简单到复杂：

```text
allocated cluster share
database GB-month × replica/backup multiplier
connection/CPU class
WAL GB and backup retention
query or workload class
dedicated instance/cluster fixed cost
operator support tier
```

避免只按主表大小计费：索引、TOAST、replica、backup 与 WAL 可能远大于主表。

### 配额是正确性保护，不只是节省

重要配额：

| 配额 | 保护 |
|---|---|
| connection | 防止 backend/内存/调度耗尽 |
| active query | 防止过度并发 |
| statement timeout | 限制无界执行 |
| lock timeout | 限制排队与级联 |
| idle transaction | 防止长快照/持锁 |
| `work_mem` class | 限制乘法内存 |
| `temp_file_limit` | 防止 spill 填盘 |
| storage/WAL growth | 保护恢复链与容量 |
| logical slot retention | 防止 WAL 无限保留 |
| maintenance window | 保证 vacuum/index/backup |

配额超限行为要确定：

```text
queue
reject
cancel
spill
throttle
degrade
escalate
```

静默超卖不是服务。

### 连接预算从端到端计算

不要让每个应用实例都配置：

```text
pool_max = max_connections
```

预算应满足：

\[
\sum pools + admin + monitoring + maintenance + HA\ reserve
\leq safe\ backend\ budget
\]

还要考虑：

- 每个应用副本数；
- failover 后流量汇聚；
- pool retry storm；
- transaction vs session pooling；
- offline/replica 独立预算；
- emergency admin 保留。

第 22、34 章会做饱和与过载演练。

### 生命周期从申请前开始

完整状态机：

```text
request
  -> classify data/workload/objective
  -> choose offering/isolation/bundles
  -> owner and cost approval
  -> provision
  -> acceptance evidence
  -> operate
  -> change / exception
  -> periodic review
  -> deprecate
  -> drain/export
  -> retain/erase
  -> decommission evidence
```

若没有 expiry/decommission，临时数据库会永久占用备份、监控和升级路径。

### 变更要分普通与例外

普通变更：

```text
catalog-defined offering
supported version
allowed extension bundle
within capacity envelope
standard backup/security
```

例外：

```text
pilot extension in production
custom preload
unsupported version
RPO/RTO override
over-quota
cross-boundary data
lab authentication
```

例外必须有：

```text
owner
risk
compensating control
evidence
expiry
review date
rollback
```

“临时允许”但没有到期日，通常等于永久。

### 删除与退役是高风险动作

本书实验里的 reset 有精确 token、target、marker 和 active-session guard；
生产退役还要更多：

1. 确认 owner 与法律/业务 retention；
2. 停止新连接和写入；
3. 记录最后 backup/manifest；
4. 导出需要保留的数据；
5. drain 外部消费者和投影；
6. 验证 DNS/service/secret/monitoring 依赖；
7. 双人审批 destructive target；
8. 擦除或进入保留；
9. 保存完成证据。

“在 inventory 中删掉一段 YAML”不等于数据已经安全退役。

### 复审由事件与周期共同触发

周期复审：

```text
owner
objective
usage/cost
capacity
version/support
exceptions
expiry
```

事件复审：

```text
major workload change
new data class
extension/version change
incident
failed restore/upgrade
ownership change
externalization trigger
provider/platform migration
```

### 平台目录最终要可验证

本章 validator 会检查：

- offering ID 唯一；
- production offering 有非空 `service_objectives`；
- allowed bundle 全部存在；
- blueprint 引用的 offering 全部存在；
- bundle 与 lower-volume gate 引用闭合；
- lab-only FDW 不得生产准入。

反例把 `pg-ha-standard.service_objectives` 置空时，必须得到：

```text
E_SERVICE_OBJECTIVE
```

机器验证不能判断 99.9% 是否合理，但能阻止“生产服务连目标字段都没有”的
结构退化。

---

[上一节：明确替代边界](../03/) · [返回本章目录](../) · [下一节：Pigsty 作为参考实现](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
