Skip to content

Benchmarking

github-actions[bot] edited this page Oct 3, 2026 · 19 revisions

基准与压力测试

简体中文 | English

本页说明基准测试的方法、已有结果和后续计划。每项结果都会标明测试环境;尚未测量的项目会注明仍在计划中。

当前服务端基准通过 skript-reflect 调用 System.nanoTime():操作返回后立即记录耗时(wallNs),校验查询和结果计数不计入其中。另由每 tick 运行的观察器记录相邻 tick 起点的最大间隔(gapNs)。报告以六位小数显示毫秒,JSON 报告和 CI 历史保留原始的整数纳秒值。纳秒只是计时单位,不代表单次结果具有纳秒级准确度;调度、GC、其他插件、数据库执行和脚本恢复运行的时机都会影响读数。

这些数字对服务端意味着什么

超过 5000 行的读取会失败,不会保存部分结果。写入时,一条语句绑定的值超过 30000 个,插件会将其拆成多条语句。这些限制可以避免请求无限增大,但允许执行的大批量写入仍可能拉长 tick。

从列表变量执行 insert many 时,插件跨 tick 分段读取源变量,每段最多约 2 毫秒或 4096 步。这样可以限制单次占用主线程的时间,但整次写入可能需要更多 tick 才完成。写入语句返回前,脚本应保持源变量不变。

测试分别观察了三个方面:

  • 插件自身的吞吐。 一次 5000 行的 insert many,在 Intel Xeon Platinum 8573C 上用时 8.022 毫秒,在 AMD EPYC 7763 上用时 8.241 毫秒。对应的 CI 记录分别位于 dev/bench/intel-xeon-platinum-8573c/data.js 和 dev/bench/amd-epyc-7763-64-core-processor/data.js。
  • 历史服务端 tick。 在 AMD EPYC 9V45 的 CI 主机上,写入 5000 行总耗时 220 毫秒,期间最长的 tick 比 50 毫秒预算超出 90 毫秒;写入 10000 行的对应数字为 320 毫秒和 100 毫秒。该历史测试中,读取 5000 行没有超出 tick 预算;读取 10000 行则被拒绝。其他结果见下表。
  • 旧版数据库驱动的行为。 另一项测试连续写入二十批、每批 5000 行,MySQL 用时 17.04 秒,PostgreSQL 和 MariaDB 各用时 1.99 秒。对其中一批,MySQL 服务端统计到 5038 条语句,其中 5000 条是插入;MariaDB 统计到 2 条,其中 1 条是多行插入。这些数据来自旧版 MySQL 插入路径,只对应当时的驱动设置。

当前服务端基准在 SQLite、MySQL、MariaDB 和 PostgreSQL 上对比 5000 行的 insert many 与手写 SQL。MySQL 默认使用参数化多行 INSERT,每条语句最多绑定 30000 个值;其他 JDBC 路径使用预处理批次。手写 SQL 则是一条直接写入值的多行 INSERT,其字符串构造不计入耗时。两者的耗时差异还包含插件读取源变量、参数绑定和写入形式的成本,不能全部归因于 ORM 映射。MongoDB 只执行插件读写用例,并明确跳过 SQL 对照。

手写 Skript 循环如果只写入普通变量,测到的是内存操作,不能用来比较数据库写入性能。

同样写入 5000 行,开发机测得 tick 超出约 10 毫秒,CI 主机测得 90 毫秒。因此,基准工作流增加了独立的 tick 作业,并在结果旁记录主机信息。

tick 超出量是观察到的最大 tick 起点间隔减去 50 毫秒,负数按零计算。它能显示 tick 延迟,却不能直接给出写入在主线程上用了多久;在一个正常 tick 内完成的工作也不会显示为超出。“热读”和“热写”是在其他用例之后重复执行 5000 行操作,单独报告。

当前 CI 覆盖范围

tick 作业为以下五个后端分别配置了 100、500、1000、2500、5000 和 10000 行的插件写入与读取,并在最后重复一次 5000 行热写入和热读取。支持 SQL 的后端另有 5000 行的插件与原始 SQL 对照。新一轮 CI 尚未产出这些数据,因此表中不填旧结果。

