图数据库基准测试与压测:LDBC SNB、混合负载与尾延迟

系统讲解图数据库基准测试与压测:基准要回答什么问题、LDBC SNB 与自研工作负载的选择、数据集生成与规模因子、混合读写负载与查询混合设计、吞吐与尾延迟指标、并发与预热、结果可复现性的硬性要求、以及一次完整压测的执行流程与常见踩坑排错要点。

引言

「哪个图数据库更快」是个几乎无法直接回答的问题:跑一条两跳查询,Neo4j 可能 3 毫秒、JanusGraph 可能 30 毫秒;但换成「全图 PageRank」,结论可能完全反转;再换成「1000 并发下的混合读写」,又变成第三种排序。更麻烦的是,网上流传的对比文章大多不可复现——没有说明数据集规模、没写清并发数、没交代缓存是冷的还是热的、把「预热后的单线程延迟」和「冷启动的并发吞吐」放在一张表里比。基准测试的价值不在于得出一个排名,而在于用可复现的方法回答「在我的数据、我的查询模式、我的并发水平下,这台机器能不能满足我的延迟与吞吐目标」。本文按工程视角讲图数据库的基准与压测:先讲基准要回答什么问题、LDBC SNB 与自研工作负载如何选择、数据集生成与规模因子、混合读写负载与查询混合设计、吞吐与尾延迟指标、并发与预热、结果可复现性的硬性要求,最后是一次完整压测的执行流程与常见踩坑。

前置:图数据库性能调优 、容量规划与成本优化 、图数据库选型对比 。


目录


1. 基准测试要回答什么问题

基准的三种用途,决定了三种完全不同的做法:

用途 A:选型(A 还是 B?)
  要公平:同一数据集、同一硬件、同一负载、同样调优力度
  产出:典型查询上的延迟/吞吐对比 + 运维复杂度定性评价
  陷阱:厂商给的 benchmark 通常在自家最擅长的负载上跑

用途 B:容量规划(需要几台机器?)
  要贴近生产:用真实数据规模与真实查询分布
  产出:目标延迟下的最大可持续吞吐 → 反推机器数
  陷阱:用峰值吞吐做规划,生产一有毛刺就雪崩

用途 C:回归验证(升级/调参有没有变快?)
  要可重复:固定数据集与负载,只改一个变量
  产出:同一套指标的前后对比 + 显著性判断
→ 先明确用途,再设计基准;三者的方法论不通用

「快」必须被拆解成可测量的维度:

1. 查询类型:点查 / 一跳 / 多跳 / 全图算法 / 聚合
2. 并发水平:单线程延迟 vs 高并发吞吐(两者常成反比)
3. 缓存状态:冷缓存(首次访问)vs 热缓存(page cache 命中)
4. 数据规模:小图放得下内存 vs 大图必须走磁盘
5. 读写比例:纯读 / 读写混合 / 写为主
6. 结果规模:返回 10 行 vs 返回 10 万行(网络与序列化开销)
→ 只报一个「平均延迟」数字,等于什么都没说

必须先定 SLO,再谈基准:没有目标的压测只是在看数字,无法得出结论。可用的 SLO 形态如「p99 延迟 ≤ 100ms 且吞吐 ≥ 500 QPS」「写入吞吐 ≥ 5 万边/秒同时读 p95 ≤ 200ms」「全图 PageRank 在 30 分钟内完成(1 亿边)」。有了 SLO,压测的产出就是「达标或不达标 + 瓶颈在哪」;没有 SLO,产出只是一张没人会看的曲线图。

心智:基准测试的用途(选型 / 容量规划 / 回归验证)决定了做法,三者方法论不通用;「快」必须拆成查询类型、并发水平、缓存状态、数据规模、读写比例、结果规模六个维度;没有 SLO 的压测只是看数字,得不出结论。


2. 基准的选择:LDBC SNB 与自研

