Github 主頁
https://github.com/panjf2000/gnet
歡迎大家圍觀~~,目前還在持續更新,感興趣的話可以 star 一下暗中觀察哦。
簡介
gnet
是一個基於 Event-Loop 事件驅動的高性能和輕量級網絡庫。這個庫直接使用 epoll 和 kqueue 系統調用而非標准 Golang 網絡包:net 來構建網絡應用,它的工作原理類似於兩個開源的網絡庫:libuv 和 libevent。
這個項目存在的價值是提供一個在網絡包處理方面能和 Redis、Haproxy 這兩個項目具有相近性能的Go 語言網絡服務器框架。
gnet
的亮點在於它是一個高性能、輕量級、非阻塞的純 Go 實現的傳輸層(TCP/UDP/Unix-Socket)網絡庫,開發者可以使用 gnet
來實現自己的應用層網絡協議,從而構建出自己的應用層網絡應用:比如在 gnet
上實現 HTTP 協議就可以創建出一個 HTTP 服務器 或者 Web 開發框架,實現 Redis 協議就可以創建出自己的 Redis 服務器等等。
gnet
衍生自另一個項目:evio
,但是性能更好。
功能
- 高性能 的基於多線程模型的 Event-Loop 事件驅動
- 內置 Round-Robin 輪詢負載均衡算法
- 簡潔的 APIs
- 基於 Ring-Buffer 的高效內存利用
- 支持多種網絡協議:TCP、UDP、Unix Sockets
- 支持兩種事件驅動機制:Linux 里的 epoll 以及 FreeBSD 里的 kqueue
- 支持異步寫操作
- 允許多個網絡監聽地址綁定在一個 Event-Loop 上
- 靈活的事件定時器
- SO_REUSEPORT 端口重用
核心設計
多線程/Go程模型
主從多 Reactors 模型
gnet
重新設計開發了一個新內置的多線程/Go程模型:『主從多 Reactors』,這也是 netty
默認的線程模型,下面是這個模型的原理圖:
它的運行流程如下面的時序圖:
主從多 Reactors + 線程/Go程池
你可能會問一個問題:如果我的業務邏輯是阻塞的,那么在 Event.React()
注冊方法里的邏輯也會阻塞,從而導致阻塞 event-loop 線程,這時候怎么辦?
正如你所知,基於 gnet
編寫你的網絡服務器有一條最重要的原則:永遠不能讓你業務邏輯(一般寫在 Event.React()
里)阻塞 event-loop 線程,否則的話將會極大地降低服務器的吞吐量,這也是 netty
的一條最重要的原則。
我的回答是,現在我正在為 gnet
開發一個新的多線程/Go程模型:『帶線程/Go程池的主從多 Reactors』,這個新網絡模型將通過引入一個 worker pool 來解決業務邏輯阻塞的問題:它會在啟動的時候初始化一個 worker pool,然后在把 Event.React()
里面的阻塞代碼放到 worker pool 里執行,從而避免阻塞 event-loop 線程,
這個模型還在持續開發中並且很快就能完成,模型的架構圖如下所示:
它的運行流程如下面的時序圖:
不過,在這個新的網絡模型開發完成之前,你依然可以通過一些其他的外部開源 goroutine pool 來處理你的阻塞業務邏輯,在這里我推薦個人開發的一個開源 goroutine pool:ants,它是一個基於 Go 開發的高性能的 goroutine pool ,實現了對大規模 goroutine 的調度管理、goroutine 復用。
你可以在開發 gnet
網絡應用的時候集成 ants
庫,然后把那些阻塞業務邏輯提交到 ants
池里去執行,從而避免阻塞 event-loop 線程。
通信機制
gnet
的『主從 Reactors 多線程』模型是基於 Golang 里的 Goroutines的,一個 Reactor 掛載在一個 Goroutine 上,所以在 gnet
的這個網絡模型里主 Reactor/Goroutine 與從 Reactors/Goroutines 有海量通信的需求,因此 gnet
里必須要有一個能在 Goroutines 之間進行高效率的通信的機制,我沒有選擇 Golang 里的主流方案:基於 Channel 的 CSP 模型,而是選擇了性能更好、基於 Ring-Buffer 的 Disruptor 方案。
所以我最終選擇了 go-disruptor:高性能消息分發隊列 LMAX Disruptor 的 Golang 實現。
自動擴容的 Ring-Buffer
gnet
利用 Ring-Buffer 來緩存 TCP 流數據以及管理內存使用。
開始使用
安裝
$ go get -u github.com/panjf2000/gnet
使用示例
用 gnet
來構建網絡服務器是非常簡單的,只需要把你關心的事件注冊到 gnet.Events
里面,然后把它和綁定的監聽地址一起傳遞給 gnet.Serve
方法就完成了。在服務器開始工作之后,每一條到來的網絡連接會在各個事件之間傳遞,如果你想在某個事件中關閉某條連接或者關掉整個服務器的話,直接把 gnet.Action
設置成 Cosed
或者 Shutdown
就行了。
Echo 服務器是一種最簡單網絡服務器,把它作為 gnet
的入門例子在再合適不過了,下面是一個最簡單的 echo server,它監聽了 9000 端口:
不帶阻塞邏輯的 echo 服務器
package main
import (
"log"
"github.com/panjf2000/gnet"
)
func main() {
var events gnet.Events
events.Multicore = true
events.React = func(c gnet.Conn) (out []byte, action gnet.Action) {
top, tail := c.ReadPair()
out = append(top, tail...)
c.ResetBuffer()
if trace {
log.Printf("%s", strings.TrimSpace(string(top)+string(tail)))
}
return
}
log.Fatal(gnet.Serve(events, "tcp://:9000"))
}
正如你所見,上面的例子里 gnet
實例只注冊了一個 React
事件。一般來說,主要的業務邏輯代碼會寫在這個事件方法里,這個方法會在服務器接收到客戶端寫過來的數據之時被調用,然后處理輸入數據(這里只是把數據 echo 回去)並且在處理完之后把需要輸出的數據賦值給 out
變量然后返回,之后你就不用管了,gnet
會幫你把數據寫回客戶端的。
帶阻塞邏輯的 echo 服務器
package main
import (
"log"
"time"
"github.com/panjf2000/gnet"
"github.com/panjf2000/ants"
)
func main() {
var events gnet.Events
events.Multicore = true
poolSize := 256 * 1024
pool, _ := ants.NewPool(poolSize, ants.WithNonblocking(true))
defer pool.Release()
events.React = func(c gnet.Conn) (out []byte, action gnet.Action) {
data := c.ReadBytes()
c.ResetBuffer()
// Use ants pool to unblock the event-loop.
_ = pool.Submit(func() {
time.Sleep(1 * time.Second)
c.AsyncWrite(data)
})
return
}
log.Fatal(gnet.Serve(events, "tcp://:9000"))
}
正如我在『主從多 Reactors + 線程/Go程池』那一節所說的那樣,如果你的業務邏輯里包含阻塞代碼,那么你應該把這些阻塞代碼變成非阻塞的,比如通過把這部分代碼通過 goroutine 去運行,但是要注意一點,如果你的服務器處理的流量足夠的大,那么這種做法將會導致創建大量的 goroutines 極大地消耗系統資源,所以我一般建議你用 goroutine pool 來做 goroutines 的復用和管理,以及節省系統資源。
I/O 事件
gnet
目前支持的 I/O 事件如下:
OnInitComplete
當 server 初始化完成之后調用。OnOpened
當連接被打開的時候調用。OnClosed
當連接被關閉的時候調用。React
當 server 端接收到從 client 端發送來的數據的時候調用。(你的核心業務代碼一般是寫在這個方法里)Tick
服務器啟動的時候會調用一次,之后就以給定的時間間隔定時調用一次,是一個定時器方法。PreWrite
預先寫數據方法,在 server 端寫數據回 client 端之前調用。
多地址綁定
// 在同一個 Server 上同時綁定 TCP 和 Unix-Socket 兩個地址
gnet.Serve(events, "tcp://:9000", "unix://socket")
定時器
Tick
會每隔一段時間觸發一次,間隔時間你可以自己控制,設定返回的 delay
變量就行。
定時器的第一次觸發是在 gnet.Serving
事件之后。
events.Tick = func() (delay time.Duration, action Action){
log.Printf("tick")
delay = time.Second
return
}
UDP 支持
gnet
支持 UDP 協議,在 gnet.Serve
里綁定 UDP 地址即可,gnet
的 UDP 支持有如下的特性:
- 數據進入服務器之后立刻寫回客戶端,不做緩存。
OnOpened
和OnClosed
這兩個事件在 UDP 下不可用,唯一可用的事件是React
。
使用多核
Events.Multicore
參數指定了 gnet
是否會使用多核來進行服務,如果是 true
的話就會使用多核,否則就是單核運行,利用的核心數一般是機器的 CPU 數量。
負載均衡
gnet
目前內置的負載均衡算法是輪詢調度 Round-Robin,暫時不支持自定制。
SO_REUSEPORT 端口復用
服務器支持 SO_REUSEPORT 端口復用特性,允許多個 sockets 監聽同一個端口,然后內核會幫你做好負載均衡,每次只喚醒一個 socket 來處理 accept 請求,避免驚群效應。
開啟這個功能也很簡單,在要綁定的監聽地址后面設置 reuseport=true
即可:
gnet.Serve(events, "tcp://:9000?reuseport=true"))
性能測試
Linux (epoll)
系統參數
# Machine information
OS : Ubuntu 18.04/x86_64
CPU : 8 Virtual CPUs
Memory : 16.0 GiB
# Go version and configurations
Go Version : go1.12.9 linux/amd64
GOMAXPROCS=8
同類型的網絡庫性能對比:
Echo Server
HTTP Server
FreeBSD (kqueue)
系統參數
# Machine information
OS : macOS Mojave 10.14.6/x86_64
CPU : 4 CPUs
Memory : 8.0 GiB
# Go version and configurations
Go Version : go version go1.12.9 darwin/amd64
GOMAXPROCS=4
Echo Server
HTTP Server
證書
gnet
的源碼允許用戶在遵循 MIT 開源證書 規則的前提下使用。
相關文章
- A Million WebSockets and Go
- Going Infinite, handling 1M websockets connections in Go
- gnet: 一個輕量級且高性能的 Golang 網絡庫
待做事項
gnet 還在持續開發的過程中,所以這個倉庫的代碼和文檔會一直持續更新,如果你對 gnet 感興趣的話,歡迎給這個開源庫貢獻你的代碼,還有你要是喜歡 gnet 的話,可以給個星星鼓勵一下哦 ~~