# 建立可信实验

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

---

可信实验不等于“环境完全没有噪声”。更现实的标准是：

```text
问题明确
因素声明
控制可验证
响应可重算
噪声被观察
顺序偏差被限制
失败没有消失
结论不越过证据
```

性能实验有两类误差：

```text
random error
  run-to-run fluctuation
  -> repetition / interval may reveal

systematic error
  wrong workload, wrong path, client bottleneck, warm-only cache
  -> more repetitions do not repair
```

跑 1,000 次错误 workload，只会非常精确地回答错误问题。

## 26.3.1 固定硬件、版本、配置、数据与随机种子 {#item-26-3-1}

### 把实验写成一个不可缺字段的 tuple

```text
result =
  f(
    hardware,
    virtualization,
    kernel,
    filesystem/storage,
    PostgreSQL build/version,
    extensions,
    configuration,
    schema,
    data,
    statistics,
    connection path,
    client,
    workload,
    time/background state
  )
```

少一个字段，结果就多一种解释。

### 硬件不只记录“8C32G”

至少记录：

```text
CPU model / architecture / socket / core / SMT
frequency policy / steal time / power state
NUMA topology
memory total / bandwidth / swap
storage device / controller / filesystem / mount
IOPS / latency / throughput / queue assumptions
network path / RTT / bandwidth
virtualization / cloud instance / noisy-neighbor boundary
```

参考 run：

```text
architecture  aarch64
client        pg-meta-1, 2 vCPU, 4,089,262,080 bytes RAM
server        pg-test-1, 1 vCPU, 2,048,679,936 bytes RAM
storage       Vagrant virtual disk on shared laptop hypervisor
```

所以它不能支持 production IOPS、endurance 或 failure-domain claim。

### PostgreSQL provenance

版本至少包括完整 build：

```sql
SELECT version();
SHOW server_version_num;
```

还要：

```text
extension versions
compiler/architecture
block size / WAL segment size
checksums
locale / encoding
huge pages / io_method
```

参考：

```text
PostgreSQL 18.6 Ubuntu build
server_version_num 180006
block size         8192
data checksums     on
```

升级 minor version 也应重新跑 regression baseline。minor release 可能修复 planner、
executor、WAL、I/O 或 correctness 问题；“major 没变”不是等价环境。

### 配置要保存 value、source 与 pending restart

只保存 `postgresql.conf` 不够：

```sql
SELECT
    name,
    setting,
    unit,
    source,
    sourcefile,
    sourceline,
    pending_restart
FROM pg_settings
ORDER BY name;
```

容量关键项：

```text
shared_buffers
work_mem / hash_mem_multiplier
maintenance_work_mem / autovacuum_work_mem
max_connections
max_worker_processes / parallel workers
effective_cache_size
random_page_cost / effective_io_concurrency
io_method / io_workers
checkpoint_timeout / max_wal_size
wal_compression / full_page_writes
synchronous_commit / fsync
track_io_timing / track_wal_io_timing
```

