Go 高性能网络编程:从 net/http 到 gRPC 的演进
Go 高性能网络编程的演进主线只有一句话:**net/http 用「goroutine-per-connection + runtime netpoller」把 C10K 问题变成写业务代码就能解决的事,而 gRPC 用「HTTP/2 多路复用 + Protobuf 二进制编码」把服务间通信的单位从「连接」改成了「流」,从根上消掉了 HTTP/1.1 的队头阻塞和头部冗余。**两者不是替代关系,而是分别守住了「对外 HTTP 接口」和「内网服务调用」两个场景。
net/http 的模型:goroutine 便宜,但不是免费的
结论:net/http 的性能天花板不在并发模型,而在 HTTP/1.1 协议本身和你没设的超时参数。
Go 的 netpoller 是 runtime 对 epoll(Linux)/kqueue(macOS)/IOCP(Windows)的封装。当某个 goroutine 卡在 socket 读写上,runtime 会把它挂起并把 fd 注册进 netpoller,等 fd 就绪再唤醒——所以「一个连接一个 goroutine」在 Go 里是廉价且线程安全的写法,goroutine 初始栈只有几 KB 且按需增长。这就是 Go 敢让新手直接写高并发服务的原因。
但默认值全是给「能跑」设计的,不是给「跑得快」设计的。生产上必须显式覆盖这几个字段:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // 防慢速攻击(Slowloris)
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 16, // 默认 1<<20,通常过大
}
客户端侧最容易踩的坑是 `http.Transport.MaxIdleConnsPerHost`,它默认只有 2。高并发调下游时,连接池瞬间被打满、大量请求卡在等空闲连接上,表现就是「CPU 不高但延迟很高」。调到 100~200 通常能立刻见效:
tr := &http.Transport{
MaxIdleConns: 2000,
MaxIdleConnsPerHost: 200, // 默认 2,头号性能坑
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
}
另外两点常被忽略:一是走 TLS 时用不了 sendfile 零拷贝(`io.Copy` 的 `ReadFrom` 优化仅在明文 HTTP 生效);二是请求体尽量用 `sync.Pool` 复用 `[]byte` 缓冲,否则高 QPS 下 GC 压力会直接吃掉吞吐。
gRPC 为什么快:把「连接」换成「流」
结论:gRPC 在服务间通信上快于 HTTP/1.1,靠的是 HTTP/2 的单连接多路复用、HPACK 头部压缩和 Protobuf 的紧凑编码,三者是叠加收益,不是单点优化。
HTTP/1.1 一个连接同一时刻只能跑一个请求,想并发只能开多条 TCP 连接,连接数一多,握手、TLS、内核调度开销全上来了。HTTP/2 把一条连接切成多个 stream(流,独立的双向数据通道),每个 stream 有自己的 ID 和流量控制窗口,互不阻塞——这就是「多路复用」。
序列化上,Protobuf 是二进制 + 字段编号编码,同样结构的数据体积通常只有 JSON 的 1/3 到 1/2,解析不需要词法分析。再加上 gRPC 原生支持四种调用模式(一元、服务端流、客户端流、双向流),流式推送场景下比轮询优雅得多。
连接保活和窗口也要配,默认值不适合长连接服务:
s := grpc.NewServer(
grpc.MaxConcurrentStreams(1000), // 默认 math.MaxUint32,等于不限,建议显式限流
grpc.MaxRecvMsgSize(16<<20), // 默认 4MB
grpc.KeepaliveParams(keepalive.ServerParameters{Time: 30 * time.Second, Timeout: 10 * time.Second}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 10 * time.Second, PermitWithoutStream: true, // 默认 5 分钟,客户端发太勤会被 GOAWAY
}),
)
客户端侧注意:`grpc.Dial` 已废弃,用 `grpc.NewClient`;跨机房或高带宽延迟积(BDP)链路可显式放大窗口:
conn, err := grpc.NewClient(addr,
grpc.WithInitialWindowSize(1<<20), // 单流窗口
grpc.WithInitialConnWindowSize(1<<20), // 整条连接窗口
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second, Timeout: 10 * time.Second, PermitWithoutStream: true,
}),
)
gRPC-Go 会根据 BDP 动态调整窗口,但显式设置能在长肥管道上更快进入高吞吐区间。
什么时候不该用 gRPC,以及中间选项
结论:对外接口继续用 net/http(开 HTTP/2 + TLS 即可),内网服务间用 gRPC,只有在「超高频、短小、纯 JSON、连接可复用」的极端场景才考虑 fasthttp 这类非常规方案。
理由很实在:gRPC 的调试成本高(不能用 curl,需要 grpcurl 或 ghz),浏览器不能直接调用(要 grpc-web 代理),Protobuf 的 schema 变更需要纪律。对外 API 上这些代价往往不划算。
fasthttp 的思路是复用 `RequestCtx` 和底层 buffer、绕开 `net/http` 的接口分配,短连接压测数字确实漂亮,但它不实现 `http.Handler` 接口,也不支持 HTTP/2,生态兼容性差——除非你压测已确认瓶颈在框架分配上,否则不建议为了数字换掉标准库。
压测环节别忘了工具链:`wrk`/`hey` 打 HTTP,`ghz` 打 gRPC,两者都支持并发数与连接数分离配置,才能定位到底是连接瓶颈还是流瓶颈。
把全文收一下:net/http 的价值是「默认安全、生态完整」,你要做的是补齐超时和连接池参数;gRPC 的价值是「协议层解决队头阻塞 + 编码层压缩体积」,你要做的是配好流控窗口、keepalive 和最大流数。演进的本质不是换框架,而是根据通信场景选对抽象层级。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





