https://www.v2ex.com/t/755862#reply53
最近两周撸了份 HTTP 1.x 的 Parser ,用于支持异步网络库的数据解析(同步网络库当然也可以使用),在此基础之上实现了 NBIO HTTP Server ,其他异步网络库也可以使用这个 Parser 进行 HTTP Server 的封装,但需依赖其他网络库实现 net.Conn 。
众所周知,标准库的 HTTP 为每个连接创建一个协程,在高并发场景下比如 10k 、100k 甚至 1000k,需要创建大量的协程,消耗大量的内存、协程调度等成本。但是使用异步网络库,可以不用为每个连接都创建单独的协程,从而降低相应的消耗、极大提高同等硬件的负载能力。
NBIO HTTP Server 兼容标准库的 http.Handler,所以已有的基于标准库的 web 框架也可以很容易地使用 NBIO HTTP Server 作为异步网络层来替换标准库。 如果需要对 fasthttp 这类不使用标准库的 web 框架进行支持,也只需参考默认兼容标准库的 Processor,实现一份对应 fasthttp Hadler 的 Processor 即可。但由于 fasthttp 默认使用 []byte 作为原始数据字段的存储,而 Parser 兼顾应用层便利在参数传递中直接转换成了 string,所以需要浪费一点不必要的 string/[]byte 转换,也可以考虑是否需要把参数传递改成 []byte,但改成 []byte 看上去就不那么友好、美观了。
NBIO HTTP Server 网络层接口在 *nix 系统上是异步的,处理流程是:
NBIO HTTP Server 的示例请参考这里: https://github.com/lesismal/nbio/tree/master/examples/http 。
这里也包括了一份百万连接的测试样例:百万连接测试代码 ,由于网络协议栈的 PORT 使用 short 类型导致的 65535 限制,为了免去单机压测部署环境的麻烦,百万连接测试的示例代码开启监听了多组端口,因为这些端口接受连接和处理 IO 都是共用相同的一组 poller,单一端口也是使用这组 poller,所以多端口跟单一端口的性能是基本一致的,有兴趣的小伙伴也可以改成单一端口、自行搭建虚拟网络或者多组 docker 、真实多机环境、压测客户端之类的进行压测 PS:NBIO 主要针对 *nix 系统,在 windows 下为了方便用户调试,使用标准库的 net 实现了接口兼容,windows 下的压测数据不用来作为性能对比的参考,压测请于 linux 环境下进行。
package main
import (
"fmt"
"net/http"
"runtime"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
)
func main() {
var (
qps uint64 = 0
total uint64 = 0
)
router := gin.New()
router.GET("/hello", func(c *gin.Context) {
atomic.AddUint64(&qps, 1)
c.String( http.StatusOK, "hello")
})
go router.Run()
ticker := time.NewTicker(time.Second)
for i := 1; true; i++ {
<-ticker.C
n := atomic.SwapUint64(&qps, 0)
total += n
fmt.Printf("running for %v seconds, NumGoroutine: %v, qps: %v, total: %v\n", i, runtime.NumGoroutine(), n, total)
}
}
wrk -t4 -c20000 -d30s --latency http://localhost:8080/hello
所有连接建立成功直到 qps 稳定的 server 日志:
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /hello --> main.main.func1 (1 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
running for 1 seconds, NumGoroutine: 2, qps: 0, total: 0
running for 2 seconds, NumGoroutine: 2, qps: 0, total: 0
running for 3 seconds, NumGoroutine: 5277, qps: 0, total: 0
running for 4 seconds, NumGoroutine: 9411, qps: 0, total: 0
running for 5 seconds, NumGoroutine: 11404, qps: 0, total: 0
running for 6 seconds, NumGoroutine: 15696, qps: 95115, total: 95115
running for 7 seconds, NumGoroutine: 16653, qps: 74368, total: 169483
running for 8 seconds, NumGoroutine: 19188, qps: 72357, total: 241840
running for 9 seconds, NumGoroutine: 19942, qps: 68762, total: 310602
running for 10 seconds, NumGoroutine: 19936, qps: 86198, total: 396800
running for 11 seconds, NumGoroutine: 20008, qps: 114406, total: 511206
running for 12 seconds, NumGoroutine: 20015, qps: 137557, total: 648763
running for 13 seconds, NumGoroutine: 20003, qps: 135883, total: 784646
running for 14 seconds, NumGoroutine: 20009, qps: 130973, total: 915619
running for 15 seconds, NumGoroutine: 20011, qps: 130860, total: 1046479
wrk 测试结果日志:
Running 30s test @ http://localhost:8080/hello
4 threads and 20000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 145.59ms 79.06ms 1.36s 88.79%
Req/Sec 32.62k 10.49k 73.27k 79.31%
Latency Distribution
50% 131.01ms
75% 151.73ms
90% 186.63ms
99% 542.54ms
3391563 requests in 30.09s, 391.37MB read
Requests/sec: 112705.44
Transfer/sec: 13.01MB
package main
import (
"fmt"
"net/http"
"runtime"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/lesismal/nbio/nbhttp"
)
func main() {
var (
qps uint64 = 0
total uint64 = 0
)
router := gin.New()
router.GET("/hello", func(c *gin.Context) {
atomic.AddUint64(&qps, 1)
c.String( http.StatusOK, "hello")
})
svr := nbhttp.NewServer(nbhttp.Config{
Network: "tcp",
Addrs: []string{"localhost:8080"},
NPoller: 8, // runtime.NumCPU(),
NParser: 8, // runtime.NumCPU(),
TaskPoolSize: 100, // runtime.NumCPU() * 10, // goroutines pool to execute http.Handler
}, router, nil, nil)
err := svr.Start()
if err != nil {
fmt.Printf("nbio.Start failed: %v\n", err)
return
}
defer svr.Stop()
ticker := time.NewTicker(time.Second)
for i := 1; true; i++ {
<-ticker.C
n := atomic.SwapUint64(&qps, 0)
total += n
fmt.Printf("running for %v seconds, online: %v, NumGoroutine: %v, qps: %v, total: %v\n", i, svr.State().Online, runtime.NumGoroutine(), n, total)
}
}
wrk -t4 -c20000 -d30s --latency http://localhost:8080/hello
所有连接建立成功直到 qps 稳定的 server 日志:
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /hello --> main.main.func1 (1 handlers)
2021/03/13 14:06:03.797 [INF] Gopher[NB] start listen on: ["localhost:8080"]
running for 1 seconds, online: 0, NumGoroutine: 19, qps: 0, total: 0
running for 2 seconds, online: 0, NumGoroutine: 19, qps: 0, total: 0
running for 3 seconds, online: 0, NumGoroutine: 19, qps: 0, total: 0
running for 4 seconds, online: 4068, NumGoroutine: 19, qps: 0, total: 0
running for 5 seconds, online: 9061, NumGoroutine: 19, qps: 0, total: 0
running for 6 seconds, online: 12567, NumGoroutine: 119, qps: 3598, total: 3598
running for 7 seconds, online: 18018, NumGoroutine: 119, qps: 126743, total: 130341
running for 8 seconds, online: 19916, NumGoroutine: 119, qps: 153748, total: 284089
running for 9 seconds, online: 19916, NumGoroutine: 119, qps: 152665, total: 436754
running for 10 seconds, online: 19916, NumGoroutine: 119, qps: 156468, total: 593222
running for 11 seconds, online: 20000, NumGoroutine: 119, qps: 146699, total: 739921
running for 12 seconds, online: 20000, NumGoroutine: 119, qps: 145776, total: 885697
running for 13 seconds, online: 20000, NumGoroutine: 119, qps: 155327, total: 1041024
running for 14 seconds, online: 20000, NumGoroutine: 119, qps: 148740, total: 1189764
running for 15 seconds, online: 20000, NumGoroutine: 119, qps: 143539, total: 1333303
wrk 测试结果日志:
Running 30s test @ http://localhost:8080/hello
4 threads and 20000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 129.22ms 26.45ms 609.89ms 74.38%
Req/Sec 38.08k 3.69k 57.58k 72.97%
Latency Distribution
50% 128.42ms
75% 144.86ms
90% 160.37ms
99% 191.20ms
4146017 requests in 30.06s, 478.43MB read
指标 | GIN+STD | GIN+NBIO |
---|---|---|
压测连接数 | 20000 | 20000 |
峰值进程协程数量 | 20000+ | 119 |
峰值内存占用 | 600+M | 60+M |
峰值 CPU 占用 | 500-600% | 400-500% |
wrk Latency Avg | 145.59ms | 129.22ms |
wrk Latency Stdev | 79.06ms | 26.45ms |
wrk Latency Max | 1.36s | 609.89ms |
wrk Latency 50% | 131.01ms | 128.42ms |
wrk Latency 75% | 151.73ms | 144.86ms |
wrk Latency 90% | 186.63ms | 160.37ms |
wrk Latency 99% | 542.54ms | 191.20ms |
wrk Req/Sec Avg | 32.62k | 38.08k |
wrk Req/Sec Stdev | 10.49k | 3.69k |
wrk Req/Sec Max | 73.27k | 57.58k |
GIN+NBIO 方式整体压测指标好于 GIN+STD,相比之下,极低的内存占用尤为明显,NBIO 可以使同配置或者低配硬件的负载能力大幅提升。
多数小伙伴们的业务可能不需要极致的资源控制、通常加机器就行,但面对海量并发场景、大规模集群时,异步网络框架可以极大降低相应的硬件成本。
现在的云、大数据、人工智能、物联网、5G 时代已经蓬勃发展,但这一切只是开始,IT 爆炸的时代,很多传统领域都在 IT 化,未来的数据量、计算量、网络传输量更会越来越迅猛地增长,海量计算的基础之上,一点算力的节约会在放大效应下变得非常明显。
以物联网为例,海量接入设备、海量并发连接数之下,golang 标准库的每个连接一个协程的默认同步模式可能会成为性能瓶颈,需要更多的硬件开销、能源消耗。超高并发场景下,以 golang 标准库方案的性能、资源消耗、负载能力,目前赶不上 java netty 、nodejs,更不用说 c/c++/rust,所以个人认为 golang 的异步基础设施很有必要,还有很大发展空间。
欢迎有兴趣的小伙伴关注、进行更多测试,以及 issue 、pr 、star,^_^