NBIO 第二弹 —— 支持 Non-Blocking HTTP 1.x

lesismal · · 2583 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

## 一、简介 最近两周撸了份 HTTP 1.x 的 [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) ,用于支持异步网络库的数据解析(同步网络库当然也可以使用),在此基础之上实现了 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) ,其他异步网络库也可以使用这个 [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) 进行 HTTP Server 的封装,但需依赖其他网络库实现 net.Conn。 众所周知,标准库的 HTTP 为每个连接创建一个协程,在高并发场景下比如10k、100k甚至1000k,需要创建大量的协程,消耗大量的内存、协程调度等成本。但是使用异步网络库,可以不用为每个连接都创建单独的协程,从而降低相应的消耗、极大提高同等硬件的负载能力。 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 兼容标准库的 http.Handler ,所以已有的基于标准库的 web 框架也可以很容易地使用 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 作为异步网络层来替换标准库。 如果需要对 fasthttp 这类不使用标准库的 web 框架进行支持,也只需参考默认兼容标准库的 [Processor](https://github.com/lesismal/nbio/blob/master/nbhttp/processor.go#L64),实现一份对应 fasthttp Hadler 的 Processor 即可。但由于 fasthttp 默认使用 []byte 作为原始数据字段的存储,而 [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) 兼顾应用层便利在参数传递中直接转换成了 string ,所以需要浪费一点不必要的 string/[]byte 转换,也可以考虑是否需要把参数传递改成 []byte,但改成 []byte 看上去就不那么友好、美观了。 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 网络层接口在 *nix 系统上是异步的,处理流程是: 1. [NBIO](https://github.com/lesismal/nbio) 作为网络层处理数据 IO 。 2. 读取到的数据回调应用层方法执行 [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) 进行解析,这里给应用层留了参数,应用层可以自己定制执行的回调函数,比如可以就在 [NBIO](https://github.com/lesismal/nbio) 读取数据的协程中进行解析,也可以自己定制协程池进行解析(但要注意,同一个连接的数据应该指定到同一个协程中进行解析,否则由于 TCP 的 Stream 特性,可能导致 "粘包" 相关的数据错乱)。为了使用者便利,如果应用层传入 nil 参数,[NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 则提供默认的协程池进行解析。 3. [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) 解析到一个完整消息后调用业务层回调进行处理,这里与 [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) 类似,可由应用层传入处理函数,如果传入 nil 参数,则由默认的协程池进行处理,这里的协程池与 [Parser](https://github.com/lesismal/nbio/blob/master/nbhttp/parser.go) 的协程池不同,因为已经是完整的消息,可以由协程池内空闲协程而非指定协程抢任务执行,以避免单个连接某个方法处理中可能存在DB等慢操作导致其他连接的消息处理被阻塞。 - 关于3中协程池,[NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 支持乱序处理、顺序回包。如果请求方的客户端实现支持单个连接的多个消息非线头阻塞发送、而不用等待每个消息收到回复才发出下个请求的数据,则该连接的多个请求有可能在 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 默认协程池中乱序执行,比如 request 1 需要1秒进行处理,request 2也到达并且只需要10ms进行处理,则 request 2先被处理完,但是 request 2回复的数据会被缓存,仍然等request 1处理完成后先回复 request 1、再回复 request 2,不会导致客户端收到的响应乱序。 ## 二、两点澄清 1. 以前有小伙伴提出,golang底层也是异步、我这种重复再造轮子也是异步、没有意义——这种说法是不正确的:golang底层也是异步,但是语言层面或者标准库net的接口层是同步的,所以才需要每个连接一个协程,而 [NBIO](https://github.com/lesismal/nbio) 接口层也是异步的,所以可以自行定制管理、避免不必要的协程创建,两者的异步是不一样的。 2. 还有的小伙伴提出,golang的同步模式是巨大的进步,我这个库又回到异步模式,是倒退——这种说法也是不准确的:底层基础设施的异步,并不代表应用层也一定要异步,golang的协程和chan足够方便,应用层完全可以自己定制多种编程模式。[NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 在上面简介流程 3 中的消息处理,应用层的 http.Handler 内,和使用标准库的方式是没有变化的,业务层仍然是按照同步的方式进行顺序逻辑的处理。 ## 三、示例代码 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 的示例请参考这里:https://github.com/lesismal/nbio/tree/master/examples/http 。 这里也包括了一份百万连接的测试样例:[百万连接测试代码](https://github.com/lesismal/nbio/tree/master/examples/http/1m) ,由于网络协议栈的 PORT 使用 short 类型导致的 65535 限制,为了免去单机压测部署环境的麻烦,百万连接测试的示例代码开启监听了多组端口,因为这些端口接受连接和处理 IO 都是共用相同的一组 poller ,单一端口也是使用这组 poller,所以多端口跟单一端口的性能是基本一致的,有兴趣的小伙伴也可以改成单一端口、自行搭建虚拟网络或者多组docker、真实多机环境、压测客户端之类的进行压测 PS:[NBIO](https://github.com/lesismal/nbio) 主要针对 *nix 系统,在 windows 下为了方便用户调试,使用标准库的 net 实现了接口兼容,windows 下的压测数据不用来作为性能对比的参考,压测请于 linux 环境下进行。 ## 四、路线图 1. Websocket 2. HTTP2.0 3. 前阵子有魔改了一份标准库的 TLS 支持异步并与 NBIO 打通,但是标准库的 TLS 原来是同步模式的代码、魔改成支持异步的很多细节我没有优化、显得臃肿浪费,希望以后有档期完全重写一份更清爽的 - 每一项都是体力活,感觉路漫漫,也希望有兴趣的大佬、小伙伴多来交流、PR ## 五、以 gin 为例,分别使用 STD、NBIO 进行压测对比 - 压测环境: 4c8t / 8g 虚拟机,C/S localhost ### 1. gin 默认使用标准库压测 #### 1)gin std server 代码 ```golang 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) } } ``` #### 2)wrk压测20k连接数 ```sh wrk -t4 -c20000 -d30s --latency http://localhost:8080/hello ``` #### 3)压测结果日志 所有连接建立成功直到 qps 稳定的 server 日志: ```sh [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 测试结果日志: ```sh 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 ``` ### 2. 使用 [NBIO HTTP Server](https://github.com/lesismal/nbio/blob/master/nbhttp/server.go) 作为 gin 的网络层压测 #### 1)gin nbio server 代码 ```golang 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) } } ``` #### 2)wrk压测20k连接数 ```sh wrk -t4 -c20000 -d30s --latency http://localhost:8080/hello ``` #### 3)压测结果 所有连接建立成功直到 qps 稳定的 server 日志: ```sh [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 测试结果日志: ```sh 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,^_^

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2583 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传