LDBC SNB(Social Network Benchmark) 是图数据库领域最主流的标准化基准,由 LDBC 组织维护。

SNB 的组成部分:
1. 数据生成器(Datagen):按社交网络语义生成可伸缩的数据
   - 有明确的数据模式(Person/Post/Comment/Forum/Tag/City...)
   - 有相关性(不是随机图:存在社区、幂律度分布、时间演化)
2. 工作负载:
   - Interactive(交互式):低延迟复杂查询 + 短事务更新
   - BI(商业智能):大规模聚合分析查询(跑分钟级)
3. 更新流:持续插入新的人、帖子、好友关系
4. 验证:查询结果有正确性校验,防止「跑得快但算错了」
→ 价值:可复现、有语义、能横向对比;社区有公开的审计结果

SNB 的查询类别(Interactive 负载的骨架):短查询(按 ID 取人/帖子、取最近若干消息)、复杂读(朋友的朋友、共同兴趣、某城市某时间段内的消息)、聚合(按标签或时间窗统计)、更新(加好友、发帖、加评论、加点赞,混合进负载)。关键点在于 SNB 的复杂查询需要多跳遍历加属性过滤加排序,这正是图数据库的真实战场,而不是「按主键取一个点」。

什么时候该自研基准:LDBC SNB 覆盖不到的场景包括——图结构不同(资金流转图有向带权且环多、供应链图层级深扇出大、知识图谱异构超节点多);查询模式不同(你的生产查询可能是「特定 5 个模板的变体」);数据规模与分布不同(真实图的度分布可能比 SNB 更极端);更新模式不同(批量导入为主、几乎不增量写)。自研的价值是直接反映你的瓶颈,代价是不可横向对比。务实做法是 SNB 做选型与回归,自研做容量规划与专项验证。

自研基准的最小可用设计:从生产慢查询日志里提取 Top-N 查询模板(带参数占位符);按生产频率给模板加权(Zipf 分布而非均匀);数据用生产样本脱敏后的真实图(结构保真最重要);记录每个模板的延迟分布而不是只看整体平均。这四步做下来,自研基准的结论通常比任何公开 benchmark 更有用。

心智:LDBC SNB 是最主流的标准化基准(Datagen + Interactive/BI 负载 + 更新流 + 正确性校验),适合选型与回归;覆盖不到的场景(资金流转、供应链、异构知识图谱、批量导入为主)应自研基准——从生产慢查询提模板、按频率加权、用脱敏真实图,结论比公开 benchmark 更有用。


3. 数据集生成与规模因子

规模因子(Scale Factor, SF) 决定数据量,是横向对比的前提。

LDBC SNB 的规模因子:
SF1  ≈ 3 GB(约 320 万节点)      SF10 ≈ 30 GB
SF3  ≈ 10 GB                      SF30 ≈ 100 GB
SF100 ≈ 300 GB(约 3 亿节点)
→ 报结果必须写清 SF,否则数字毫无意义
→ 「SF1 上 5ms、SF100 上 5s」完全可能(内存放不下时性能断崖)

生成数据时必须控制的三个性质:

1. 度分布:真实社交/资金图是幂律(少数枢纽 + 大量低度节点)
   → 用均匀随机图压测会严重低估超节点带来的问题
2. 社区结构:真实图有聚类(朋友的朋友也认识)
   → 无聚类的图会让多跳查询结果规模失真
3. 时间演化:节点与边有先后顺序(新节点更活跃)
   → 时间维度影响增量写入与时序查询的性能
→ SNB Datagen 已内建这些性质,自研生成要自己补

图数据的规模与内存的关系(最容易踩的坑):图库的遍历性能高度依赖邻接表命中缓存——数据量小于内存时遍历基本全在内存、延迟稳定;数据量接近内存时 page cache 频繁淘汰、延迟抖动剧烈;数据量大于内存时随机 IO 主导、延迟可能高一个数量级。压测必须报告「数据量 / 可用内存」的比值,否则同一份数据在两台机器上会跑出完全不同的结论,这也是「小图上 A 快、大图上 B 快」现象的根本原因。

