NBIO 第二彈 —— 支援 Non-Blocking HTTP 1.x
一、簡介
最近兩週擼了份 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 作為網路層處理資料 IO 。
- 讀取到的資料回撥應用層方法執行 Parser 進行解析,這裡給應用層留了引數,應用層可以自己定製執行的回撥函式,比如可以就在 NBIO 讀取資料的協程中進行解析,也可以自己定製協程池進行解析(但要注意,同一個連線的資料應該指定到同一個協程中進行解析,否則由於 TCP 的 Stream 特性,可能導致 "粘包" 相關的資料錯亂)。為了使用者便利,如果應用層傳入 nil 引數,NBIO HTTP Server 則提供預設的協程池進行解析。
- 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,不會導致客戶端收到的響應亂序。
二、兩點澄清
- 以前有小夥伴提出,golang 底層也是非同步、我這種重複再造輪子也是非同步、沒有意義——這種說法是不正確的:golang 底層也是非同步,但是語言層面或者標準庫 net 的介面層是同步的,所以才需要每個連線一個協程,而 NBIO 介面層也是非同步的,所以可以自行定製管理、避免不必要的協程建立,兩者的非同步是不一樣的。
- 還有的小夥伴提出,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 環境下進行。
四、路線圖
- Websocket
- HTTP2.0
- 前陣子有魔改了一份標準庫的 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,^_^
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- HTTP 1.x 學習筆記 —— Web 效能權威指南HTTP筆記Web
- HTTP/1.x 及 Service Worker 快取實踐小結HTTP快取
- WormHole分析第二彈Worm
- 新生答疑 第二彈
- RestSharp 元件第二彈REST元件
- ARC 雜記第二彈
- "Spring 1.x無容器Session狀態支援"到底什麼意思?SpringSession
- 簡單彈幕第二彈(c3animate實現)
- 移動前端第二彈:善用meta前端
- 彗星HTTP操作支援庫 - 易語言支援庫HTTP
- react-native第二彈來了!React
- Chrome彈窗提醒Flash將不再支援Chrome
- OSS支援HTTP/2已知影響HTTP
- 掘金秋招求職徵文,好文分享第二彈?求職
- 趣文:程式語言擬人化(第二彈)
- 11.2.0.3 RAC 靜默安裝第二彈
- 【第二章】深入HTTP請求流程HTTP
- SOA=SOME/IP?你低估了這件事 | 第二彈
- AI繪畫第二彈——影象風格遷移AI
- 計算機網路 基礎面試第二彈計算機網路面試
- 【第二彈】嵌入式工程師面試題工程師面試題
- 程式碼混淆與反混淆學習-第二彈
- Google Chrome 將增加對 HTTP Exchanges 的支援GoChromeHTTP
- 應用同時支援HTTP和HTTPSHTTP
- Spring Boot第二彈,配置檔案怎麼造?Spring Boot
- ActiveMQ第二彈:使用Spring JMS與ActiveMQ通訊MQSpring
- C#Light 和 uLua的對比第二彈C#
- Google I/O開發者大會第二彈之未來Go
- 製作 Rust 語言非同步 ORM 框架(Mybatis)第二彈Rust非同步ORM框架MyBatis
- 第二彈!python爬蟲批量下載高清大圖Python爬蟲
- 程式設計師專屬精美簡歷合集——第二彈程式設計師
- 打死也不敢說自己火的面試題更新第二彈面試題
- Python快速入門第二彈合法的變數名Python變數
- 手摸手第二彈,視覺化 RecyclerView 快取機制視覺化View快取
- 小巧玲瓏的react框架(第二彈)正式命名--aominiReact框架
- Go Module 支援 HTTP 協議的私有庫方案GoHTTP協議
- Dio 3.0釋出,支援Flutter Web 和 Http/2.0FlutterWebHTTP
- 升級nginx以支援http2的方法NginxHTTP