在 Go 1.27.1(macOS,kqueue)里验证网络轮询器:2000 条 TCP 连接各有一个 goroutine 阻塞在 Read 上,线程数只从 5 增加到 12,goroutine 状态是 [IO wait],进程里只有一个 KQUEUE 描述符;同样 200 个阻塞读,走 os.Pipe 只多 1 个线程,直接 syscall.Read 阻塞描述符多 198 个线程([syscall] 状态);读超时由轮询器的定时器实现。
在 Go 1.27.1 里测系统调用阻塞时 P 的移交:只有 1 个 P 时,一个 goroutine 阻塞在 syscall.Syscall6 的系统调用里(哪怕长达 100 ms),另一个 goroutine 只等 13–65 µs 就拿到 P;改用不通知运行时的 RawSyscall6,另一个 goroutine 要等到系统调用结束;有空闲 P 时等待更短(4–36 µs);并摘录 sysmon 的 20 µs 起步、至多 10 ms 的睡眠间隔与 retake 的条件。
在 Go 1.27.1(arm64 macOS)里验证异步抢占:只有 1 个 P、一个没有函数调用的死循环 goroutine 占着它时,main 的 time.Sleep(10ms) 实际 25–30 ms 醒来;GOMAXPROCS=2 时 runtime.GC() 只要 0.2 ms;而 GODEBUG=asyncpreemptoff=1 下这些程序都不结束。并看汇编(循环里 0 个 CALL)和运行时源码里关于安全点与 SIGURG 的说明。
在 Docker(Linux 虚拟机、cgroup v2)里验证 Go 1.27.1 的默认 GOMAXPROCS:按 --cpus 取值(2.5 → 3,1 或 0.5 → 2),go.mod 写 go 1.24 时仍是 10,运行中改限制约 1 秒后跟随,并在 --cpus=1 下对比默认值 2 与强制 10 的 CPU 限流时间(约 3 秒 vs 约 27 秒)。
在 Go 1.27.1(arm64 macOS)里剖开 time.Timer:NewTimer 每次 3 次分配、约 248 字节,AfterFunc 1 次分配、约 139 字节;通道 cap/len 恒为 0,到期未读时 Reset/Stop 返回 true 且没有旧值;Sleep 与 Timer 的唤醒误差(100µs 中位约 30 µs,1ms 约 260 µs,10ms 约 2 ms);并读源码里每个 P 一个定时器堆、通道型定时器有人阻塞才入堆的判断。
在 Go 1.27.1 里验证定时器泄漏:丢掉不 Stop 的 NewTicker、time.Tick、NewTimer 和循环里的 time.After,GC 后堆都不增长,而保留引用的 10 万个 Ticker 占 25.8 MB;旧的 asynctimerchan 设置在 1.27 已被删除,无法复现 1.23 之前的泄漏;time.After 每轮 3 次分配 145 ns,复用 Timer 0 次分配 83 ns;慢接收方会丢 tick。
在 Go 1.27.1 里观察后台监控线程 sysmon 的工作:schedtrace 下空闲与空转的调度器状态对比;完全空闲 3.5 秒 CPU 时间 0.000 秒;空闲进程只有在已发生过一次 GC 之后才会在 2 分钟时被强制 GC(@120.014s),从没 GC 过的进程 130 秒 NumGC 仍是 0;并读 sysmon 源码里的五件事:轮询网络、更新 GOMAXPROCS、叫醒 scavenger、retake、强制 GC。
在 Go 1.27.1 里实测 runtime/metrics:共 107 个指标(75 个累计型);分配字节数、GC 次数、Sys 与 MemStats 逐字节一致,goroutine 数 66 对 NumGoroutine 的 51(指标包含系统 goroutine);读 1 个指标 188 ns,读全部 1934 ns,ReadMemStats 22760 ns;读不存在的指标名返回 KindBad;并读 /sched/latencies 直方图。
在 Go 1.27.1(arm64 macOS)里实测 CPU profile:真实占比 60.1/30.0/10.0 的三个函数,profile 给出 58.07/29.98/9.85;4 个 goroutine 并行时 Total samples 是墙钟的 276.62%;Sleep 的睡眠时间不在 profile 里,但 Sleep 路径上的 kevent 占了 65.47%;窗口 100 ms 时 10% 的函数估计在 0.0%–10.0% 间摆动,4 秒时只在 9.5%–11.2% 间;并读了 100 Hz 采样率的源码注释。
在 Go 1.27.1 里实测 heap profile:同一份 profile 里,丢弃的 98 MB 垃圾只在 alloc_space 里出现、不在 inuse_space 前列;刚分配的内存不GC就看不全(20133 kB 对 54477 kB);采样率从默认 512 KiB 提高到 4096 字节,小对象估计的波动从 12394–18538 kB 缩到 13936–14464 kB,而 MemProfileRate=1 让每次 64 字节分配从 10 ns 涨到 512 ns。
在 Go 1.27.1 里实测 block 与 mutex profile:8 个 goroutine 争一把锁,block profile 把 1363.67 ms 的等待记在 sync.(*Mutex).Lock 上,mutex profile 把 1363.74 ms 记在 sync.(*Mutex).Unlock 上;channel 等待只出现在 block profile;采样 1/100 时 mutex 读数在 0–1761 ms 之间乱跳;打开 profile 对无竞争加锁没有可测的代价。
在 Go 1.27.1 里实测执行追踪:go tool trace -pprof=sync 把 4 个 goroutine 争锁的等待(Mutex.Lock 314.33 ms)归到 main.holdLock;-pprof=sched 的调度延迟总量(804–1906 ms)明显低于程序直接量到的排队时间 4690 ms;用户标注的区域时长 12.03/12.03/12.01 ms;开 trace 让一个分配密集的程序慢约 22%、每秒约 18 MB。