数据准备的检查清单:

- 规模因子与总数据量(节点数/边数/属性字节数)写进报告
- 度分布的形状(max/median 度数、超节点数量)
- 是否含时间戳、是否有环、有向还是无向
- 数据量与可用内存的比值
- 导入耗时与导入方式(批量导入 vs 在线写入)
→ 缺任何一项,别人都无法复现你的结论

自研数据生成的最小实现(当 SNB Datagen 的模式与你的图不符时):

import random

def gen_powerlaw_graph(n, alpha=2.5):
    """生成幂律度分布的有向图:少数枢纽 + 大量低度节点"""
    weights = [1.0 / (i ** alpha) for i in range(1, n + 1)]
    total = sum(weights)
    weights = [w / total for w in weights]
    deg_target = [max(1, int(random.choices(range(1, n + 1), weights)[0] / 10))
                  for _ in range(n)]
    edges = set()
    for u in range(n):
        for _ in range(deg_target[u]):
            v = random.choices(range(n), weights)[0]
            if v != u:
                edges.add((u, v))
    return list(edges)

# 关键:生成后必须校验度分布是否真的是幂律(双对数坐标下近似直线),
# 否则生成器参数写错会退化成均匀随机图,压测结论全部失真

心智:规模因子(SF)是横向对比的前提,报结果必须写清;生成数据必须保留幂律度分布、社区聚类与时间演化三个性质,否则会低估超节点问题;图性能与「数据量 / 可用内存」的比值强相关,压测报告必须写明这个比值。


4. 混合读写负载设计

纯读压测是最容易做也最没用的压测:生产系统的痛点在读写混合——写入带来锁竞争、事务日志、page cache 污染,读延迟会显著恶化。混合负载有三个设计参数:读写比例(如 90/10,资金类可能 70/30);写入形态(点插入、边插入、属性更新、批量导入、删除,其中边插入通常最贵,要更新两端邻接表加索引);读查询混合(按生产频率加权 Zipf,而不是均匀轮询)。三者组合才构成有意义的负载,单独调一个都会失真。

用脚本表达一个混合负载:

import random, time, statistics

READ_TEMPLATES = [                      # (名称, 权重, 查询)
    ("point_lookup", 0.40, "MATCH (a:Account {id:$id}) RETURN a"),
    ("one_hop",      0.30, "MATCH (a:Account {id:$id})-[:T]->(b) RETURN count(b)"),
    ("two_hop",      0.20, "MATCH (a:Account {id:$id})-[:T*2]->(b) RETURN count(DISTINCT b)"),
    ("agg_window",   0.10, "MATCH (a:Account {id:$id})-[:T]->(b) "
                           "WHERE b.ts > $ts RETURN sum(b.amount)"),
]
WRITE_TEMPLATES = [
    ("add_edge",    0.70, "MATCH (a:Account {id:$s}),(b:Account {id:$d}) "
                          "MERGE (a)-[:T {ts:$ts, amount:$amt}]->(b)"),
    ("update_prop", 0.30, "MATCH (a:Account {id:$s}) SET a.score = $v"),
]

def pick(templates):
    r, acc = random.random(), 0.0
    for name, w, q in templates:
        acc += w
        if r <= acc:
            return name, q
    return templates[-1][0], templates[-1][2]

def run_mixed(driver, seconds, write_ratio=0.1):
    """按比例混合读写,记录每个模板的延迟分布"""
    lat, stop = {}, time.time() + seconds
    while time.time() < stop:
        name, q = pick(WRITE_TEMPLATES if random.random() < write_ratio
                       else READ_TEMPLATES)
        params = {"id": rand_id(), "s": rand_id(), "d": rand_id(),
                  "ts": now_ms(), "amt": rand_amt(), "v": random.random()}
        t0 = time.perf_counter()
        with driver.session() as s:
            s.run(q, **params).consume()      # 必须 consume,读全结果
        lat.setdefault(name, []).append((time.perf_counter() - t0) * 1000)
    return {k: {"p50": statistics.median(v),
                "p99": sorted(v)[int(len(v) * 0.99) - 1]} for k, v in lat.items()}

