Files
layue13andClaude Opus 5.5 b7d6497ca8 perf: WebSocket 经 CDN:wss 改用 BoringSSL、保活 Ping、整合包形态基准
- ProxySessionBenchmark 新增 --profile modded(仿大型整合包的双向包组与 2 MiB 登录突发,逐字节校验)、
  --websocket-tls 1(keytool 一次性证书的 wss)和 --client-coalesce 1。
- wss 监听改用随包附带的 BoringSSL(netty-tcnative-boringssl-static 2.0.84):TLS 占 wss 代理 I/O CPU 的 55%,
  换用后每帧 CPU 在每一对配对测量中都更低(窗口 1 -22%,窗口 8 -36%),帧率 +31%/+34%;其他平台回落 JDK,
  启动日志标明所用实现。jar 由 10.4 MB 增至 16.5 MB。
- 新增 websocket.pingIntervalSeconds(默认 30,0 关闭):升级后代理静默不超过该时长,期间无写出就发空 Ping,
  避免 CDN/负载均衡在安静的登录、选服阶段按空闲超时断开。IdleStateHandler 版本在窗口 8 多耗 7.6% CPU,
  未达规则;改为写时置位标志、每半个间隔检查的实现,JFR 中几乎不可见。
- 文档:WebSocket 接入新增“经 CDN 部署”,更正消息大小描述;记录见
  testing/benchmarks/results/2026-10-23-websocket-cdn.md。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:40:48 +08:00

