《MMO 服务器开发》附录 C 实测数据汇总

《MMO 服务器开发》卷一附录 C:把全卷 55 条实测数字按主题收进一张可追溯的表——并发与连接、编解码、弱网、定时器、Actor 背压、幂等、AOI 带宽、端到端承载与 Go/Java 对照。每条都标出处节号、测量条件与复现命令,并附基准三纪律与不可移植声明。

附录 C 实测数据汇总

附录 B 把「怎么把书跑起来」写成了一份清单,本附录做另一半:把卷一里跑出来的数字集中收进一张表。这些数字散落在 17 章里,各有各的口径与上下文,看单篇时容易只记住「很快」,合起来看才能读出结构——吞吐的天花板在哪、延迟和并发是什么关系、增量相比全量到底省多少。

本附录只做一件事:把每条数字的出处钉死。每一行都给出指标、数值、测量条件与出处节号,最后附一份可粘贴的复现命令表。凡是正文里没出现过的数字,本附录一个字都不补——没有出处就不收录,这是这张表唯一的纪律。所有数字来自同一台写作机、同一轮实跑,只作同机相对量级,绝对数值不可移植。

C.1 硬件与软件基线

所有实测都在下表这一套环境里跑出。换一台机器、换一个 Go 版本,任何一个数字都会变,所以先把它摆在这里,方便你对照。

项值验证命令
宿主机macOS 26.3(Darwin 25.3.0),10 核 / 32 GiBsw_vers / sysctl -n hw.ncpu
Gogo1.26.0 darwin/arm64go version
Go 模块代理https://goproxy.cn,directgo env GOPROXY
容器运行时Colima,aarch64,6 vCPU / 10 GiBdocker version
Docker client / engine29.7.2 / 29.5.2docker --version
Java(对照)Temurin 21.0.12.1 LTSdocker run --rm eclipse-temurin:21-jdk java -version
docker-compose5.5.0(独立二进制,docker compose 不可用)docker-compose version

两个内核参数直接决定了压测能不能做对,必须记牢:

参数实测值影响
kern.ipc.somaxconn128千连接压测必须错峰建连,否则超出队列的连接被直接 RST
ulimit -n / kern.maxfilesperproc1048576 / 122880文件描述符不紧张,瓶颈不在 fd
hw.ncpu10所有「本机 N 核」的结论都指这个数

注意 Go 跑在宿主机、Java 跑在 Colima 容器——两条栈的处理器数不同(宿主 10 核 vs 容器 6 vCPU),所以下面对照表里的 Go 与 Java 数字只能看方向,不能直接比绝对值。

C.2 按主题分组的实测表

下表是卷一实测数据的全集,共 55 条。列的含义:指标是量什么,数值原样照抄正文,测量条件是这组数字的前提,出处是该数字所在的小节(点开可回原文核对)。同一行的数值若含多个指标,用 / 分隔,顺序与「指标」列一致。