写入的三种「强度」要分开测:单条写入(逐条事务,测的是事务开销与日志刷盘,最常见的在线形态);批量写入(每事务 N 条,吞吐可能高一个数量级,但延迟被批量放大);批量导入(离线工具,测的是导入吞吐,与在线写入不是一个量级)。报告里必须写清是哪种,否则「写入 10 万/秒」可能是批量导入的数字。

并发模型:并发数不是越大越好——低并发(116)测单请求延迟,反映算法与索引质量;中并发(32256)测吞吐,反映资源利用率;高并发(>256)通常开始排队,延迟飙升而吞吐不增。必须画「并发数 → 吞吐 + p99 延迟」的曲线找到饱和拐点,这才是容量规划的依据;只测一个并发数(如 100)得到的是曲线上的一个点,无法指导决策。

心智:混合读写负载要同时定读写比例、写入形态、读查询混合三个参数;写入必须区分单条、批量事务、离线导入三种强度;并发压测要画「并发 → 吞吐 + p99」曲线找饱和拐点,只测一个并发数无法指导容量规划。


5. 指标:吞吐、尾延迟与正确性

必须同时报的三个指标族:

1. 吞吐:QPS / TPS,以及「在什么延迟下达到」
   → 「吞吐 5000 QPS」若不写延迟,可能是 p99 已经 5 秒
2. 延迟分布:p50 / p90 / p95 / p99 / p999 / max
   → 平均值掩盖一切;p99 才是用户感知与 SLO 的关键
3. 正确性:结果是否与预期一致(防「跑得快但算错」)
   → LDBC 有官方校验;自研基准要自己写断言
→ 三个缺一不可,尤其正确性最容易被压测脚本忽略

尾延迟为什么是核心:平均 20ms、p99 800ms 的系统,用户体验是「偶尔卡死」。尾延迟的来源包括 GC 与内存回收停顿、page cache 淘汰后的随机 IO、锁竞争与事务重试、后台任务(压缩、检查点、备份)抢占资源、超节点查询偶发命中。因此压测必须跑足够久以覆盖后台任务的周期,否则测不到尾延迟;经验上单次压测时长应至少覆盖一次完整的后台任务周期。

延迟的统计口径要写清:测的是客户端延迟还是服务端延迟(网络与排队差异可能数倍);是否包含连接建立与认证;是否包含结果集的完整读取(consume 与 iterate,只发查询不读结果是经典作弊手法);冷缓存还是热缓存、是否预热、预热多久;是否统计了超时与失败请求(还是直接从样本里剔除)。五个口径不写清,数字无法对比。

正确性校验的最小做法:给每个查询模板配一个已知答案的用例,结果不一致直接抛错。

def assert_correct(driver, checks):
    """checks: [(名称, 查询, 参数, 期望值), ...]"""
    for name, query, params, expected in checks:
        with driver.session() as s:
            got = s.run(query, **params).single()[0]
        if got != expected:
            raise AssertionError(f"{name}: got {got}, expected {expected}")
正确性校验的三种粒度:
1. 逐查询断言:每个模板配一个已知答案的用例(最实用)
2. 交叉校验:与另一个实现或另一份数据快照的结果对拍
3. 不变量校验:如「计数不为负」「路径长度单调」等结构性约束
→ 加断言的成本很低,漏掉的代价是结论完全错误

