禁用nagle、用bufio.writer聚合并手动flush、避免高频分配、必要时用单goroutine写队列;因tcp栈对小包有天然惩罚,直接write会导致系统调用频繁、延迟高、吞吐骤降。

为什么 net.Conn.Write 直接发小包在微服务里很慢
高频小包(比如每毫秒一条心跳或指标上报)直接调用 net.Conn.Write,会频繁触发系统调用、陷入内核、触发 Nagle 算法延迟合并,实际吞吐可能掉到理论值的 1/10。这不是 Go 实现的问题,是 TCP 栈层面对小包的天然惩罚。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 禁用 Nagle:连接建立后立刻调用
conn.SetNoDelay(true),避免默认的 200ms 合并等待 - 别依赖单次
Write原子性:TCP 不保证“一次 Write 对应一个 TCP 包”,尤其在高并发写时,Write可能阻塞、拆分、或与后续写合并 - 避免在 hot path 上分配临时
[]byte:高频场景下,每次拼包都make([]byte, ...)会显著抬高 GC 压力
用 bufio.Writer 聚合但必须手动控制 flush 时机
bufio.Writer 是最轻量的聚合方案,但它默认只在缓冲区满或调用 Flush() 时才真正发数据——如果业务逻辑不显式 flush,包就卡在内存里,延迟不可控。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 设置合理缓冲区大小:
bufio.NewWriterSize(conn, 4096)比默认 4KB 更适合多数微服务小包场景;太大增加延迟,太小失去聚合意义 - flush 不能靠定时器硬等:用 channel 或计数器驱动,例如“攒够 10 条”或“距离上次 flush 超过 5ms”就触发,避免长尾延迟
- 务必检查
Flush()返回值:网络抖动时它可能返回io.ErrShortWrite或其他错误,不处理会导致后续写失败静默丢包
自定义写队列 + 单 goroutine 序列化发送更可控
当需要精确控制顺序、超时、重试或跨多个连接复用聚合逻辑时,bufio.Writer 就不够用了。此时用带缓冲 channel + 独立 writer goroutine 是更稳的选择。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- channel 容量设为 1024 或更低:太高会积压导致 OOM,太低容易丢包;配合
select非阻塞写 + 丢弃策略(如select { case ch ) - writer goroutine 必须持有
sync.Pool复用序列化 buffer,避免每次 marshal 都 new slice - 不要在 writer 中做耗时操作(如 JSON 序列化):提前在业务 goroutine 序列化好
[]byte再入队,writer 只负责粘包和Write - 注意连接断开时的清理:关闭 channel 后,writer 要检测
conn.Write错误并退出,避免 goroutine 泄漏
聚合包格式选二进制还是 JSON?看上下游是否可控
聚合本身不关心内容格式,但格式影响解析成本、兼容性和调试难度。JSON 看得见,但解析慢、体积大;二进制紧凑高效,但上下游必须约定 schema。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 内部微服务间通信优先用 Protocol Buffers 或 FlatBuffers:序列化快、体积小、天然支持多消息嵌套(比如
repeated Metric) - 如果下游是 Python/Node.js 等动态语言且无 protobuf 支持,可用 msgpack:比 JSON 快 3–5 倍,体积小一半,且无需预定义 schema
- 绝对不要在聚合包里混用编码:比如前 10 字节是 length-prefix,后面却是 JSON,这种“半定制协议”极易在边界条件出错(如长度字段溢出、截断)
- 聚合头必须带校验:哪怕只是
crc32,否则一个字节损坏可能导致整包解析失败甚至 panic
真正难的不是怎么聚合,是怎么让聚合不引入新延迟、不掩盖真实错误、不把简单问题变成分布式状态同步问题。每个环节的 buffer、channel、error handling,都要按“它一定会坏”来设计。