分组指标数值测量条件出处
并发与连接基线 8 连接吞吐4159 rt/s8 条连接,本机 10 核,单进程2.2
并发与连接2000 连接零失败conns=2000 failed=0错峰建连 5s 摊开,绕开 somaxconn=1282.2
并发与连接2000 连接 RTT P50(惊群)1.339902 s2000 条几乎同时 dial,rtt_n=100002.2
并发与连接2000 连接 RTT P50(错峰)803.416 ms错峰建连,rtt_n=100002.2
并发与连接2000 连接吞吐(惊群)1008 rt/sLittle 定律 2000 = 1008 × 1.98s2.2
并发与连接goroutine 栈占用约 2053 B/实例100000 实例存活,StackInuse +195.8 MiB2.3
并发与连接1000 空连接内存约 6.5 KB/连接HeapAlloc 增量 / 10006.2
并发与连接会话表 Bind(单锁 / 64 分片)495.2 / 102.6 ns/op1000 账号,10 核,64 B/op 1 alloc7.1
编解码帧固定头尺寸11 字节length(4)|cmd(2)|seq(4)|flags(1)3.1
编解码三种编码尺寸json=95 B flat=32 B varint=30 BTestSizes,同一结构体4.1
编解码JSON 编码 / 解码295.8 / 1137.0 ns/op-benchmem -count=54.3
编解码扁平化编码 / 解码16.7 / 14.1 ns/op解码 0 B/op 0 allocs4.3
编解码Varint 编码 / 解码45.2 / 22.0 ns/op解码 0 B/op 0 allocs4.3
可靠 UDP 与弱网KCP 报文头 / mss24 B / 488 BMTU 固定 5125.2
可靠 UDP 与弱网RTO 收敛98 → 82 ms链路真实 RTT=60ms5.2
可靠 UDP 与弱网0% 丢包 P50/P99KCP 40.0/49.0 ms;TCP 式 34.7/39.9 ms确定性弱网模拟器,300 条5.3
可靠 UDP 与弱网5% 丢包 P99KCP 155.0 ms;TCP 式 239.6 ms冗余 1.14×5.3
可靠 UDP 与弱网20% 丢包 P50/P99KCP 55.0/357.0 ms;TCP 式 86.8/359.6 ms冗余 1.49×5.3
定时器与时间轮时间轮挂载77 ns/op, 79 B, 1 allocinterval=10ms, slots=89.2
定时器与时间轮time.AfterFunc 挂载约 120 ns/op, 约 120 B, 1 alloc同机对照,N=100009.1
定时器与时间轮共享表扫描90288 ns/op(10000 条),0 B约 9 ns/条,0 allocs9.1
定时器与时间轮扫描摊销约 36 µs/s10000 连接,节拍 timeout/8=2.5s9.1
定时器与时间轮心跳超时精度配置 300ms,实测 304–305ms看门狗节拍 37.5ms6.2
Actor 与背压邮箱 FIFO 乱序数04 发送者 × 1000 条10.2
Actor 与背压背压上界Σcap + 1 = 4三段 cap=1 的 RejectBlock 链路10.2
Actor 与背压RejectDrop 丢弃数8容量 2 的邮箱收 10 条10.2
Actor 与背压-race 邮箱测试12 个测试全绿go test -race10.2
幂等幂等窗口单键内存37.8 B/键500k 活键 HeapAlloc 增量 / 键数11.1
幂等窗口活键上界100100k 键、ttl=100ms、r=1000/s → r×ttl11.1
幂等Window.Seen 探测134 ns/op, 0 allocs含一次 map 查找/插入11.1
幂等FingerprintOf(2 参数)≈34 ns/op, 0 allocshash/fnv 全在栈上不逃逸11.2
幂等Settle 命中 / 首次落 store37 / 321 ns/op首次含 map 插入,151 B/op11.3
幂等200 次重放授予次数1窗口 1ms;64 并发同指纹也为 111.3
同步与 AOI 带宽全量 / 增量快照691020 / 2716 B500 实体 × 60 tick,每 tick 2 个新实体移动12.2
同步与 AOI 带宽全量 / 增量比254.4×同一段变更序列(同机相对量级)12.2
同步与 AOI 带宽AOI 工作集15.5 MB100 万实体,格边长 32,256×256 格12.3
同步与 AOI 带宽半径 32 每客户端1245 B/s,查询 7.7–9.2 µs3×3 格,mean interest 47.912.3
同步与 AOI 带宽半径 128 每客户端 / 扇出19698 B/s / 18785 MB/s9×9 格,查询 41–65 µs12.3
路由与扇出单帧去重判定约 7.3 ns/op, 0 B, 0 allocs64 位滑动窗口8.2
路由与扇出广播编一次 / 逐条编16480 / 67489 ns/op1000 连接,编一次 16544 B 3 allocs8.3
路由与扇出广播分配次数差约 660×2001 allocs vs 3 allocs8.3
端到端承载单世界吞吐天花板约 3000–3200 QPS50/100 连接持续负载,瓶颈是世界 goroutine17.3
端到端承载50 连接 × 400 请求3223 QPS,P50 13.0 / P99 88.2 ms失败 017.3
端到端承载100 连接 × 200 请求3008–3160 QPS,P50 26.1 / P99 124–171 ms失败 017.3
端到端承载500 连接 × 20 请求1693 QPS,P50 55.6 / P99 477.1 ms失败 017.3
端到端承载1000 连接 × 10 请求793 QPS,P50 283.8 / P99 1642.1 ms失败 117.3
端到端承载冒烟 200 连接 × 10 请求qps≈1001,p50 0.60 / p99 5.33 ms附录 B 复现序列附录 B
防护fuzz 5 秒执行次数约 150 万次-fuzztime=5s,10 个 worker16.1
防护AES-256-GCM 封解往返620 ns/op, 565 B/op, 5 allocs256 字节明文,-benchmem16.3
防护socket write 底噪约 80 µs/次单连接校准6.2
防护每 IP 上限 / 握手令牌桶4 条接纳;突发 3 后拒绝,100ms 恢复 120 条 loopback;Burst=3 Refill=100ms16.2
Go vs Java 对照goroutine 创建 / 栈485 ns/实例 / 约 2053 BGo 1.26,n=1000002.3
Go vs Java 对照虚拟线程创建 / 内存0.841 µs/实例 / 约 1006 BJVM 21.0.12.1,n=1000002.3
Go vs Java 对照平台线程创建 / 内存3.603 µs/实例 / 约 479 BJVM 21.0.12.1,n=100002.3
Go vs Java 对照Java 可用处理器 / 最大堆6 / 2482 MiBColima 6 vCPU 容器内 JVM 默认2.3