资源指标必须同步采集:CPU 使用率与饱和度(runqueue)、内存(堆使用量、page cache 命中率、换页次数)、磁盘(IOPS、吞吐、平均等待时间、队列深度)、网络(往返延迟、重传率)、数据库内部(活跃事务数、锁等待、慢查询计数、JVM GC 次数)。只有延迟数字没有资源指标,无法回答「瓶颈在哪」;建议用统一时间轴对齐采集,便于事后归因。

延迟报告的推荐格式:

模板权重p50p95p99p999错误率
point_lookup40%2ms4ms9ms30ms0%
one_hop30%6ms14ms40ms120ms0%
two_hop20%45ms180ms620ms2.1s0.3%
agg_window10%90ms400ms1.4s4s1.2%
这张表比「平均 38ms」有用得多:
- two_hop 的 p99 是 p50 的 14 倍 → 尾延迟问题集中在这个模板
- agg_window 有 1.2% 错误率 → 超时或资源不足,必须单列不能剔除
- 按权重加权后整体 p99 约 620ms,远高于平均值暗示的「还行」
→ 报告模板固定下来,回归对比才有意义

心智:吞吐、延迟分布(p50~p999)、正确性三个指标族缺一不可;尾延迟是核心,压测时长必须覆盖后台任务周期才能测到;延迟口径(客户端/服务端、是否读全结果、冷热缓存、是否剔除超时)必须写清;同时采集 CPU/内存/磁盘/GC 等资源指标才能定位瓶颈。


6. 压测工具与实现

三类工具,各有适用面:

1. 官方驱动 + 自写脚本:完全可控,能精确表达业务负载与正确性校验;
   代价是要自己实现并发、统计、限流
2. 通用压测框架(JMeter / Gatling / k6 / Locust):并发模型、报告、
   限流开箱即用;代价是图查询的参数化与结果校验要额外写
3. 图数据库自带压测工具(LDBC 官方驱动、厂商套件):与标准对齐、
   结果可对比;代价是只能测它支持的负载
→ 常见组合:官方驱动跑 SNB(选型),Locust/k6 跑自研混合负载(容量)

用 Locust 表达图查询负载的骨架:

from locust import User, task, between
from neo4j import GraphDatabase
import random

class GraphUser(User):
    wait_time = between(0.001, 0.01)      # 控制请求间隔,模拟思考时间

    def on_start(self):
        self.driver = GraphDatabase.driver(
            "bolt://db:7687", auth=("neo4j", "pw"),
            max_connection_pool_size=50)   # 池要够,否则测的是池等待

    @task(40)
    def point_lookup(self):
        with self.driver.session() as s:
            s.run("MATCH (a:Account {id:$id}) RETURN a",
                  id=random.randint(1, 1_000_000)).consume()

    @task(30)
    def one_hop(self):
        with self.driver.session() as s:
            s.run("MATCH (a:Account {id:$id})-[:T]->(b) RETURN count(b)",
                  id=random.randint(1, 1_000_000)).consume()

    @task(20)
    def two_hop(self):
        with self.driver.session() as s:
            s.run("MATCH (a:Account {id:$id})-[:T*2]->(b) "
                  "RETURN count(DISTINCT b)",
                  id=random.randint(1, 1_000_000)).consume()

    @task(10)
    def add_edge(self):
        with self.driver.session() as s:
            s.run("MATCH (a:Account {id:$s}),(b:Account {id:$d}) "
                  "MERGE (a)-[:T {ts:$ts}]->(b)",
                  s=random.randint(1, 1_000_000),
                  d=random.randint(1, 1_000_000), ts=0).consume()

    def on_stop(self):
        self.driver.close()

参数分布比查询本身更容易出错:所有请求都用同一个 ID 会全部命中同一页缓存、测出「假快」;ID 均匀随机会让高度数节点与低度节点被访问的概率相同,而真实业务往往「热点更热」(幂律访问)。正确做法是按生产访问频次分布(Zipf)采样 ID,并单独统计「热点 ID」与「长尾 ID」的延迟——参数分布错了,压测结果的绝对值可能差一个数量级。