后端 插件曲线与热用例 原始 SQL 对照 新版 CI 结果
SQLite 已配置 已配置 待生成
MySQL 已配置 已配置 待生成
MariaDB 已配置 已配置 待生成
PostgreSQL 已配置 已配置 待生成
MongoDB 已配置 跳过:不支持 SQL 待生成

SQL 对照分别记录 pluginwrite、pluginread、rawwrite 和 rawread 日志;MongoDB 则在插件读写后明确记录 SKRIPTORM_BENCH=SKIP。

下表使用旧版粗粒度计时,部分写入耗时还包含后续计数查询。它仅作为历史参考,不能与新版 wallNs、gapNs 直接比较。

CI 记下的是这些,来自提交 51aa760 上的 Benchmarks #2、作业 Tick benchmark,按 CPU 存在 dev/bench/amd-epyc-9v45-96-core-processor/tick/data.js:

操作 行数 墙钟 tick 超出
读 100 60 ms 10 ms
读 500 60 ms —
读 1000 60 ms —
读 2500 100 ms —
读 5000 90 ms 0 ms
读 10000 60 ms 0 ms
热读 5000 70 ms 0 ms
写 100 90 ms 10 ms
写 500 30 ms 20 ms
写 1000 80 ms 30 ms
写 2500 70 ms 30 ms
写 5000 220 ms 90 ms
写 10000 320 ms 100 ms
热写 5000 190 ms 0 ms

“写入 500 行”的总耗时为 30 毫秒,tick 却超出 20 毫秒,说明共享主机上的其他活动可能影响了单次采样。这类耗时数据用于观察趋势,工作流不会据此判定失败。

下文说明各层的测量方法、工具和目前尚未覆盖的项目。

测量分层

数据库操作的耗时主要来自三个层次:

  • A:只有 JVM。 值转换、方言拼出来的 SQL、批次怎么切、结果怎么变成行、事务外面的那些记账。这里没有数据库,也没有服务端:用 JMH,单独放在一个模块里,日常构建不为它花时间。
  • B:真数据库。 往返、服务端自己的解析与执行、连接池、提交。用 Testcontainers,每个后端一个真实例,装置就是集成测试已经在用的那一套。
  • C:真 Paper 服务端。 语句对 tick 的占用:从变量里把批次读出来、把结果写进变量、大结果占住的内存、MSPT。一次性服务端,用脚本驱动。

分层测量有助于定位问题:值转换属于 A,驱动批处理属于 B,主线程上的 Skript 变量读写属于 C。

触发器的总耗时包括主线程之外执行的数据库操作,不能单独用来判断服务端是否卡顿。读取输入变量、保存查询结果则占用主线程时间。两项指标需要分别报告,才能区分数据库延迟与 tick 延长。

当前服务端用例报告什么

Paper 脚本目前记录 wallNs、gapNs、tick 数,以及影响行数或已保存行数。报告从 gapNs 推算 tick 超出量,并校验行数。它尚不能单独测出主线程 CPU 耗时、数据库耗时、内存分配量或 MSPT 百分位。

后续计划补充的指标

下表是各测量层的目标,不代表每个现有服务端用例都已输出这些指标。

指标 用什么读 来自
服务端线程毫秒数 服务端线程上的时间 C
线程外毫秒数 墙钟 B、C
触发器总延迟 脚本看到的那段墙钟 B、C
吞吐 每个值、每一行 A、B、C
交给驱动的语句数与行数 数出来的,不是量出来的 B
服务端真正收到的语句数,以及它写下的行数 数出来的,不是量出来的 B,在安装型服务器的作业里
每次操作的分配量 字节,不是时间 A、B
峰值堆占用 用例结束后、回收一次之后 B、C
MSPT 百分位(p50、p99、最大值) 服务端自己的 tick C

要把触发器总耗时拆成数据库耗时与主线程耗时,还需要额外的测量手段;目前的 wallNs 包含这两部分,以及脚本等待恢复运行的时间。

计数与耗时