`effective_cache_size` 是 planner estimate，不是分配的 cache。`work_mem` 是每个
operation 可能使用的基础限制，不是整台 server 的总内存。PostgreSQL
[resource consumption](https://www.postgresql.org/docs/18/runtime-config-resource.html)
提醒，一个复杂 query 可能有多个 sort/hash，多 session 和 parallel worker 会把
总内存放大许多倍。

### 数据版本必须可证明

记录：

```text
schema migration/version
DDL hash
generator/seed
row counts
relation and index sizes
distribution parameters
statistics/analyze timestamp
live/dead tuple and bloat state
partition set
sequence position
```

参考 fixture 每个 scale 都重建 immutable table：

```text
customer
product
order_history
```

每次 measured run 前只重置：

```text
inventory
order_live
```

这样五次 run 有相同 initial mutable state，同时保留自然 cache 与 background
演化。它不代表 production bloat/churn。

### 随机 seed 与 run order 都要固定

seed 固定：

- script choice；
- random key；
- Zipf sample；
- rate schedule（若使用）。

run order 也会影响结果。若总是：

```text
c1 -> c8
```

c8 总是得到更暖 cache，也总是承受 c1 留下的 dirty page。

参考采用 counterbalanced order：

```text
r1 c1 -> c8
r2 c8 -> c1
r3 c1 -> c8
r4 c8 -> c1
r5 c1 -> c8
```

scale 仍按 S -> M -> L，因为重建大数据代价高。这是保留的 order confound，
报告必须写明。

### source hash 绑定执行实现

正式 evidence 保存 21 个 source file SHA-256：

```text
contracts
SQL
pgbench scripts
samplers
runner
validator
reviewer
task
```

若脚本改了一个 cast、weight 或采样窗口，旧 evidence 不再声称由当前 source
产生。参考 run 在 CPU sampler 口径修正后完整重跑，而不是把旧 transaction
结果与新 CPU 算法拼在一起。

### manifest 最小字段

```json
{
  "run_id": "...",
  "captured_at": "...",
  "target": "...",
  "versions": {},
  "settings": {},
  "source_hashes": {},
  "dataset": {},
  "workload_id": "...",
  "run_order": [],
  "raw_files": {},
  "cleanup": {},
  "claims_not_made": []
}
```

raw file 要有 size/hash；只保存总结无法重算 quantile。

## 26.3.2 预热、重复、置信区间与异常值 {#item-26-3-2}

### 预热要说明预热什么

可能需要预热：

```text
client process and connections
authentication/TLS
prepared statements
PostgreSQL shared buffers
OS page cache
JIT compilation
relation extension
filesystem allocation
storage cache
statistics/exporter discovery
```

“跑 5 秒预热”只是动作，不是证明。

参考 run 对每个 scale/concurrency 做 5 秒自然预热，随后 reset mutable table。
它能：

- 验证正式 protocol 与 script；
- 建立连接；
- 触碰部分 immutable working set；
- 暴露 parameter typing。

它不能保证 899 MiB L dataset 全部 warm，也不能制造 cold-cache baseline。

### 正式时长取决于最慢周期

观察窗口要覆盖：

```text
checkpoint cycle
autovacuum cycle
backup overlap
storage cache behavior
CPU thermal/power changes
application burst length
replica/archive catch-up
```

PostgreSQL 官方建议至少几分钟，可能需要数小时。

本章每 run 8 秒，是为了在 disposable laptop sandbox 中：

- 演示完整 30-run evidence pipeline；
- 观察短时 curve 与测量偏差；
- 保持负载有界；
- 不批准生产数字。

它不是 production baseline 的推荐时长。

### 重复的实验单位必须独立定义

参考每次 run：

```text
reset order_live and inventory
snapshot native counters
start wait and OS samplers
run 8 s pgbench
stop at exact measured window
wait for stats flush
snapshot counters
```

immutable data 与 cache 不重建，所以 run 并非完全独立；它是“同一 scale 阶段内的
重复”。报告不能把五次当五台独立机器。

生产比较建议：

- 多个 run；
- 多个时间段；
- 至少一次 fresh environment；
- 若硬件采购，多个同型 node；
- candidate/control 交错，而不是先跑完 A 再跑 B。

### 不要用一个平均数吞掉分布

每个 run 保存 transaction latency sample：

$$
\{x_1,x_2,\dots,x_n\}
$$

cell pooled p95：

$$
Q_{0.95}\left(
\bigcup_{r=1}^{5} X_r
\right)
$$

它回答“该 cell 所有保存 transaction sample 的 p95”。

run-level p95：

$$
q_r=Q_{0.95}(X_r)
$$

再报告 $q_r$ 的 median/interval，回答“一个典型 run 的 p95 如何波动”。

这两者不能混名。

参考 public summary 保存：

```text
pooled p50/p95/p99/max
run TPS median
run TPS deterministic bootstrap 95 interval
```

### bootstrap interval 的边界

对五个 TPS：

```text
resample five runs with replacement
compute median
repeat 10,000 times with fixed bootstrap seed
take 2.5% and 97.5%
```

这给出教学用 interval，但：

- n=5 很小；
- run 不完全独立；
- systematic bias 不会进入 interval；
- interval 不是“真实 TPS 有 95% 概率在里面”；
- performance distribution 可能非平稳。

它主要让读者看见“不确定性不是零”。

### 异常值不能看结果再删除

L-c8 五次 TPS：

```text
2216.087
2739.827
2774.267
2824.359
2798.272
```

第一轮显著低，p95 也更高。可能原因：

- L 初始化尾部 dirty write；
- working set 尚未稳定；
- background checkpoint/archive/replication；
- virtual neighbor；
- random interleaving。

本章保留它，因此：

```text
median TPS         2774.267
bootstrap interval [2216.087, 2824.359]
```

正确流程：

1. 在实验前声明 outlier rule；
2. 保存原始样本；
3. 检查 concurrent evidence；
4. 报告含/不含敏感性分析；
5. 只有明确测量故障才排除；
6. 排除要留 audit record。

“看起来不正常”不是删除依据。

### candidate 与 baseline 的比较

对每个 matched run：

$$
\Delta_r
=
\frac{candidate_r-baseline_r}{baseline_r}
$$

优先比较 paired distribution，而不是两个独立平均。验收可以是：

```text
throughput regression < 3%
p95 regression < 5%
p99 no material tail expansion
error/late/skipped unchanged
resource demand does not worsen beyond budget
```

阈值来自业务风险与实验噪声，不是固定模板。

## 26.3.3 冷热缓存、后台任务与邻居噪声 {#item-26-3-3}

### PostgreSQL 有至少两层 cache

```text
PostgreSQL shared buffers
  -> OS page cache
      -> device/controller cache
          -> physical/virtual storage
```

`blks_hit`：

```text
found in PostgreSQL shared buffers
```

`blks_read`：

```text
PostgreSQL requested a block read
```

不代表 physical disk read。OS 可能立即从 page cache 返回。

参考：

```text
S/M cells  blks_read = 0 during measured runs
L-c1       2,500
L-c8       14,491
```

可以说 L 触发了 PostgreSQL-level reads，不能说发生了同样数量的 physical I/O。
因此 runner 同时采 `/proc` disk counters，并用 Pigsty node metrics 做较粗的
corroboration。

### 真 cold cache 是破坏性实验

常见做法：

```text
restart PostgreSQL
drop OS page cache
reboot host
recreate VM
```

它们会影响同机其他 workload，不能在共享或生产节点随便执行。若要测 cold
start：

- dedicated environment；
- 明确授权；
- 固定动作与复位；
- 同时记录 storage cache；
- 区分 crash recovery、clean restart 和 first read；
- 多次重建，不把一次 boot 当分布。

本章禁止 drop cache、restart 和 checkpoint on demand，只声明 warm-natural。

### autovacuum 是 workload 的一部分

PostgreSQL 官方 pgbench 指南提醒，dead row/space 与 autovacuum 会改变结果。

两种实验都合理：

```text
controlled engine microbenchmark
  deliberately isolate maintenance

production representative benchmark
  keep realistic autovacuum and churn
```

错误做法是关闭 autovacuum 获得数字，却不在 claim 里写。

若保留 autovacuum，记录：

```sql
SELECT
    relname,
    n_live_tup,
    n_dead_tup,
    autovacuum_count,
    autoanalyze_count,
    last_autovacuum,
    last_autoanalyze
FROM pg_stat_user_tables;
```

并观察 progress、I/O、WAL 和 latency。

### checkpoint 与 full-page image

checkpoint 后第一次修改某 page，`full_page_writes=on` 时可能产生 full-page
image。于是相同 transaction mix 的 WAL/tx 会随 checkpoint phase 改变。

观察：

```text
pg_stat_checkpointer
pg_stat_wal.wal_fpi
pg_stat_wal.wal_bytes
Pigsty PGSQL Persist
```

不要为了“稳定”关闭 full-page writes；那会改变 durability。

PostgreSQL [WAL 配置](https://www.postgresql.org/docs/18/runtime-config-wal.html)
说明这些参数的恢复语义。本章保持 `fsync=on`、`full_page_writes=on`、
`synchronous_commit=on`。

### backup、archive 与 replica 会消耗真实资源

写 workload 同时驱动：

```text
primary WAL generation
WAL sender
replica receive/write/replay
archive command
backup repository
```

参考全实验 Pigsty window：

```text
WAL rate max            46.2 MB/s
replica replay gap max   3.47 MB
replay gap median        0
```

该 window 包含 bulk initialization、warm-up 和 measured run。max 不能归因给
某个 cell；median 0 也不证明 read-your-writes。它只是“复制路径在实验期间有过
推进和短时距离”的 corroboration。

### virtual neighbor 让“同一台机器”也不相同

shared hypervisor 可能同时运行：

- load generator；
- database VMs；
- monitoring；
- local build；
- host desktop workload。

guest 中看到的 CPU busy 不包含所有 host contention 细节。需要：

```text
steal time
host load
storage latency
run-to-run spread
dedicated-host rerun
```

本章把 shared-hypervisor 写入 `why_null`，而不是用五次重复假装消除。

### background inventory

每次 run 保存：

```text
pg_stat_database delta
pg_stat_wal delta
pg_stat_io delta
pg_stat_checkpointer delta
pg_stat_bgwriter delta
pg_stat_statements queryid-only delta
pg_stat_activity wait sample
client/server /proc sample
Pigsty full-window range
```

统计不 reset。PostgreSQL cumulative statistics 默认有更新延迟，并可能在
transaction 内缓存 snapshot；官方
[viewing statistics](https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-STATS-VIEWS)
解释了 `PGSTAT_MIN_INTERVAL`、`stats_fetch_consistency` 与
`pg_stat_clear_snapshot()`。runner 在 run 后等待 flush，并比较 reset timestamp。

## 26.3.4 绝对性能结论只适用于记录过的环境 {#item-26-3-4}

### 一条绝对数字的完整名称

不是：

```text
PostgreSQL = 2920 TPS
```

而是：

```text
pg36_shop shop-mix-v1
S dataset, 8 closed-loop clients, zero think time
prepared persistent sessions
direct pg-meta-1 -> pg-test-1 primary
PostgreSQL 18.6, Pigsty v4.5.0 sandbox
five 8-second teaching runs
median 2920.5 TPS
pooled p95 9.448 ms
zero observed failures/late
server work median 84.2%
```

标题很长，因为 claim 的边界本来就长。

### absolute capacity 与 regression baseline 分开

短、噪声较大的实验仍可做 CI regression signal：

```text
same controlled environment
same workload
same run order
candidate vs baseline interleaved
```

它不能自动成为生产采购依据。

| 用途 | 证据要求 |
|---|---|
| script smoke | 能运行、事务语义正确 |
| CI regression | 相对、matched、噪声已知 |
| sandbox demand estimate | OS/PG evidence + 明确边界 |
| production capacity | representative environment + SLO load |
| procurement | production candidate hardware + failure/maintenance |

### 环境变化触发 rebaseline

至少在以下变化后重建：

- PostgreSQL/Pigsty/kernel/extension 升级；
- CPU、RAM、storage、filesystem、VM type；
- `shared_buffers`、WAL、checkpoint、I/O、pool 配置；
- schema/index/partition/constraint；
- query/plan/protocol；
- operation mix、key skew、data size/bloat；
- backup/replica/topology；
- application retry/timeout/admission；
- SLO 或 failure model。

不能把三年前、旧 instance type 的 TPS 线性乘 CPU 核数。

### 外推必须带假设

参考 CPU 投影：

$$
\lambda_{65}
=
\frac{0.65}{D_{cpu}}
$$

这是 local linear model。它没有证明：

$$
D_{cpu}(\lambda)
=
\text{constant}
$$

靠近 contention/knee 后，service demand 可能随负载增加。外推距离越大，越需要
新测量点。

### 生产 gate 的证据矩阵

| 维度 | sandbox reference | production gate |
|---|---|---|
| hardware | shared laptop VMs | target compute/storage/network |
| run length | 8 s × 5 | minutes/hours covering cycles |
| arrival | closed-loop c1/c8 | open-loop rate sweep |
| path | direct primary | app + HAProxy + PgBouncer |
| data | synthetic S/M/L | representative shape/skew/bloat |
| failure | none | declared node/path loss |
| maintenance | observed incidental | scheduled overlap |
| backup | incidental | backup/restore window |
| cache | warm-natural | declared warm/cold scenarios |
| approval | sandbox passed | independent review |

任何一行空缺，production claim 就保持 pending。

### 可信实验检查单

- [ ] target 与 production boundary 已验证；
- [ ] load generator 和 server 身份独立；
- [ ] source、版本、配置、schema、data 有 hash/manifest；
- [ ] seed 与 run order 固定；
- [ ] warm-up 的对象与限制明确；
- [ ] 正式时长覆盖所需周期；
- [ ] 每个 cell 有足够重复；
- [ ] raw transaction sample 可重算 quantile；
- [ ] failed/retry/late/skipped 没有消失；
- [ ] client/server/PG/Pigsty 证据时间对齐；
- [ ] statistics reset timestamp 未变化；
- [ ] cache/background/neighbor 被观察；
- [ ] outlier rule 预先声明；
- [ ] absolute、relative、production claim 分级；
- [ ] unknown 与 cleanup evidence 同时发布。

---

[上一节：设计代表性工作负载](../02/) · [返回本章目录](../) · [下一节：找到饱和点与瓶颈](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