压测客户端的自我防护:客户端不能成为瓶颈(监控客户端 CPU,打满则加压无效);连接池要足够大,否则测的是等待连接而不是数据库处理;单机客户端通常只能产生几千 QPS,更高需要分布式加压;超时与错误要计数并单列,不要静默丢弃;固定随机种子让不同轮次的参数序列一致,便于对比。客户端瓶颈是压测最常见的「结论错误」来源。

心智:工具选择按目的定:官方驱动做混合负载与正确性校验、Locust/k6 做并发压测、厂商套件做标准对齐;参数分布必须按生产访问频次(Zipf)采样,均匀随机与固定 ID 都会得出错误结论;压测客户端本身可能成为瓶颈,必须监控客户端资源并固定随机种子。


7. 结果可复现性

可复现性是基准测试的底线,不满足它的结果没有参考价值。

必须报告的内容(缺一不可):
1. 硬件:CPU 型号/核数、内存、磁盘类型(SSD/NVMe)、网络
2. 软件:数据库版本、配置项(尤其内存与并发相关)、JVM 参数
3. 数据:规模因子、节点/边数、导入方式与耗时
4. 负载:查询模板、参数分布、读写比例、并发数、持续时间
5. 口径:客户端/服务端延迟、是否读全结果、冷热缓存、预热时长
6. 结果:吞吐、p50~p999、错误率、资源使用率
→ 缺任何一项,别人都无法复现,也就无法验证你的结论

控制变量:一次只改一个。反面案例是「换了数据库版本 + 加了内存 + 调了 page cache 配置」,结果变快了但无法归因是哪一项的贡献。正确做法是先固定基线(记录完整配置快照),每次只改一个变量并跑多轮取中位数,同时记录改动前后所有指标而不只是延迟。调参回归验证尤其要守这条,否则「优化」可能是巧合。

多轮与统计显著性:单轮压测的数字不可信(可能恰好赶上一次 GC 或一次备份)。同一配置至少跑 3~5 轮,报告每轮结果与中位数;观察轮间波动,波动超过 10% 说明环境不稳定,先排查环境;对「A 比 B 快 5%」这类小差异要给出置信区间或明确说明不显著——在环境噪声 10% 的情况下,「快 5%」是无效结论。

环境隔离清单:压测期间不要跑备份、压缩、检查点等后台任务(除非就是要测它们的影响);关闭或固定 CPU 频率调节,避免降频影响结果;同一台机器不要同时跑客户端与服务端(除非就是测单机形态);固定 NUMA 绑定与线程数(跨 NUMA 访问会显著影响延迟);记录是否与其他负载共享物理机(云上尤其常见)。环境噪声是「结果不可复现」的头号原因。

心智:可复现性要求完整报告硬件、软件与配置、数据规模、负载定义、延迟口径、结果与资源指标;控制变量一次只改一个;同一配置多轮取中位数并判断显著性(5% 的差异在 10% 噪声下无意义);压测期间隔离后台任务与频率调节等环境噪声。


8. 实践:一次完整压测

目标 SLO:读写混合(90/10)下 p99 ≤ 200ms,可持续吞吐 ≥ 2000 QPS。

第一步:准备数据与环境:

数据:LDBC SNB SF10(约 30GB)+ 生产脱敏样本做交叉验证
环境:单机 32 核 / 128GB / NVMe;数据量/内存 ≈ 0.25(可全放内存)
配置快照:page cache 分配 80GB、堆 16GB、并发连接上限 1000
基线记录:无负载时的空转资源占用(CPU 2%、内存 18GB)

第二步:预热与冷热对照:导入完成后立即压测,记录前 5 分钟的延迟曲线,观察 page cache 逐步填充带来的延迟下降过程(冷启动测试);然后跑 10 分钟同分布负载让缓存达到稳态,再正式测量 30 分钟(热态测试)。两个结果都要报告,只报热态会掩盖冷启动的雪崩风险。

