Repository navigation
Benchmarking
简体中文 | 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 行操作,单独报告。
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。
编译、单元测试、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
- Connections
- Tables
- Raw statements
- Writing rows
- Reading rows
- Updating and deleting
- Affected rows
- Errors and waiting
- Transactions
- Types
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.