给 OLTP 系统定尺寸,不能只把并发数拉高、记录一次峰值 TPS 就结束。数据库规模、客户端并发、TPC-E 类工作负载的合法交易速率,以及压测工具自身的并发实现,都会改变最终结论。
这次实验使用 DBT-5 这一 TPC-E-like fair-use 实现,在 EC2 系统上执行机械化搜索:从最小有效的 5000 客户数据库开始,把用户数从 1 增加到逻辑处理器数量的 2 倍,每组测试保持 1 小时稳定运行,再根据规范允许的交易速率调整数据库规模。经过多轮扩大、折半和重新验证,观察到的最佳组合是 32000 客户、24 用户。
需要强调的是,这是一种容量分析方法,而不是一次跑分;DBT-5 的结果也不等同于经过审计的官方 TPC-E 成绩。
峰值 TPS 为什么不能直接作为容量答案
OLTP 压测至少存在两个相互关联的搜索维度:
- 数据库规模:本次从 5000 客户起步,最终测试范围扩展到 97000 客户。
- 并发用户数:从 1 开始,逐个增加到逻辑处理器数量的 2 倍。
如果只固定一个数据库规模并扫描并发,得到的峰值可能不符合 TPC-E 类规范中数据库规模与交易速率之间的约束。反过来,如果只根据 CPU 数选择一个并发值,也可能错过数据库规模变化之后出现的新峰值。
因此更可靠的过程是:
- 使用最小合法规模 5000 客户创建数据库。
- 在 1 到
2 × 逻辑处理器数的范围内逐一测试用户数。 - 每组测试保持 1 小时稳定状态,避免用短时突发代表持续容量。
- 找出当前规模下的最大稳定吞吐。
- 按 TPC-E 规范或 DBT-5 对应规则,判断该交易速率对当前客户数是否合法。
- 如果不合法,扩大数据库规模并重新扫描并发。
- 建立合法与不合法的规模边界后,用折半法逼近合适规模。
- 每次改变客户数后,重新确认原来的最佳并发是否仍然最佳。
第 8 步很重要。数据库变大后,缓存命中、I/O 压力和锁竞争都可能变化,不能把某个规模下得到的 24 用户机械地套到所有规模上。
把测试计划变成可审计的实验矩阵
AI 工具适合生成测试矩阵、调度长时间任务、检查缺失结果和汇总数据,但合法交易速率必须来自你实际采用的规范版本或测试工具规则,不能让模型凭空推断。
下面的脚本可以直接生成测试计划,也可以从结果 CSV 中选出每个数据库规模下的最佳合法结果。候选规模只是演示;实际使用时应替换为扩容和折半过程中得到的客户数。
#!/usr/bin/env python3
import argparse
import csv
import os
from pathlib import Path
def make_plan(output: Path, scales: list[int], logical_cpus: int) -> None:
with output.open("w", newline="") as f:
writer = csv.DictWriter(
f,
fieldnames=["run_id", "customers", "users", "steady_state_seconds"],
)
writer.writeheader()
run_id = 1
for customers in scales:
for users in range(1, logical_cpus * 2 + 1):
writer.writerow({
"run_id": run_id,
"customers": customers,
"users": users,
"steady_state_seconds": 3600,
})
run_id += 1
print(f"Wrote {run_id - 1} runs to {output}")
def analyze(results: Path) -> None:
required = {
"customers", "users", "tps", "legal_max_tps", "errors"
}
with results.open(newline="") as f:
rows = list(csv.DictReader(f))
if not rows or not required.issubset(rows[0]):
raise SystemExit(
"results CSV must contain: " + ", ".join(sorted(required))
)
legal_rows = []
for row in rows:
row["customers"] = int(row["customers"])
row["users"] = int(row["users"])
row["tps"] = float(row["tps"])
row["legal_max_tps"] = float(row["legal_max_tps"])
row["errors"] = int(row["errors"])
if row["errors"] == 0 and row["tps"] <= row["legal_max_tps"]:
legal_rows.append(row)
if not legal_rows:
raise SystemExit("No error-free, legal results found")
best_by_scale = {}
for row in legal_rows:
customers = row["customers"]
current = best_by_scale.get(customers)
if current is None or row["tps"] > current["tps"]:
best_by_scale[customers] = row
print("Best legal result at each scale:")
for customers in sorted(best_by_scale):
row = best_by_scale[customers]
print(
f"customers={customers:>7} users={row['users']:>3} "
f"tps={row['tps']:.2f} legal_max={row['legal_max_tps']:.2f}"
)
winner = max(legal_rows, key=lambda row: row["tps"])
print("\nOverall best legal result:")
print(winner)
def main() -> None:
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
plan = sub.add_parser("plan")
plan.add_argument("--output", type=Path, default=Path("plan.csv"))
plan.add_argument("--logical-cpus", type=int, default=os.cpu_count() or 1)
plan.add_argument("--scales", type=int, nargs="+", required=True)
report = sub.add_parser("analyze")
report.add_argument("results", type=Path)
args = parser.parse_args()
if args.command == "plan":
make_plan(args.output, args.scales, args.logical_cpus)
else:
analyze(args.results)
if __name__ == "__main__":
main()
保存为 size_oltp.py 后,可以这样生成实验矩阵:
chmod +x size_oltp.py
./size_oltp.py plan \
--logical-cpus "$(getconf _NPROCESSORS_ONLN)" \
--scales 5000 16000 24000 32000 48000 97000 \
--output plan.csv
head plan.csv
这里的规模序列不是对原实验路径的复刻,而是一组可修改的候选值。实际做折半搜索时,应根据上一轮结果生成新的中点。
压测执行器需要把每行计划映射到你自己的 DBT-5 初始化和运行命令。完成后生成如下表头的 results.csv:
customers,users,tps,legal_max_tps,errors
其中 legal_max_tps 应通过规范或经过确认的规则计算,不能直接使用本轮观测到的 TPS。随后运行:
./size_oltp.py analyze results.csv
如果有 24 个逻辑处理器,单个数据库规模就需要执行 48 组一小时测试。正式开跑前应计算实例时间和成本,并通过冒烟测试尽早排除配置错误。
工具缺陷会伪装成数据库瓶颈
这轮工作中最值得警惕的发现,并不是某个 PostgreSQL 参数,而是测试工具曾经把 Trade Result 和 Market Feed 交易限制为一次只能执行一个。这样的串行化会制造人为吞吐上限:CPU、存储和数据库可能还有余量,但驱动端已经无法继续施压。
允许多个此类交易并发处理后,系统表现出现了显著改善,也意味着之前的实验需要重新开始。这个代价无法通过修补图表消除,因为工具语义已经改变,新旧数据不再属于同一个实验基线。
每轮长测之前可以安排短冒烟测试,至少检查:
- 所有交易类型都实际发生,而不是只跑了少数路径。
- 预期可并发的交易没有被全局锁、单消费者或单线程队列串行化。
- 客户端没有先于数据库耗尽连接、线程或文件描述符。
- 错误数、超时数和重试数被单独记录,不能只看成功 TPS。
- 重新加载数据库后,客户数和数据分布与计划一致。
- 工具、数据库配置和代码提交版本被写入结果元数据。
PostgreSQL 参数先固定,再逐轮调优
实验预先调整了 shared_buffers 和 max_wal_size 等已知需要关注的设置,但没有把第一次配置当作最终答案。更稳妥的做法是先建立一个固定基线,完成规模和并发搜索,再结合系统行为继续调优。
可以先记录当前配置,保证每轮实验可追溯:
psql "$DATABASE_URL" -X -v ON_ERROR_STOP=1 <<'SQL'
SELECT version();
SHOW shared_buffers;
SHOW max_wal_size;
SHOW max_connections;
SHOW checkpoint_timeout;
SQL
若要修改参数,应根据实例内存、WAL 生成速度和恢复要求选择数值。不要直接复制另一台机器的配置;同时注意某些参数需要重启 PostgreSQL 才能生效。
采用这套方法时的检查清单
这次结果将最佳点定位在 32000 客户和 24 用户,但这个数字只属于当时的 EC2 环境、数据库配置、数据规模和 DBT-5 实现。迁移到另一种实例、存储或代码版本后,都应重新执行搜索。
落地时可以坚持四条原则:
- 把 AI 当作实验编排器,而不是性能结论的裁判。
- 同时搜索规模与并发,并在每个新规模上复核最佳用户数。
- 只比较配置、代码和工具语义一致的实验结果。
- 先验证压测工具能产生真实并发,再分析数据库瓶颈。
真正可靠的容量结论,来自可重复的矩阵、足够长的稳定窗口、明确的合法性规则,以及发现工具缺陷后愿意推倒重来的纪律。