第三步:并发爬坡找饱和点:逐级加压(如 1/4/16/64/128/256/512),每级跑 120 秒,记录吞吐与 p99。

典型曲线形态:
1→64:吞吐近似线性上升,延迟基本持平(资源未饱和)
64→256:吞吐增速放缓,p99 开始抬升(接近饱和)
256→512:吞吐不再增长,p99 翻倍(排队主导)
→ 饱和点(吞吐拐点)就是容量上限;SLO 应取拐点之前的某个点
→ 规划时留 30%~50% 余量,不要贴着拐点部署

第四步:混合读写比例扫描:固定并发 128,扫描写比例 0% / 5% / 10% / 20% / 30%,观察写比例上升时读延迟的恶化速度。若写比例从 10% 升到 20% 让 p99 翻倍,说明写路径(锁、日志、page cache 污染)是主要瓶颈——这个扫描直接回答「我们的写入强度还能不能加」。

第五步:结论与报告:

结论模板:
- 在 <硬件配置> 上,<数据规模> 的数据,<负载定义> 下:
  吞吐 <X> QPS 时 p99 = <Y> ms;饱和点约 <Z> QPS(并发 <C>)
  写入比例从 10% 提升到 20% 使 p99 从 <a> 升到 <b>
- 瓶颈定位:<CPU/内存/磁盘/GC/锁> 中的哪一项
- 规划建议:按 SLO 留 30%~50% 余量 → 需要 <N> 台
→ 报告里必须有「瓶颈在哪」和「建议怎么扩」,否则只是数据搬运

第六步:把结果落成可对比的表:

每次压测产出一行记录:
配置哈希 | 数据 SF | 并发 | 写比例 | QPS | p50 | p99 | 错误率 | 瓶颈
→ 累积多行后就形成「配置 → 性能」的对照表,
  升级、调参、换硬件都能直接查表,而不必重跑
→ 这张表也是回归验证的基线,没有它「变快了」永远只是感觉

心智:一次完整压测的流程是「准备数据与环境 → 冷热对照 → 并发爬坡找饱和点 → 读写比例扫描 → 出结论与规划建议」;冷启动与热态结果都要报;容量规划应取饱和拐点之前并留 30%~50% 余量;报告必须给出瓶颈定位与扩容建议。


9. 常见错误与排错

压测结论错误的十个常见来源:

1. 客户端成为瓶颈:加压机 CPU 打满,测出的是客户端的极限
2. 不读结果集:只发查询不消费结果,延迟被严重低估
3. 参数分布失真:固定 ID 或均匀随机,与生产访问分布不符
4. 只测热缓存:忽略冷启动,上线后首次访问雪崩
5. 只测一个并发数:得到一个点而非曲线,无法定容量
6. 压测时长太短:没覆盖后台任务周期,测不到尾延迟
7. 剔除超时请求:把最慢的样本从统计里删掉,指标失真
8. 数据量未对齐内存:小图跑出「全内存」的好成绩,大图完全不同
9. 未做正确性校验:跑得快但算错,结论毫无价值
10. 环境噪声未隔离:备份/压缩/降频同时进行,结果不可复现
→ 这十条覆盖了绝大多数「结论错误」的案例,逐条核对

排错:结果异常时按什么顺序查:客户端资源是否打满(CPU/网络/连接池等待);服务端资源瓶颈在哪(CPU/内存/磁盘 IOPS/GC);是否有后台任务在跑(备份、压缩、检查点);查询计划是否退化(索引是否命中、是否全扫);参数分布是否合理(热点比例、结果集大小);数据是否真的都进了内存(page cache 命中率)。按这个顺序查,90% 的异常能在前两步定位。

「跑得快」的三种假象:全部命中缓存(检查 page cache 命中率是否接近 100%,若是则加大数据量再测);查询被短路(如 LIMIT 极小、谓词恒假,检查返回行数与预期是否一致);写入被缓冲(未真正落盘,检查日志刷盘策略,压测后重启数据库验证数据完整性)。三种假象都表现为「数字好得不真实」,遇到时先怀疑自己。

