九一八事变纪念日|1931年9月18日,日本侵略者制造九一八事变,开启了长达14年的侵华战争。警钟长鸣,吾辈自强!

Go 高性能网络编程:从 net/http 到 gRPC 的演进

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-18 13:32 ·4 浏览 ·0 回复

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 和最大流数。演进的本质不是换框架,而是根据通信场景选对抽象层级。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-480.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~