迫于分享创造节点似乎不对,重新发到 GO 节点 NBIO 第二弹,支持 Non-Blocking HTTP 1.x - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
lesismal
V2EX    Go 编程语言

迫于分享创造节点似乎不对,重新发到 GO 节点 NBIO 第二弹,支持 Non-Blocking HTTP 1.x

  •  
  •   lesismal
    lesismal 2021-03-15 19:49:50 +08:00 1290 次点击
    这是一个创建于 1674 天前的主题,其中的信息可能已经有所发展或是发生改变。

    上一弹在这里

    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 系统上是异步的,处理流程是:

    1. NBIO 作为网络层处理数据 IO 。
    2. 读取到的数据回调应用层方法执行 Parser 进行解析,这里给应用层留了参数,应用层可以自己定制执行的回调函数,比如可以就在 NBIO 读取数据的协程中进行解析,也可以自己定制协程池进行解析(但要注意,同一个连接的数据应该指定到同一个协程中进行解析,否则由于 TCP 的 Stream 特性,可能导致 "粘包" 相关的数据错乱)。为了使用者便利,如果应用层传入 nil 参数,NBIO HTTP Server 则提供默认的协程池进行解析。
    3. Parser 解析到一个完整消息后调用业务层回调进行处理,这里与 Parser 类似,可由应用层传入处理函数,如果传入 nil 参数,则由默认的协程池进行处理,这里的协程池与 Parser 的协程池不同,因为已经是完整的消息,可以由协程池内空闲协程而非指定协程抢任务执行,以避免单个连接某个方法处理中可能存在 DB 等慢操作导致其他连接的消息处理被阻塞。
    • 关于 3 中协程池,NBIO HTTP Server 支持乱序处理、顺序回包。如果请求方的客户端实现支持单个连接的多个消息非线头阻塞发送、而不用等待每个消息收到回复才发出下个请求的数据,则该连接的多个请求有可能在 NBIO HTTP Server 默认协程池中乱序执行,比如 request 1 需要 1 秒进行处理,request 2 也到达并且只需要 10ms 进行处理,则 request 2 先被处理完,但是 request 2 回复的数据会被缓存,仍然等 request 1 处理完成后先回复 request 1 、再回复 request 2,不会导致客户端收到的响应乱序。

    二、两点澄清

    1. 以前有小伙伴提出,golang 底层也是异步、我这种重复再造轮子也是异步、没有意义这种说法是不正确的:golang 底层也是异步,但是语言层面或者标准库 net 的接口层是同步的,所以才需要每个连接一个协程,而 NBIO 接口层也是异步的,所以可以自行定制管理、避免不必要的协程创建,两者的异步是不一样的。
    2. 还有的小伙伴提出,golang 的同步模式是巨大的进步,我这个库又回到异步模式,是倒退这种说法也是不准确的:底层基础设施的异步,并不代表应用层也一定要异步,golang 的协程和 chan 足够方便,应用层完全可以自己定制多种编程模式。NBIO HTTP Server 在上面简介流程 3 中的消息处理,应用层的 http.Handler 内,和使用标准库的方式是没有变化的,业务层仍然是按照同步的方式进行顺序逻辑的处理。

    三、示例代码

    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 环境下进行。

    四、路线图

    1. Websocket
    2. HTTP2.0
    3. 前阵子有魔改了一份标准库的 TLS 支持异步并与 NBIO 打通,但是标准库的 TLS 原来是同步模式的代码、魔改成支持异步的很多细节我没有优化、显得臃肿费,希望以后有档期完全重写一份更清爽的
    • 每一项都是体力活,感觉路漫漫,也希望有兴趣的大佬、小伙伴多来交流、PR

    五、以 gin 为例,分别使用 STD 、NBIO 进行压测对比

    • 压测环境:4c8t / 8g 虚拟机,C/S localhost

    1. gin 默认使用标准库压测

    1 ) gin std server 代码

    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 连接数

    wrk -t4 -c20000 -d30s --latency http://localhost:8080/hello 

    3 )压测结果日志

    所有连接建立成功直到 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 

    2. 使用 NBIO HTTP Server 作为 gin 的网络层压测

    1 ) gin nbio server 代码

    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 连接数

    wrk -t4 -c20000 -d30s --latency http://localhost:8080/hello 

    3 )压测结果

    所有连接建立成功直到 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,^_^

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     931 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 19:57 PVG 03:57 LAX 12:57 JFK 15:57
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86