一个可复用的核对表:压测前——SLO 已定义、数据已核对、配置快照已记录、正确性用例已写;压测中——客户端资源已监控、参数分布已确认、超时已计数、时长已覆盖后台周期;压测后——多轮结果已比对、冷热已分开报告、瓶颈已定位、余量已留出。把这张表变成脚本里的断言,比靠人记住可靠得多。

核对表(建议直接写进压测脚本):

阶段检查项
压测前SLO 已定义、数据已核对、配置快照已记录、正确性用例已写
压测中客户端资源已监控、参数分布已确认、超时已计数、时长覆盖后台周期
压测后多轮结果已比对、冷热已分开报告、瓶颈已定位、余量已留出
把这张表变成脚本里的断言,例如「压测结束时若客户端 CPU > 80% 则标记结果无效」,
比靠人记住可靠得多;失败时脚本直接报错,避免拿无效数据出结论。

心智:压测结论错误大多来自客户端瓶颈、不读结果、参数分布失真、只测热缓存、只测一个并发、时长太短、剔除超时、数据未对齐内存、无正确性校验、环境噪声十类;异常排查按「客户端 → 服务端 → 后台任务 → 执行计划 → 参数分布 → 缓存命中」顺序;遇到「好得不真实」的数字先怀疑自己。


10. 速查表

全篇速查:

主题结论
基准用途选型 / 容量规划 / 回归验证,方法论不通用
前提先定 SLO,否则只是看数字
标准基准LDBC SNB(Datagen + Interactive/BI + 更新流 + 校验)
自研时机结构/查询/更新模式与 SNB 不符时
规模因子必须写清,否则数字无意义
生成数据保留幂律度分布、社区聚类、时间演化
内存比值数据量/可用内存决定性能形态,必须报告
混合负载读写比例 + 写入形态 + 查询混合三参数
写入强度单条 / 批量事务 / 离线导入,不可混谈
并发画「并发 → 吞吐 + p99」曲线找饱和拐点
指标吞吐 + p50~p999 + 正确性,缺一不可
延迟口径客户端/服务端、是否读全结果、冷热缓存
参数分布按生产频次(Zipf)采样,别均匀随机
可复现硬件/软件/数据/负载/口径/结果六项全报
规划余量饱和拐点前留 30%~50%

一句话记忆:图数据库基准测试的价值不是排名,而是用可复现的方法回答「在我的数据、查询模式与并发下能否满足 SLO」;用途分选型、容量规划、回归验证三类,方法论不通用,且必须先定 SLO;LDBC SNB 是最主流的标准化基准(Datagen 生成幂律图 + Interactive/BI 负载 + 更新流 + 正确性校验),适合选型与回归,结构与查询模式不符时应自研(从生产慢查询提模板、按频率加权、用脱敏真实图);规模因子与「数据量/可用内存」比值必须写进报告,否则同一份数据在两台机器上会得出相反结论;混合读写负载要同时定读写比例、写入形态(单条/批量/离线导入不可混谈)与查询混合,并发压测要画「并发 → 吞吐 + p99」曲线找饱和拐点;指标必须同时报吞吐、延迟分布(p50~p999)与正确性,并写清延迟口径(客户端还是服务端、是否读全结果、冷热缓存、是否剔除超时);参数分布要按生产访问频次采样,均匀随机与固定 ID 都会得出错误结论;可复现性是底线,硬件/软件配置/数据规模/负载定义/延迟口径/结果六项缺一不可,控制变量一次只改一个;最后,规划容量应取饱和拐点之前并留 30%~50% 余量,报告必须给出瓶颈定位与扩容建议。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「graphdb」更多文章

  1. 查询缓存与物化视图
  2. 图数据测试策略与回归验证
  3. 图数据库并发控制与批量更新