Skip to content

Benchmarking

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

基准与压力测试

简体中文 | English

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

服务端每次更新称为一个 tick;通常每秒更新 20 次,每次的目标耗时不超过 50 毫秒。

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

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

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

从列表变量执行 insert many 时,插件跨 tick 分段读取源变量。每段最多处理 4096 个步骤,每 64 步检查一次时间,运行约 2 毫秒后暂停。单个值的转换仍可能超过 2 毫秒,因此这只是控制主线程耗时的目标,并非硬上限。整次写入可能需要更多 tick 才完成;写入语句返回前,脚本应保持源变量不变。

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

  • JVM 基准中的插入耗时。 一次 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 行没有超出 50 毫秒;读取 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 循环如果只写入普通变量,测到的是内存操作,不能用来比较数据库写入性能。

旧版脚本用 Skript 的 now 计时(约每 10 毫秒变化一次),测得 5000 行写入在开发机上的 tick 超出约 10 毫秒,在 CI 主机上超出 90 毫秒。基准工作流会在每条结果旁记录主机信息。

tick 超出量是观察到的最大 tick 起点间隔减去 50 毫秒,负数按零计算。这个指标能显示 tick 延迟,却不能直接给出写入在主线程上用了多久;在一个正常 tick 内完成的工作也不会显示为超出。表中的“热读”和“热写”只是指在其他用例之后再执行一次 5000 行读取或写入,不表示缓存一定已预热。

当前 CI 覆盖范围

tick 作业分别对下表的五种数据库实现(后端)运行 100、500、1000、2500、5000 和 10000 行的插件写入与读取,并在最后再次执行 5000 行写入和读取。支持 SQL 的数据库另有 5000 行的插件与原始 SQL 对照。输入行只填充 id 列,这样比较集中在行的传输和存储,而不是某种六列数据组合。

后端 插件曲线与热用例 原始 SQL 对照 新版 CI 结果
SQLite 已配置 已配置 Run #4,AMD EPYC 7763,SQLite
MySQL 已配置 已配置 Run #4,Intel Xeon 6973P-C,mysql:8.4
MariaDB 已配置 已配置 Run #4,AMD EPYC 7763,mariadb:11.4
PostgreSQL 已配置 已配置 Run #4,AMD EPYC 7763,postgres:17
MongoDB 已配置 跳过:不支持 SQL Run #4,Intel Xeon 6973P-C,mongo:8

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

Run #4 于 2026-10-03 在提交 6476c9b 上完整通过,环境为 Paper 26.2 build 124、Skript 2.16.2、 Java 25.0.4.1、Linux 6.17.0-1022-azure 和 4 个可见 CPU。下表是该次运行的 5000 行结果。 为便于阅读,wallNs 在这里换算成毫秒;上传的 JSON 和 CI 历史仍保留纳秒整数。每个数 只代表共享 GitHub runner 上的一次运行,不能据此给数据库做普遍排名。

后端 写入总耗时 写入最大 tick 间隔 读取总耗时 读取最大 tick 间隔
SQLite 735.828866 ms 65.767643 ms 103.186998 ms 103.066121 ms
MySQL 462.517747 ms 50.395059 ms 78.302884 ms 78.218777 ms
MariaDB 685.122311 ms 66.066614 ms 95.789059 ms 95.813175 ms
PostgreSQL 585.103286 ms 66.926809 ms 94.172914 ms 94.082624 ms
MongoDB 717.433104 ms 50.968606 ms 78.761947 ms 78.579400 ms

同一轮曲线中的 10000 行写入和重复执行的 5000 行热写入:

后端 10000 行写入耗时 最大 tick 间隔 5000 行热写入耗时 热写入最大 tick 间隔
SQLite 1501.908676 ms 94.670715 ms 779.342861 ms 50.628368 ms
MySQL 1398.534310 ms 51.944607 ms 684.881529 ms 50.515912 ms
MariaDB 1149.905662 ms 101.135769 ms 675.078706 ms 50.746686 ms
PostgreSQL 1255.801227 ms 95.786030 ms 623.383051 ms 50.885682 ms
MongoDB 1196.824500 ms 53.798503 ms 683.646486 ms 50.583169 ms

以下对照与上面的曲线分开运行,四列都是处理 5000 行的总耗时。插件写入包含跨 tick 读取输入变量的时间,原始 SQL 的字符串则在计时前就已构造完成。两者走的是不同的 写入路径,因此耗时比值不能直接当成 ORM 自身的开销。