110 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试、烟测与性能验证
## 开发验证
```powershell
.\gradlew.bat check
.\gradlew.bat :proxy-core:installedDistSmokeTest
.\gradlew.bat -Pluckperms :proxy-core:permissionAcceptance
.\smoke\container-network.ps1
sh testing/smoke/container-channel.sh
```
定向测试覆盖控制通道认证与代次、目录租约、插件生命周期与超时、协议边界、relay 背压,以及合成 TCP 登录和切换。初次路由最新专项验收包括自动化覆盖和实际 Java 8 Uranium 后端协议探针,见[2026-09-27 路由验收](../testing/smoke/results/2026-09-27-initial-routing.md);它不是完整模组包视觉验收。2026-09-26 Prism 记录保留为其提交和环境下的历史真实客户端/配对测量,见[历史 Prism 记录](../testing/benchmarks/results/2026-09-26-prism-pack.md)。下面其他历史结果也只支持其记录注明的范围。
`testing/smoke/container-network.ps1` 使用 Docker bridge 中的独立代理、后端和客户端容器,依次检查静态配置和控制通道注册、登录及 PLAY 转发;控制通道模式还验证注销后已连接会话继续转发。PowerShell 7 和 Docker Desktop 是运行前提。`testing/smoke/container-channel.sh` 在 Unix shell 下验证控制通道的同一跨容器路径。2026-09-25 的[容器网络烟测记录](../testing/smoke/results/2026-09-25-container-network.md)是旧实现的历史结果,不代表当前通道已在那次运行中验证。容器烟测不能代替跨物理主机或目标 Forge 整合包实测。
`.github/workflows/verify.yml` 在每个 Pull Request 上运行 `gradlew -Pluckperms check`,并单独构建、测试 `client-mod/`。`.github/workflows/ci.yml` 在 `main` 推送或手动触发时先执行 `gradlew -Pluckperms check release`,再上传 `build/release/` 并发布 Maven 模块。发布步骤要求 `MAVEN_USER` 与 `MAVEN_PASSWORD` secrets;本地 `check` 不能证明远端 Maven 发布成功。版本规则、Maven 坐标和发行物 见[发布与依赖版本](publishing.md)。
带 `-Pluckperms` 时 `check` 包含 `permissionAcceptance`(本地默认 `check` 不编译 LuckPerms,因此不含这一项;CI 始终开启):使用发行包中的实际 LuckPerms JAR、隔离插件类加载器和临时 H2 数据库,验证原生命令、授权/拒绝/未设置、组继承、临时节点到期、后端上下文、连接代次隔离及关闭后重新打开数据库。输出位于 `proxy/core/build/permission-acceptance/run-*/`,成功标记为 `PERMISSION_ACCEPTANCE_PASS`。它使用 API 玩家探针,不连接真实 Minecraft 客户端,也不证明外部 SQL/Redis 联调成功;真实会话的权限准备和释放由 TCP 生命周期测试另行覆盖。
如果本地已有 Uranium 1.7.10 可运行包及其编译好的 `MinecraftProtocolProbe`,可运行 `testing/smoke/local-uranium.ps1 -BundlePath <包目录> -ProbeClassesPath <探针类目录>`。脚本复制服务端到忽略目录,启动 Java 8 后端与当前安装包,再让探针经代理完成状态查询、FML 登录、Join Game 和持续 Keep Alive,结束时停止两个进程。本地一次结果与具体前提见 [最小 Uranium 联机记录](../testing/smoke/results/2026-09-25-local-uranium.md)。初次路由的当前 `ProxyMain` 协议探针结果另见[路由验收](../testing/smoke/results/2026-09-27-initial-routing.md)。真实客户端记录见下段和其单独的历史验收档案;它们不应与当前路由分支的完整模组包验收混为一谈。
`testing/smoke/local-uranium-transfer.ps1 -BundlePath <包目录>` 复制并启动两台 Uranium,然后编译仓库中的协议探针,通过实际 `ProxySessionListener` 请求从旧服切换到新服。加上 `-InstalledPlugin` 则启动已安装的 `ProxyMain`:旧服由静态配置注册,临时插件用 `Servers.register` 注册目标服,在初始落点回调读取 `ServerView`,再通过 `Players.transfer` 发起转服。默认模式使用协议探针核对 FML 重置、重新握手、世界切换包和目标连接的 Keep Alive,见 [两台 Uranium 转服记录](../testing/smoke/results/2026-09-25-local-uranium-transfer.md)。`-InstalledPlugin -ReturnToOld` 让协议探针验证旧服→新服→旧服的两次切换。`-InstalledPlugin -PrismClient` 改用本机 Prism 中的 `1.7.10` Forge 实例,要求客户端在目标服保持连接 10 秒;可同时使用 `-ReturnToOld` 验证真实客户端往返。两个真实客户端脚本都可用 `-PrismInstance <实例名>` 选择其他实例。若实例的文件夹名与启动名不同,另传 `-PrismInstanceFolder <文件夹名>`,以便准确定位并清理这次启动的客户端;`-PrismPath` 指向其他 Prism 安装位置。`testing/smoke/local-uranium.ps1 -PrismClient` 可单独验证首次登录。这些实测仍不能替代目标整合包验收。
安装包转服烟测可加 `-DebugSession`,在本次复制的 Uranium 配置中开启登录阶段日志,并将代理会话的连接、登录写入和失败路径 DEBUG 日志写入所打印运行目录的 `proxy.stdout.log`。加 `-TraceBackend` 会在代理与两台 Uranium 之间放置本地 TCP 中继,并将每个方向的前 512 字节记录到运行目录的 `tap-old.log` 和 `tap-new.log`;这一诊断选项会改变连接时序。初次登录 EOF 的复现实验见 [运行记录](../testing/smoke/results/2026-09-26-initial-login-repeat.md);增强日志和中继后捕获的 Uranium 登录停滞及源码假设见 [登录停滞记录](../testing/smoke/results/2026-09-26-uranium-login-stalls.md)。
## 历史验证记录
带日期的逐轮验证结果放在 [`records/`](records/),与 `testing/smoke/results/`、`testing/benchmarks/results/` 一样只证明各自注明的提交和环境:
- [2026-09-26 验证记录](records/2026-09-26-validation-log.md):玩家操作与服务器列表、统一事件模型、历史类型化事件与访问检查。
- [2026-09-27 富文本与命令补全验证](records/2026-09-27-rich-text-completion.md)。
## 泄漏闸门与变异检查
`proxy-core` 的测试 JVM 以 `-Dio.netty.leakDetection.level=paranoid` 运行,`dev.moonbridge.testing.LeakGate` 自动注册到每个测试类:类结束后强制 GC 并读取 Netty 的泄漏报告,有未释放的缓冲区就使该类失败。`LeakGateTest` 证明它能发现故意制造的泄漏。注意 `Unpooled.wrappedBuffer(byte[])` 创建的缓冲区不被追踪,涉及它们的测试应直接断言 `refCnt()`。
2026-10-05 对手写 `release()` 位置做过一次变异检查(把单个 `release()` 换成不改引用计数的空操作,再跑覆盖它的测试):对 relay、帧处理器、Tab 补全、协议编码和加解密共 47 处,改造前 23 处无人守护;集中到 `ByteBufs`、`FrameTransformHandler` 并补测试后,剩余 33 处中 32 处被抓到,最后一处随加解密处理器改用 Netty 基类而消失。`ConnectionGate` 与 `Session` 的第二轮检查(3 路并发,每个工作线程一份仓库副本,失败的变异再跑一次以排除负载下的偶发超时)覆盖 17 处:10 处被抓到(`ConnectionGate` 全部 3 处由新增单元测试抓到),1 处结果不稳定不计入(`Session:143`),6 处存活。存活中的 `mapTransitionFrame`、`transferOpening` 的帧改写与失败分支、关闭时清空过渡缓冲,已抽成 `TransitionFrames`、`TransitionBuffer` 并补了单元测试(对这两个类再做一次变异,4 处全部被抓到)。`Session:143` 与 `Session:147`(来自既非前端也非后端的通道的消息、非 ByteBuf 消息)由 `SessionFrameOwnershipTest` 直接构造 `Session` 守护,对这两行的变异都被抓到;至此已评估的手写 release 位置都有测试守护。
## 合成 relay 基准
`testing/benchmarks/run-relay.ps1` 比较同一 JVM、同一个本机回声后端上的直连、原始字节 relay、按帧 relay,以及安装了实际 `KeepAliveBridge` 的按帧 relay。每个连接先预热,再重复发送固定长度的合成 Minecraft 帧并读取同样长度的回声;连接数、每连接消息数、预热数、帧负载字节数、重复轮数和同时在途的消息数均可配置。`-Window 1` 是逐包等待回声;更大的窗口使用独立写线程持续发送,读线程按顺序核对回声,并用信号量限制在途消息数。基准在轮次间交替执行三种 relay,并在首尾测直连基线。输出往返吞吐、按单向传输字节计算的 MiB/s、往返延迟 p50/p95/p99、JVM GC 次数/耗时和堆已用量变化。
```powershell
.\benchmarks\run-relay.ps1 -Connections 8 -Messages 2000 -Warmup 200 -Payload 4096
.\benchmarks\run-relay.ps1 -Connections 8 -Messages 2000 -Warmup 200 -Payload 4096 -Window 16
```
该基准只覆盖本机 TCP 回声和三种 relay 数据路径。不同窗口使用不同的客户端发送方式,应在相同窗口内比较。负载是合成帧,不含登录、Forge 握手、模组流量、转服时的实体 ID 改写或真实客户端/后端行为;结果不代表 1.7.10 整合包等价性能,也不设 CI 性能门槛。堆变化是阶段前后的粗略观测,不是分配速率;请在目标机器、JDK 和连接规模上多轮运行并记录环境,避免把单次结果当成容量承诺。
本地一次测量的环境、参数与原始输出见 `testing/benchmarks/results/2026-09-25-local-relay.md`。
加入实际 `KeepAliveBridge` 后的配对测量见 `testing/benchmarks/results/2026-09-25-keepalive-bridge.md`;两份结果使用的 relay 事件循环安排不同,不能直接视为前后性能对比。
## 真实 TCP 慢接收端背压回归
`RawRelaySocketTest` 使用实际 loopback TCP 和 Netty NIO 通道,缩小 socket 发送缓冲与 Netty 写水位,让停止读取的接收方造成真实的待发送积压。测试观察在目标仍有未完成写入时源读取停止(Linux 上短读取可能低于高水位,因此不以不可写状态为条件)、排队字节有界,恢复接收后逐字节校验双向数据,并覆盖拥塞中接收方断开后的通道与待发送写入清理。
```powershell
./gradlew.bat :proxy-core:test --tests '*RawRelaySocketTest'
```
这是有意限制缓冲的原始转发层回归,不经过登录、加密或 Forge 协议,不代表默认配置下的容量或目标整合包表现。现有 `RawRelayTest` 继续覆盖暂停、恢复、交接及引用计数的确定性边界。
## 合成会话基准
`testing/benchmarks/run-proxy-session.ps1` 在同一 JVM 中比较直接连接模拟后端,以及经过实际 `ProxySessionListener` 登录、落点选择和会话转发后连接同一后端。客户端先完成离线登录,读取 Join Game 和 Position and Look,再预热;计时仅包含固定 PLAY 帧的往返。`-Window` 设置每个客户端允许的在途请求数,默认 `1`(stop-and-wait);大于 `1` 时客户端最多连续发送 Window 个帧,再按 TCP 顺序读取回声并逐条校验,每收到一条便补发一条。预热和计时阶段、直连和代理都使用相同的发送算法。测得的单条延迟从该请求写出前计时到其回声读完;延迟样本和在途时间戳都使用有界数组,受 `Connections`、`Messages` 和 Window 上限约束。出现错误回声或阶段超时时基准以失败退出。
```powershell
.\benchmarks\run-proxy-session.ps1 -Connections 4 -Messages 1000 -Warmup 100 -Payload 1024 -Repeats 2 -Window 1
.\benchmarks\run-proxy-session.ps1 -Connections 4 -Messages 1000 -Warmup 100 -Payload 1024 -Repeats 2 -Window 1 -Mode post-transfer
```
2026-09-25 的多轮、同条件本机测量及原始输出见 [生命周期修复后的会话基准记录](../testing/benchmarks/results/2026-09-25-session-after-lifecycle.md)。更早的测量见 [原会话基准记录](../testing/benchmarks/results/2026-09-25-current-session-benchmark.md)。这些记录对应各自注明的源码版本,不是当前事件 API 的性能结果,也不代表后端负载观测。
`-Mode post-transfer` 让代理客户端先从 `bench` 转到 `replacement`,直连客户端直接进入同一个 `replacement` 模拟后端;转服与预热均在计时外。两后端使用不同玩家实体 ID,因此代理在转服后仍运行实体 ID 映射路径。[转服前后会话基准记录](../testing/benchmarks/results/2026-09-25-post-transfer-session.md) 包含窗口 1 和 16 的同条件多轮测量及原始输出。结果没有显示值得据此修改普通转发路径的稳定差异。
加入 Forge 握手阶段旧世界数据包过滤后,在该历史版本上重跑的 [转服后基准](../testing/benchmarks/results/2026-09-25-forge-transfer-gate-followup.md) 与先前结果的范围重叠;合成流量没有显示稳定的大幅退化,目标整合包仍需单独测量。
同一提交在独立 Linux 容器中的 [转服后会话基准](../testing/benchmarks/results/2026-09-25-linux-container-post-transfer.md) 也完成了窗口 1 和 16 的各五轮对照与逐包校验。该结果仅验证另一运行环境中的合成路径;目标整合包和跨主机网络仍需实测。
2026-09-25 会话路径的 [JFR 采样记录](../testing/benchmarks/results/2026-09-25-session-jfr-screening.md) 只提供了分配线索,代理 I/O 线程的 CPU 执行样本不足以定位热点;因此没有据此改动普通转发路径。
[玩家命令拦截初筛](../testing/benchmarks/results/2026-09-26-command-interceptor.md) 比较同一合成 PLAY 回声流量下启用与关闭命令拦截器的结果;三轮吞吐和延迟范围重叠。该实验没有发送命令,也不能替代目标整合包的容量验收。
将 `-Window` 改为 `16` 可测每连接最多 16 个在途往返的情形。
`-Burst N`(直接运行 Java 时为 `--burst N`)让每次往返由客户端一次写出 N 个帧、模拟后端一次 flush 回声,接近一个服务器 tick 内的多个小 PLAY 包;此时 Window 按批计数。输出另含代理 I/O 线程每帧 CPU 时间,以及在 Linux 上读取 `/proc/net/snmp` 得到的全机 TCP 发送段数/帧(含 ACK,其他平台显示 `n/a`)。[2026-10-05 批量 flush 记录](../testing/benchmarks/results/2026-10-05-relay-batched-flush.md)用它比较逐帧 flush 与按读取批次 flush。`-Transport auto|nio|epoll|kqueue`(`--transport`)选择代理使用的网络传输层,默认 `auto`;[2026-10-05 传输层记录](../testing/benchmarks/results/2026-10-05-network-transport.md)比较了 epoll 与 NIO。
`--websocket 1`(直接运行 Java 时)让代理阶段的客户端通过 `ws://` 连接代理的 WebSocket 监听器:完成升级后,客户端每次 flush 发送一条带掩码的二进制消息,读取时按 RFC 6455 拆出服务器消息的负载。直接阶段仍用 TCP 连接同一个模拟后端,所以代理/直连之比混入了 WebSocket 客户端自身的开销;比较 WebSocket 路径时以代理 I/O 线程每帧 CPU 为准。[2026-10-22 WebSocket 合并记录](../testing/benchmarks/results/2026-10-22-websocket-coalescing.md)用它比较了逐帧消息与按 flush 合并。
`--websocket-tls 1`(需同时给 `--websocket 1`)让代理的 WebSocket 监听器提供 `wss://`:启动时用 JDK 的 `keytool` 生成一次性 ECDSA P-256 自签名证书,代理端按运行时可用的提供者(BoringSSL 可加载时为 OpenSSL,否则为 JDK)终结 TLS,客户端用 JDK `SSLSocket`。加 `-Dio.netty.handler.ssl.noOpenSsl=true` 可强制代理使用 JDK 提供者。
`--profile modded` 把等长回显换成仿大型整合包的固定种子流量:每个往返是客户端一个 tick(模组自定义负载、位置包、着地包共 3 个包),后端回以该 tick 的下行包组(平均约 10 个包、6 KB:大量 20–600 字节的模组负载与方块实体更新,约每 3 个 tick 一个 2–8 KiB 负载,约每 20 个 tick 一个 30–60 KiB 区块)。每次 Position and Look 之后后端还发送约 2 MiB 的 30,000 字节负载,模拟 Forge 注册表同步;其耗时单独报告为 `login_burst_ms`,不计入计时。每个下行包都带 tick 序号并逐字节校验;帧数与每帧 CPU 计入双向。默认客户端每个包单独写出并 flush(与已发布的客户端 mod 一样每包一条消息),`--client-coalesce 1` 改为每个 tick 一次写出。[2026-10-23 CDN 场景记录](../testing/benchmarks/results/2026-10-23-websocket-cdn.md)用这两个模式测量了 ws/wss 基线、BoringSSL 与 JDK TLS 的对比和保活 Ping 的开销。
原 stop-and-wait 小样本见 `testing/benchmarks/results/2026-09-25-proxy-session-benchmark-smoke.md`;Window 参数的小样本和 Window=1/16 重复测量见 `testing/benchmarks/results/2026-09-25-proxy-session-window.md`。这些是环回网络上的合成帧对照,不能代表 Forge 整合包、真实后端或跨主机部署的性能。
## 后端宿主真实服务端验收
`testing/smoke/backend-uranium.ps1` 将已经接受 EULA 的 Uranium 发行包中的服务端 JAR、运行库和 EULA 复制到全新隔离目录,启动两个 Java 8 后端及实际 Java 25 Proxy。它安装 `MoonBridgeBackend`、两个独立的 Bukkit 业务插件和一个外部 Proxy 验收插件,通过正式插件 API 检查注册与消息服务;不会复制或修改原有世界、模组、插件及凭据。Bukkit 业务插件以发行包 `backend/` 中的 JAR 和服务器 Bukkit API 编译(compileOnly,运行时依赖 `MoonBridgeBackend`),并在实际插件加载器下验证服务类身份一致。
```powershell
.\gradlew.bat check :proxy-core:installDist
.\smoke\backend-uranium.ps1 -BundlePath C:\path\to\accepted-uranium-bundle `
-BukkitApiJar C:\path\to\accepted-uranium-bundle\Uranium-server.jar
```
脚本默认使用本机 Zulu Java 8 和 Java 25,可通过 `-Java8Home`、`-Java25Home` 指定其他路径。完整成功输出 `BACKEND_URANIUM_PASS`;任一阶段失败会返回非零退出码并保留日志。后台进程有启动、请求、停机期限,退出时只清理本次创建的进程。
验收覆盖 Proxy→后端、后端→Proxy、后端→后端请求,UUID/replyTo、多个独立订阅者接收同一广播、Bukkit 主线程与零在线玩家、业务插件停用隔离、Proxy 重启后的自动重新注册、正常停服快速注销、异常退出后的租约清理和后端重启。具体构件哈希和运行结果见[2026-09-27 后端宿主记录](../testing/smoke/results/2026-09-27-backend-host.md)。