C.3 复现方法与可追溯校验

复现要按主题分组跑,每条命令都从对应章的 module 根执行。下表给出「主题 → 命令」的对照,直接粘贴即可:

主题复现命令
并发与连接go test ./internal/netio/ -bench . -benchmem
编解码go test ./internal/codec/ -bench . -benchmem -count=5 -run '^$'
可靠 UDP 与弱网go test -run TestLossyBenchmark -v ./internal/kcp/
定时器与时间轮go test ./internal/timing/ -bench . -benchmem
Actor 与背压go test -race ./internal/actor/
幂等go test ./internal/idem/ -bench . -benchmem
同步与 AOI 带宽go test ./internal/sync/ -run TestFanoutReport -v -count=1
路由与扇出go test ./internal/router/ -run '^$' -bench . -benchmem -count=3
端到端承载./bin/bot -mode=bench -addr=127.0.0.1:19002 -conns=200 -reqs=10 -stagger=2s -out=bench.json
防护go test -run=^$ -fuzz=FuzzDecodeFrom -fuzztime=5s ./internal/proto/
Go vs Java 对照go test ./internal/netio/ -bench BenchmarkSpawn -benchmem(Java 侧见附录 B 的 Docker 命令)

数字再多,也要能被程序核对——否则「这张表可靠」只是一句自我感觉良好的话。下面这段是 L1,go test 可以直接跑:它把上面的表编码成 Go 结构,断言每条都有出处节号,并回到落盘的正文里核对节号真实存在。

// file: measurements/appendixc_test.go
// tier: L1
// verify: go test ./measurements/
package measurements

import (
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
)

// Measurement 是附录 C 的一条实测数据。每一条都必须带出处节号 Source,
// 附录里不允许出现「无出处」的数字。
type Measurement struct {
	Group     string // 主题分组
	Metric    string // 指标名
	Value     string // 数值(含单位,照抄正文)
	Condition string // 测量条件
	Source    string // 出处节号,必须是真实存在的 mmo-net-* slug
}