后端 插件写入 原始 SQL 写入 插件读取 原始 SQL 读取
SQLite 727.519482 ms 17.148948 ms 112.968835 ms 104.414372 ms
MySQL 586.972838 ms 46.793973 ms 76.245264 ms 91.635107 ms
MariaDB 628.867037 ms 74.916475 ms 91.608649 ms 89.937610 ms
PostgreSQL 778.881589 ms 61.349564 ms 99.224999 ms 102.880893 ms
MongoDB 685.494020 ms 不适用 78.850687 ms 不适用

100、500、1000、2500 和 10000 行用例、5000 行热用例、影响行数或保存行数校验,以及 预期中的 10000 行读取拒绝也都已完成。gap 不是插件 CPU 耗时,而是观察到的最大 tick 起点间隔,其中包含调度和共享主机活动。本次运行早于提交 21898af 的 MySQL max_allowed_packet 修复;要测量当前 MySQL 路径,需要在该修复推送后重新运行。

下表使用 Skript 的 now 计时(约每 10 毫秒变化一次),部分写入耗时还包含后续计数查询。这组旧数据仅供参考,不能与新版 wallNs、gapNs 直接比较。

以下历史结果来自提交 51aa760 的 Benchmarks #2 工作流(Tick benchmark 作业),保存在 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:不启动 Paper 的 JVM 基准。 JMH 是用于反复运行并测量 Java 代码的基准测试工具,用例放在独立模块中。纯逻辑可以不连接数据库单独测量;现有插入用例使用内存 H2,因此结果同时包含数据库执行和插件代码的成本。
  • B:真实数据库。 测量数据库往返、语句解析与执行、连接池及事务提交等成本。通过 Testcontainers 启动各后端实例,沿用集成测试的配置。
  • C:Paper 服务端。 观察脚本执行期间的耗时和 tick 间隔,包括主线程读取源变量、保存查询结果等工作。基准脚本运行在一次性服务端上;内存占用与 MSPT(每个 tick 的耗时,单位毫秒)的分布仍需另外测量。

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

触发器的总耗时包含数据库操作、主线程上的变量处理,以及等待脚本恢复运行的时间,不能单独用于判断服务端是否卡顿。最长 tick 间隔可以显示延迟,但不能准确拆分这些成本。

当前服务端用例报告什么

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

后续计划补充的指标

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

指标 拟记录内容 测量层
主线程耗时 操作在服务端主线程上占用的时间 C
后台耗时 数据库工作在后台线程执行的时间 B、C
触发器延迟 从语句开始到脚本恢复运行的总耗时 B、C
吞吐量 每秒处理的行数和值数 A、B、C
提交给驱动的语句数与行数 在 JDBC 边界统计调用与行数 B
数据库收到的语句数与写入行数 通过数据库计数器统计 B
每次操作的内存分配量 每次操作分配的字节数 A、B
峰值堆占用 操作期间持续采样得到的最大堆占用 B、C
回收后堆占用 用例结束并执行垃圾回收后的堆占用 B、C
MSPT 分布(p50、p99、最大值) 每个 tick 的耗时:中位数(p50)、99% 的 tick 不超过的耗时(p99)和最大值 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 并重启服务器,或在协议层另行计数。

工具与后续方案

JMH、Testcontainers 和基准历史记录已用于现有测试。其余工具列为补充指标的可选方案。

用途 工具 作用
微基准(A) JMH 正式计时前先运行代码,让 JVM 完成常见优化;用独立进程测量,并避免 JVM 把结果未被使用的被测代码删掉
提交给驱动的语句数与行数(B) datasource-proxy 或计数驱动 统计 JDBC 调用;单次批量调用实际包含多少行,还需另行记录
数据库收到的语句数与往返数(B) 数据库会话计数器或网络抓包 观察驱动批处理后的实际行为;结果受驱动版本和连接参数影响
tick 采样的百分位(C) HdrHistogram 对多次 tick 采样计算分位数
真实数据库(B) Testcontainers 启动测试数据库;复用实例可减少重复启动时间
历史与对比 benchmark-action/github-action-benchmark 按 CPU 和后端保存结果;耗时变化不作为构建失败条件
内存分配与停顿 JMH -prof gc、JFR、async-profiler 分析内存分配,以及 JVM 为垃圾回收等操作暂停线程的时间
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。

tick 基准作业会因结果行数不符或脚本报告错误而失败,不会因耗时变化而失败。后续如增加其他稳定且可复现的检查指标,可以与受共享 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 结果位于 dev/bench/<slug>/tick-ns/<backend>/。
  • **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