附录 C 实测数据汇总
附录 B 把「怎么把书跑起来」写成了一份清单,本附录做另一半:把卷一里跑出来的数字集中收进一张表。这些数字散落在 17 章里,各有各的口径与上下文,看单篇时容易只记住「很快」,合起来看才能读出结构——吞吐的天花板在哪、延迟和并发是什么关系、增量相比全量到底省多少。
本附录只做一件事:把每条数字的出处钉死。每一行都给出指标、数值、测量条件与出处节号,最后附一份可粘贴的复现命令表。凡是正文里没出现过的数字,本附录一个字都不补——没有出处就不收录,这是这张表唯一的纪律。所有数字来自同一台写作机、同一轮实跑,只作同机相对量级,绝对数值不可移植。
C.1 硬件与软件基线
所有实测都在下表这一套环境里跑出。换一台机器、换一个 Go 版本,任何一个数字都会变,所以先把它摆在这里,方便你对照。
| 项 | 值 | 验证命令 |
|---|---|---|
| 宿主机 | macOS 26.3(Darwin 25.3.0),10 核 / 32 GiB | sw_vers / sysctl -n hw.ncpu |
| Go | go1.26.0 darwin/arm64 | go version |
| Go 模块代理 | https://goproxy.cn,direct | go env GOPROXY |
| 容器运行时 | Colima,aarch64,6 vCPU / 10 GiB | docker version |
| Docker client / engine | 29.7.2 / 29.5.2 | docker --version |
| Java(对照) | Temurin 21.0.12.1 LTS | docker run --rm eclipse-temurin:21-jdk java -version |
docker-compose | 5.5.0(独立二进制,docker compose 不可用) | docker-compose version |
两个内核参数直接决定了压测能不能做对,必须记牢:
| 参数 | 实测值 | 影响 |
|---|---|---|
kern.ipc.somaxconn | 128 | 千连接压测必须错峰建连,否则超出队列的连接被直接 RST |
ulimit -n / kern.maxfilesperproc | 1048576 / 122880 | 文件描述符不紧张,瓶颈不在 fd |
hw.ncpu | 10 | 所有「本机 N 核」的结论都指这个数 |
注意 Go 跑在宿主机、Java 跑在 Colima 容器——两条栈的处理器数不同(宿主 10 核 vs 容器 6 vCPU),所以下面对照表里的 Go 与 Java 数字只能看方向,不能直接比绝对值。
C.2 按主题分组的实测表
下表是卷一实测数据的全集,共 55 条。列的含义:指标是量什么,数值原样照抄正文,测量条件是这组数字的前提,出处是该数字所在的小节(点开可回原文核对)。同一行的数值若含多个指标,用 / 分隔,顺序与「指标」列一致。
| 分组 | 指标 | 数值 | 测量条件 | 出处 |
|---|---|---|---|---|
| 并发与连接 | 基线 8 连接吞吐 | 4159 rt/s | 8 条连接,本机 10 核,单进程 | 2.2 |
| 并发与连接 | 2000 连接零失败 | conns=2000 failed=0 | 错峰建连 5s 摊开,绕开 somaxconn=128 | 2.2 |
| 并发与连接 | 2000 连接 RTT P50(惊群) | 1.339902 s | 2000 条几乎同时 dial,rtt_n=10000 | 2.2 |
| 并发与连接 | 2000 连接 RTT P50(错峰) | 803.416 ms | 错峰建连,rtt_n=10000 | 2.2 |
| 并发与连接 | 2000 连接吞吐(惊群) | 1008 rt/s | Little 定律 2000 = 1008 × 1.98s | 2.2 |
| 并发与连接 | goroutine 栈占用 | 约 2053 B/实例 | 100000 实例存活,StackInuse +195.8 MiB | 2.3 |
| 并发与连接 | 1000 空连接内存 | 约 6.5 KB/连接 | HeapAlloc 增量 / 1000 | 6.2 |
| 并发与连接 | 会话表 Bind(单锁 / 64 分片) | 495.2 / 102.6 ns/op | 1000 账号,10 核,64 B/op 1 alloc | 7.1 |
| 编解码 | 帧固定头尺寸 | 11 字节 | length(4)|cmd(2)|seq(4)|flags(1) | 3.1 |
| 编解码 | 三种编码尺寸 | json=95 B flat=32 B varint=30 B | TestSizes,同一结构体 | 4.1 |
| 编解码 | JSON 编码 / 解码 | 295.8 / 1137.0 ns/op | -benchmem -count=5 | 4.3 |
| 编解码 | 扁平化编码 / 解码 | 16.7 / 14.1 ns/op | 解码 0 B/op 0 allocs | 4.3 |
| 编解码 | Varint 编码 / 解码 | 45.2 / 22.0 ns/op | 解码 0 B/op 0 allocs | 4.3 |
| 可靠 UDP 与弱网 | KCP 报文头 / mss | 24 B / 488 B | MTU 固定 512 | 5.2 |
| 可靠 UDP 与弱网 | RTO 收敛 | 98 → 82 ms | 链路真实 RTT=60ms | 5.2 |
| 可靠 UDP 与弱网 | 0% 丢包 P50/P99 | KCP 40.0/49.0 ms;TCP 式 34.7/39.9 ms | 确定性弱网模拟器,300 条 | 5.3 |
| 可靠 UDP 与弱网 | 5% 丢包 P99 | KCP 155.0 ms;TCP 式 239.6 ms | 冗余 1.14× | 5.3 |
| 可靠 UDP 与弱网 | 20% 丢包 P50/P99 | KCP 55.0/357.0 ms;TCP 式 86.8/359.6 ms | 冗余 1.49× | 5.3 |
| 定时器与时间轮 | 时间轮挂载 | 77 ns/op, 79 B, 1 alloc | interval=10ms, slots=8 | 9.2 |
| 定时器与时间轮 | time.AfterFunc 挂载 | 约 120 ns/op, 约 120 B, 1 alloc | 同机对照,N=10000 | 9.1 |
| 定时器与时间轮 | 共享表扫描 | 90288 ns/op(10000 条),0 B | 约 9 ns/条,0 allocs | 9.1 |
| 定时器与时间轮 | 扫描摊销 | 约 36 µs/s | 10000 连接,节拍 timeout/8=2.5s | 9.1 |
| 定时器与时间轮 | 心跳超时精度 | 配置 300ms,实测 304–305ms | 看门狗节拍 37.5ms | 6.2 |
| Actor 与背压 | 邮箱 FIFO 乱序数 | 0 | 4 发送者 × 1000 条 | 10.2 |
| Actor 与背压 | 背压上界 | Σcap + 1 = 4 | 三段 cap=1 的 RejectBlock 链路 | 10.2 |
| Actor 与背压 | RejectDrop 丢弃数 | 8 | 容量 2 的邮箱收 10 条 | 10.2 |
| Actor 与背压 | -race 邮箱测试 | 12 个测试全绿 | go test -race | 10.2 |
| 幂等 | 幂等窗口单键内存 | 37.8 B/键 | 500k 活键 HeapAlloc 增量 / 键数 | 11.1 |
| 幂等 | 窗口活键上界 | 100 | 100k 键、ttl=100ms、r=1000/s → r×ttl | 11.1 |
| 幂等 | Window.Seen 探测 | 134 ns/op, 0 allocs | 含一次 map 查找/插入 | 11.1 |
| 幂等 | FingerprintOf(2 参数) | ≈34 ns/op, 0 allocs | hash/fnv 全在栈上不逃逸 | 11.2 |
| 幂等 | Settle 命中 / 首次落 store | 37 / 321 ns/op | 首次含 map 插入,151 B/op | 11.3 |
| 幂等 | 200 次重放授予次数 | 1 | 窗口 1ms;64 并发同指纹也为 1 | 11.3 |
| 同步与 AOI 带宽 | 全量 / 增量快照 | 691020 / 2716 B | 500 实体 × 60 tick,每 tick 2 个新实体移动 | 12.2 |
| 同步与 AOI 带宽 | 全量 / 增量比 | 254.4× | 同一段变更序列(同机相对量级) | 12.2 |
| 同步与 AOI 带宽 | AOI 工作集 | 15.5 MB | 100 万实体,格边长 32,256×256 格 | 12.3 |
| 同步与 AOI 带宽 | 半径 32 每客户端 | 1245 B/s,查询 7.7–9.2 µs | 3×3 格,mean interest 47.9 | 12.3 |
| 同步与 AOI 带宽 | 半径 128 每客户端 / 扇出 | 19698 B/s / 18785 MB/s | 9×9 格,查询 41–65 µs | 12.3 |
| 路由与扇出 | 单帧去重判定 | 约 7.3 ns/op, 0 B, 0 allocs | 64 位滑动窗口 | 8.2 |
| 路由与扇出 | 广播编一次 / 逐条编 | 16480 / 67489 ns/op | 1000 连接,编一次 16544 B 3 allocs | 8.3 |
| 路由与扇出 | 广播分配次数差 | 约 660× | 2001 allocs vs 3 allocs | 8.3 |
| 端到端承载 | 单世界吞吐天花板 | 约 3000–3200 QPS | 50/100 连接持续负载,瓶颈是世界 goroutine | 17.3 |
| 端到端承载 | 50 连接 × 400 请求 | 3223 QPS,P50 13.0 / P99 88.2 ms | 失败 0 | 17.3 |
| 端到端承载 | 100 连接 × 200 请求 | 3008–3160 QPS,P50 26.1 / P99 124–171 ms | 失败 0 | 17.3 |
| 端到端承载 | 500 连接 × 20 请求 | 1693 QPS,P50 55.6 / P99 477.1 ms | 失败 0 | 17.3 |
| 端到端承载 | 1000 连接 × 10 请求 | 793 QPS,P50 283.8 / P99 1642.1 ms | 失败 1 | 17.3 |
| 端到端承载 | 冒烟 200 连接 × 10 请求 | qps≈1001,p50 0.60 / p99 5.33 ms | 附录 B 复现序列 | 附录 B |
| 防护 | fuzz 5 秒执行次数 | 约 150 万次 | -fuzztime=5s,10 个 worker | 16.1 |
| 防护 | AES-256-GCM 封解往返 | 620 ns/op, 565 B/op, 5 allocs | 256 字节明文,-benchmem | 16.3 |
| 防护 | socket write 底噪 | 约 80 µs/次 | 单连接校准 | 6.2 |
| 防护 | 每 IP 上限 / 握手令牌桶 | 4 条接纳;突发 3 后拒绝,100ms 恢复 1 | 20 条 loopback;Burst=3 Refill=100ms | 16.2 |
| Go vs Java 对照 | goroutine 创建 / 栈 | 485 ns/实例 / 约 2053 B | Go 1.26,n=100000 | 2.3 |
| Go vs Java 对照 | 虚拟线程创建 / 内存 | 0.841 µs/实例 / 约 1006 B | JVM 21.0.12.1,n=100000 | 2.3 |
| Go vs Java 对照 | 平台线程创建 / 内存 | 3.603 µs/实例 / 约 479 B | JVM 21.0.12.1,n=10000 | 2.3 |
| Go vs Java 对照 | Java 可用处理器 / 最大堆 | 6 / 2482 MiB | Colima 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 三条纪律与免责
读这张表之前,先记住基准的三条纪律,它们解释了为什么有些数字看着「不够漂亮」:
- 进程隔离:基准必须跑在自己独占的进程里。同机还跑着 hugo、浏览器、别的编译时,内存带宽和缓存被抢,数字会飘。
- 预热到稳定:第一次查询要冷启动缓存、要触发页错误,必须丢弃。12.3 的每种半径先空跑 2000 次才开始计数。
- 工作集超过缓存:实体数组小到能塞进 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
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。