// Table 是附录 C 的机器可读副本,顺序即正文表格顺序。
var Table = []Measurement{
	// —— 并发与连接 ——
	{"并发与连接", "基线 8 连接吞吐", "4159 rt/s", "8 条连接,本机 10 核,单进程", "mmo-net-02-2-goroutine-per-connection"},
	{"并发与连接", "2000 连接零失败", "conns=2000 failed=0", "错峰建连 5s 摊开,绕开 somaxconn=128", "mmo-net-02-2-goroutine-per-connection"},
	{"并发与连接", "2000 连接 RTT P50(惊群)", "1.339902 s", "2000 条几乎同时 dial,rtt_n=10000", "mmo-net-02-2-goroutine-per-connection"},
	{"并发与连接", "2000 连接 RTT P50(错峰)", "803.416 ms", "错峰建连,rtt_n=10000", "mmo-net-02-2-goroutine-per-connection"},
	{"并发与连接", "2000 连接吞吐(惊群)", "1008 rt/s", "Little 定律 2000 = 1008 × 1.98s", "mmo-net-02-2-goroutine-per-connection"},
	{"并发与连接", "goroutine 栈占用", "约 2053 B/实例", "100000 实例存活,StackInuse +195.8 MiB", "mmo-net-02-3-goroutine-vs-virtual-threads"},
	{"并发与连接", "1000 空连接内存", "约 6.5 KB/连接", "HeapAlloc 增量 / 1000", "mmo-net-06-2-heartbeat-and-timeout"},
	{"并发与连接", "会话表 Bind(单锁 / 64 分片)", "495.2 / 102.6 ns/op", "1000 账号,10 核,64 B/op 1 alloc", "mmo-net-07-1-session-table"},
	// —— 编解码 ——
	{"编解码", "帧固定头尺寸", "11 字节", "length(4)|cmd(2)|seq(4)|flags(1)", "mmo-net-03-1-message-header-and-cmd"},
	{"编解码", "三种编码尺寸", "json=95 B flat=32 B varint=30 B", "TestSizes,同一结构体", "mmo-net-04-1-binary-codec-from-scratch"},
	{"编解码", "JSON 编码 / 解码", "295.8 / 1137.0 ns/op", "-benchmem -count=5", "mmo-net-04-3-codec-benchmark"},
	{"编解码", "扁平化编码 / 解码", "16.7 / 14.1 ns/op", "解码 0 B/op 0 allocs", "mmo-net-04-3-codec-benchmark"},
	{"编解码", "Varint 编码 / 解码", "45.2 / 22.0 ns/op", "解码 0 B/op 0 allocs", "mmo-net-04-3-codec-benchmark"},
	// —— 可靠 UDP 与弱网 ——
	{"可靠 UDP 与弱网", "KCP 报文头 / mss", "24 B / 488 B", "MTU 固定 512", "mmo-net-05-2-kcp-from-scratch"},
	{"可靠 UDP 与弱网", "RTO 收敛", "98 → 82 ms", "链路真实 RTT=60ms", "mmo-net-05-2-kcp-from-scratch"},
	{"可靠 UDP 与弱网", "0% 丢包 P50/P99", "KCP 40.0/49.0 ms;TCP 式 34.7/39.9 ms", "确定性弱网模拟器,300 条", "mmo-net-05-3-lossy-network-benchmark"},
	{"可靠 UDP 与弱网", "5% 丢包 P99", "KCP 155.0 ms;TCP 式 239.6 ms", "冗余 1.14×", "mmo-net-05-3-lossy-network-benchmark"},
	{"可靠 UDP 与弱网", "20% 丢包 P50/P99", "KCP 55.0/357.0 ms;TCP 式 86.8/359.6 ms", "冗余 1.49×", "mmo-net-05-3-lossy-network-benchmark"},
	// —— 定时器与时间轮 ——
	{"定时器与时间轮", "时间轮挂载", "77 ns/op, 79 B, 1 alloc", "interval=10ms, slots=8", "mmo-net-09-2-time-wheel-from-scratch"},
	{"定时器与时间轮", "time.AfterFunc 挂载", "约 120 ns/op, 约 120 B, 1 alloc", "同机对照,N=10000", "mmo-net-09-1-heartbeat-and-cooldown"},
	{"定时器与时间轮", "共享表扫描", "90288 ns/op(10000 条),0 B", "约 9 ns/条,0 allocs", "mmo-net-09-1-heartbeat-and-cooldown"},
	{"定时器与时间轮", "扫描摊销", "约 36 µs/s", "10000 连接,节拍 timeout/8=2.5s", "mmo-net-09-1-heartbeat-and-cooldown"},
	{"定时器与时间轮", "心跳超时精度", "配置 300ms,实测 304–305ms", "看门狗节拍 37.5ms", "mmo-net-06-2-heartbeat-and-timeout"},
	// —— Actor 与背压 ——
	{"Actor 与背压", "邮箱 FIFO 乱序数", "0", "4 发送者 × 1000 条", "mmo-net-10-2-mailbox-and-backpressure"},
	{"Actor 与背压", "背压上界", "Σcap + 1 = 4", "三段 cap=1 的 RejectBlock 链路", "mmo-net-10-2-mailbox-and-backpressure"},
	{"Actor 与背压", "RejectDrop 丢弃数", "8", "容量 2 的邮箱收 10 条", "mmo-net-10-2-mailbox-and-backpressure"},
	{"Actor 与背压", "-race 邮箱测试", "12 个测试全绿", "go test -race", "mmo-net-10-2-mailbox-and-backpressure"},
	// —— 幂等 ——
	{"幂等", "幂等窗口单键内存", "37.8 B/键", "500k 活键 HeapAlloc 增量 / 键数", "mmo-net-11-1-idempotency-window"},
	{"幂等", "窗口活键上界", "100", "100k 键、ttl=100ms、r=1000/s → r×ttl", "mmo-net-11-1-idempotency-window"},
	{"幂等", "Window.Seen 探测", "134 ns/op, 0 allocs", "含一次 map 查找/插入", "mmo-net-11-1-idempotency-window"},
	{"幂等", "FingerprintOf(2 参数)", "≈34 ns/op, 0 allocs", "hash/fnv 全在栈上不逃逸", "mmo-net-11-2-command-id-and-fingerprint"},
	{"幂等", "Settle 命中 / 首次落 store", "37 / 321 ns/op", "首次含 map 插入,151 B/op", "mmo-net-11-3-at-least-once-correctness"},
	{"幂等", "200 次重放授予次数", "1", "窗口 1ms;64 并发同指纹也为 1", "mmo-net-11-3-at-least-once-correctness"},
	// —— 同步与 AOI 带宽 ——
	{"同步与 AOI 带宽", "全量 / 增量快照", "691020 / 2716 B", "500 实体 × 60 tick,每 tick 2 个新实体移动", "mmo-net-12-2-snapshot-and-delta"},
	{"同步与 AOI 带宽", "全量 / 增量比", "254.4×", "同一段变更序列(同机相对量级)", "mmo-net-12-2-snapshot-and-delta"},
	{"同步与 AOI 带宽", "AOI 工作集", "15.5 MB", "100 万实体,格边长 32,256×256 格", "mmo-net-12-3-interest-filtering-bandwidth"},
	{"同步与 AOI 带宽", "半径 32 每客户端", "1245 B/s,查询 7.7–9.2 µs", "3×3 格,mean interest 47.9", "mmo-net-12-3-interest-filtering-bandwidth"},
	{"同步与 AOI 带宽", "半径 128 每客户端 / 扇出", "19698 B/s / 18785 MB/s", "9×9 格,查询 41–65 µs", "mmo-net-12-3-interest-filtering-bandwidth"},
	// —— 路由与扇出 ——
	{"路由与扇出", "单帧去重判定", "约 7.3 ns/op, 0 B, 0 allocs", "64 位滑动窗口", "mmo-net-08-2-sequence-and-ordering"},
	{"路由与扇出", "广播编一次 / 逐条编", "16480 / 67489 ns/op", "1000 连接,编一次 16544 B 3 allocs", "mmo-net-08-3-broadcast-and-fanout"},
	{"路由与扇出", "广播分配次数差", "约 660×", "2001 allocs vs 3 allocs", "mmo-net-08-3-broadcast-and-fanout"},
	// —— 端到端承载 ——
	{"端到端承载", "单世界吞吐天花板", "约 3000–3200 QPS", "50/100 连接持续负载,瓶颈是世界 goroutine", "mmo-net-17-3-bot-and-first-benchmark"},
	{"端到端承载", "50 连接 × 400 请求", "3223 QPS,P50 13.0 / P99 88.2 ms", "失败 0", "mmo-net-17-3-bot-and-first-benchmark"},
	{"端到端承载", "100 连接 × 200 请求", "3008–3160 QPS,P50 26.1 / P99 124–171 ms", "失败 0", "mmo-net-17-3-bot-and-first-benchmark"},
	{"端到端承载", "500 连接 × 20 请求", "1693 QPS,P50 55.6 / P99 477.1 ms", "失败 0", "mmo-net-17-3-bot-and-first-benchmark"},
	{"端到端承载", "1000 连接 × 10 请求", "793 QPS,P50 283.8 / P99 1642.1 ms", "失败 1", "mmo-net-17-3-bot-and-first-benchmark"},
	{"端到端承载", "冒烟 200 连接 × 10 请求", "qps≈1001,p50 0.60 / p99 5.33 ms", "附录 B 复现序列", "mmo-net-appendix-b-environment"},
	// —— 防护 ——
	{"防护", "fuzz 5 秒执行次数", "约 150 万次", "-fuzztime=5s,10 个 worker", "mmo-net-16-1-protocol-fuzz"},
	{"防护", "AES-256-GCM 封解往返", "620 ns/op, 565 B/op, 5 allocs", "256 字节明文,-benchmem", "mmo-net-16-3-packet-encryption-signature"},
	{"防护", "socket write 底噪", "约 80 µs/次", "单连接校准", "mmo-net-06-2-heartbeat-and-timeout"},
	{"防护", "每 IP 上限 / 握手令牌桶", "4 条接纳;突发 3 后拒绝,100ms 恢复 1", "20 条 loopback;Burst=3 Refill=100ms", "mmo-net-16-2-flood-and-conn-exhaustion"},
	// —— Go vs Java 对照 ——
	{"Go vs Java 对照", "goroutine 创建 / 栈", "485 ns/实例 / 约 2053 B", "Go 1.26,n=100000", "mmo-net-02-3-goroutine-vs-virtual-threads"},
	{"Go vs Java 对照", "虚拟线程创建 / 内存", "0.841 µs/实例 / 约 1006 B", "JVM 21.0.12.1,n=100000", "mmo-net-02-3-goroutine-vs-virtual-threads"},
	{"Go vs Java 对照", "平台线程创建 / 内存", "3.603 µs/实例 / 约 479 B", "JVM 21.0.12.1,n=10000", "mmo-net-02-3-goroutine-vs-virtual-threads"},
	{"Go vs Java 对照", "Java 可用处理器 / 最大堆", "6 / 2482 MiB", "Colima 6 vCPU 容器内 JVM 默认", "mmo-net-02-3-goroutine-vs-virtual-threads"},
}