在共享 CI 主机上,相同测试的耗时曾相差 10% 至 20%;语句数则比较稳定。因此,CI 可以直接检查计数,耗时只作报告,不据此判定构建失败。

前述旧版二十批写入测试对每个数据库使用相同脚本和表定义。MySQL 与 MariaDB 共用 SQL 方言,但驱动发送批次的方式不同:当时在 JDBC 层,插件只提交一个批次;服务端计数却显示,Connector/J 将其中 5000 行逐行发送。单看耗时无法找到这个原因。当前 MySQL 路径改为参数化多行插入,下述旧计数不能作为新版的预期值。

基准模块还会统计 JDBC 层的调用。使用内存 H2 和通用 JDBC 路径时,一次 5000 行、六列的插入提交了一个批次。插件准确报告了 5000 个影响行,调用结束后表中也有 5000 行。

ServerSideCountIntegrationTest 测量真实数据库收到的语句。对 MySQL 系数据库,它在插件调用前后读取 Questions,扣除探针自身的开销,同时读取 Innodb_rows_inserted、Com_insert 和 Com_stmt_execute。这些计数覆盖整个服务器,因此测试会先检查是否有其他连接;如有,就只报告数字,不据此断言结果。驱动版本和设置会改变计数,所以结果会连同环境信息一起记录,不用于 CI 门禁。

在一次 5000 行测试中,MySQL 8.0.46 和 8.4.11 各记录到 5038 条语句,其中 5000 条是插入语句(comInsert=5000、comStmtExecute=0)。MariaDB 11.4.13 记录到 2 条语句,其中一条是多行插入(comInsert=1、comStmtExecute=1)。测试账号无法从 MariaDB 取得插入行数。PostgreSQL 16.15 为目标表统计到 5000 行、为整个数据库统计到 5057 行,但现有计数器无法提供语句数;等待异步统计视图更新用了 814 毫秒。各后端都由插件报告了准确的 5000 个影响行,表中也确实增加了 5000 行。

这些都是每个后端在一台主机上的单次结果,不代表区间。日志记录了环境信息:os=Linux 6.17.0-1022-azure arch=amd64 cores=4 java=25.0.4.1 commit=81526798498f5ad30068fcb3299f2085062f868d。即使在同一次工作流中,MySQL 和 MariaDB 的作业运行于 Intel Xeon Platinum 8370C,PostgreSQL 的作业则运行于 AMD EPYC 7763。比较耗时时应看各作业实际使用的主机。

测试读取 PostgreSQL 的 pg_stat_user_tables.n_tup_ins 和 pg_stat_database.tup_inserted,并等待这些异步统计数据更新。它们统计行数,无法推算发送了多少条语句。要获得语句数,需要配置 pg_stat_statements 并重启服务器,或在协议层另行计数。

测试工具

用途 工具 为什么是它
微基准(A) 手工接入的 JMH JDK 工程师自己在用的那套;预热、进程分离、消除死代码是它的活,不该是我们的
交给驱动的语句数与行数(B) datasource-proxy,或自己包一层计数驱动 在不改动被测代码的前提下数清插件自己的切分——但在这个层级上,一个批次就是一次调用,带一行还是带五千行都一样
服务端真正收到的语句数与往返数(B) 数据库自己的会话计数器,或抓包 驱动自己的批处理只在这一层可见,而 MySQL 那次的差别正住在这里;它随驱动版本和连接参数变化,所以每次运行都报告,但不拿来做门禁
tick 采样的百分位(C) HdrHistogram JMH 自带自己的置信区间;采样循环需要自己的直方图
真数据库(B) 开了复用(reuse)的 Testcontainers 集成测试本来就会起真实例;复用让每次运行不必重付启动成本
历史与对比 benchmark-action/github-action-benchmark 按 CPU 和后端保存结果;耗时变化不作为构建失败条件
分配与停顿 JMH -prof gc、JFR、async-profiler 给出每次操作的字节数,并在某个用例比预期慢时给出 GC 与安全点的细节
MSPT(C) Paper 自己的 tick 采样 量 tick,而不需要先信任一个我们自己写的插件