// TestEveryEntryIsComplete 钉死「附录里没有无出处的数字」,并核对分组覆盖。
func TestEveryEntryIsComplete(t *testing.T) {
	if len(Table) == 0 {
		t.Fatal("Table 为空")
	}
	seen := map[string]int{}
	for i, m := range Table {
		if strings.TrimSpace(m.Group) == "" || strings.TrimSpace(m.Metric) == "" ||
			strings.TrimSpace(m.Value) == "" || strings.TrimSpace(m.Condition) == "" ||
			strings.TrimSpace(m.Source) == "" {
			t.Fatalf("第 %d 条字段缺失: %+v", i, m)
		}
		if !strings.HasPrefix(m.Source, "mmo-net-") {
			t.Fatalf("第 %d 条出处 %q 不是本卷 slug", i, m.Source)
		}
		seen[m.Group]++
	}
	for _, g := range []string{"并发与连接", "编解码", "可靠 UDP 与弱网", "定时器与时间轮",
		"Actor 与背压", "幂等", "同步与 AOI 带宽", "路由与扇出", "端到端承载", "防护", "Go vs Java 对照"} {
		if seen[g] == 0 {
			t.Fatalf("分组 %q 没有任何条目", g)
		}
	}
}

var slugRe = regexp.MustCompile(`(?m)^slug:\s*"?([A-Za-z0-9\-]+)"?\s*$`)

// loadRealSlugs 从落盘正文收集真实存在的 slug;目录不可达就跳过,
// 让测试在本机有意义、在别处也不假失败。
func loadRealSlugs(t *testing.T) map[string]bool {
	t.Helper()
	dir := os.Getenv("AEGIS_CONTENT_DIR")
	if dir == "" {
		dir = "/Users/bingrong.yan/projects/birdor/plumephp/content/posts/books/网络与并发内核"
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Skipf("语料目录不可达,跳过存在性核对: %v", err)
	}
	slugs := map[string]bool{}
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
			continue
		}
		b, err := os.ReadFile(filepath.Join(dir, e.Name()))
		if err != nil {
			t.Fatalf("读取 %s 失败: %v", e.Name(), err)
		}
		if mm := slugRe.FindSubmatch(b); mm != nil {
			slugs[string(mm[1])] = true
		}
	}
	return slugs
}

// TestEverySourceExists 是本附录最硬的一条:每条数据的出处节号都必须在
// 落盘正文里找到对应 slug,写错的节号会在这里被抓出来。
func TestEverySourceExists(t *testing.T) {
	real := loadRealSlugs(t)
	src := map[string]int{}
	for i, m := range Table {
		if !real[m.Source] {
			t.Fatalf("第 %d 条出处 %q 在正文里不存在", i, m.Source)
		}
		src[m.Source]++
	}
	if len(src) < 15 {
		t.Fatalf("出处只覆盖 %d 个节,疑似漏收", len(src))
	}
	t.Logf("收录 %d 条,覆盖 %d 个真实出处节", len(Table), len(src))
}