基准模块直接声明 JMH 及其注解处理器,因为现成的 Gradle 插件不支持本项目使用的 Gradle 版本。运行 ./gradlew :benchmarks:jmh,结果会写入 build/benchmarks/results.json。可用 -Pbenchmarks.filter=<正则> 选择用例,或用 -Pbenchmarks.forks=1 缩短本地测试时间。JMH 的注解处理器生成 Java 代码,因此基准类使用 Java。

CI 检查

编译、单元测试、ktlint 和集成测试编译已纳入 CI。

性能检查只应对语句数、每次操作的内存分配量等稳定指标判定失败。共享 CI 主机的耗时波动较大,不适合作为构建失败的阈值;耗时单独报告。

基线放在哪里

基准结果只与硬件环境相近的运行比较。

  • CI 历史由基准测试 action 保存在 gh-pages 分支的 dev/bench/<slug>/ 下,按 CPU 型号分组。ubuntu-latest 可能分配不同处理器:同一提交的两次运行分别落在 AMD EPYC 9V74 和 Intel Xeon Platinum 8573C 上,插入用例耗时相差 1.32 倍。每个作业都会单独记录 CPU 型号;新版 tick 结果还按后端分开,存于 dev/bench/<slug>/tick-ns/<backend>/。
  • gh-pages 分支用于存储基准数据,本项目没有从中发布图表站点。JMH 原始结果位于 dev/bench/<slug>/data.js;tick 结果使用上述后端目录。
  • **benchmarks/baseline.json**仅供本地参考,不作为 CI 阈值。benchmarks/baseline.environment.txt 记录生成基线时使用的机器。

结果还会记录 JDK、操作系统、CPU、容器镜像、数据库驱动、Paper 和 Skript 的版本,以及提交和工作区状态,以便区分代码变动与环境变动。

测试计划

  • 阶段 0:微基准。 为值转换、结果存储、批次输入及 insert many 发送的语句数建立 JMH 用例,保存 JSON 结果和参考基线。
  • 阶段 1:数据库矩阵。 为各后端、各类语句和不同表结构加入语句计数。
  • 阶段 2:服务端 tick(已配置五种后端)。 ./gradlew serverBenchmark 启动装有 Skript、skript-reflect 和本插件的 Paper 服务端,运行 server-benchmark/skript 中的脚本。脚本通过 SKRIPTORM_BENCH 输出以纳秒为单位的耗时及最长 tick 间隔;如果写入行数不符或脚本输出 FAIL,任务就会失败。CI 另行检查 5000 行读取上限和 30000 值写入预算。夜间和按需运行的 tick 矩阵将结果存入 dev/bench/<slug>/tick-ns/<backend>/。详见读取行和写入行。脚本计时器无法提供 MSPT 百分位。
  • 阶段 3:压力与故障测试(计划中)。 覆盖并发、取消、执行途中断开数据库、查询期间重载插件、超限结果与批次,以及连接池耗尽。
  • 阶段 4:完善 CI 报告(计划中)。 增加上述尚缺的指标,将稳定的正确性检查与耗时趋势分开处理。

这里不覆盖什么

  • 在线玩家。 空闲服务端上的结果无法预测大量玩家和实体活动时的表现。
  • 长时间运行。 现有用例仅持续数秒,无法发现几天后才出现的泄漏。
  • 跑不起来的后端。 通用 JDBC 连接能接上本仓库没有测过的驱动,在 MySQL 上量到的结果并不能描述它们。
  • PostgreSQL 语句数。 当前统计信息能提供行数和事务数,不能统计单条操作发送的语句数。
  • 跨机器的绝对耗时。 耗时必须结合测试环境解读。

skript-orm

参考

实用指南


skript-orm (English)

Reference

Practical guides


Wiki 由仓库中的 README 和 docs/ 自动生成。修改文档请到仓库提交,直接编辑 Wiki 的内容会在下次同步时被覆盖。

This wiki is generated from the README files and docs/ in the repository. Please submit changes there; direct wiki edits are overwritten on the next sync.

Clone this wiki locally