本机实测:go vet ./... 干净,go test ./measurements/ 全绿,TestEverySourceExists 报告「收录 55 条,覆盖 24 个真实出处节」。这行日志就是本附录的验收凭证——只要有任何一条的出处节号被写错或指向不存在的节,测试立刻红。

C.4 三条纪律与免责

读这张表之前,先记住基准的三条纪律,它们解释了为什么有些数字看着「不够漂亮」:

  1. 进程隔离:基准必须跑在自己独占的进程里。同机还跑着 hugo、浏览器、别的编译时,内存带宽和缓存被抢,数字会飘。
  2. 预热到稳定:第一次查询要冷启动缓存、要触发页错误,必须丢弃。12.3 的每种半径先空跑 2000 次才开始计数。
  3. 工作集超过缓存:实体数组小到能塞进 L2,测出来的是「缓存命中」的理想值,搬到线上就崩。12.3 刻意用 15.5 MB 工作集越过本机 L2。

免责声明同样重要,缺一条都会把结论用歪:

  • 同机相对量级,绝对数值不可移植。 换 CPU、换 Go 版本、把工作集缩到缓存内,任何一个数字都会变。这张表能告诉你「半径和成本是平方关系」「延迟约等于并发除以吞吐」,不能告诉你「你的服务器能扛多少客户端」。
  • 跨栈不可直接比较。 Go 数字来自宿主机(10 核),Java 数字来自 Colima 容器(6 vCPU)。2.3 的对照只能读「虚拟线程比平台线程轻」这个方向,不能把 485 ns 和 0.841 µs 当成同一把尺子上的读数。
  • 禁止「快 N 倍」式绝对话。 9.2 的时间轮挂载只比 time.AfterFunc 快约 1.5 倍,8.3 的「编一次」相对「编 N 次」也只是一个相对关系;这些数字的意义是「方向与结构」,不是「加速比承诺」。
  • L4 类数字不可复现。 16.2 的每 IP 配额、16.1 的 fuzz 次数都在本机实测;真实 DDoS 与公网洪泛属于不可复现环境,本附录不收录任何未实跑的公网数字。

本附录小结:55 条数字,24 个出处节,每一条都能在正文里找到原文与测量条件,也能用 C.3 的命令重新跑出来。卷一至此结束;回到 18.1 卷一回顾 可以看这些零件如何按依赖排成一条线,或者从 附录 A 查 aegis 的模块接口。

阅读上一节:附录 B 环境与复现速查 · 阅读下一节:附录 D 常见错误与 FAQ

继续阅读

探索更多技术文章

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

全部文章 返回首页

「mmo」更多文章

  1. 《MMO 服务器开发》工程化与运营 · 目录
  2. 《MMO 服务器开发》9.3 上线检查清单
  3. 《MMO 服务器开发》9.2 性能预算达标