本文目錄一覽:
- 1、iris 真的是最快的Golang 路由框架嗎
- 2、golang 有哪些比較穩定的 web 開發框架
- 3、GoLang — Gin框架
- 4、golang反射框架Fx
- 5、golang性能測試框架k6源碼分析
iris 真的是最快的Golang 路由框架嗎
對各種Go http路由框架的比較, Iris明顯勝出,它的性能遠遠超過其它Golang http路由框架。
但是,在真實的環境中,Iris真的就是最快的Golang http路由框架嗎?
Benchmark測試分析
在那篇文章中我使用的是Julien Schmidt的 測試代碼,他模擬了靜態路由、Github API、Goolge+ API、Parse API的各種情況,因為這些API是知名網站的開放的API,看起來測試挺真實可靠的。
但是,這個測試存在著一個嚴重的問題,就是Handler的業務邏輯非常的簡單,各個框架的handler類似,比如Iris的Handler的實現:
funcirisHandler(_ *iris.Context) {}funcirisHandlerWrite(c *iris.Context) { io.WriteString(c.ResponseWriter, c.Param(“name”))}funcirisHandlerTest(c *iris.Context) { io.WriteString(c.ResponseWriter, c.Request.RequestURI)}
幾乎沒有任何的業務邏輯,最多是往Response中寫入一個字元串。
這和生產環境中的情況嚴重不符!
實際的產品肯定會有一些業務的處理,比如參數的校驗,數據的計算,本地文件的讀取、遠程服務的調用、緩存的讀取、資料庫的讀取和寫入等,有些操作可能花費的時間很多,一兩個毫秒就可以搞定,有的卻很耗時,可能需要幾十毫秒,比如:
從一個網路連接中讀取數據 寫數據到硬碟中 調用其它服務,等待服務結果的返回 ……
這才是我們常用的case,而不是一個簡單的寫字元串。
因此那個測試框架的Handler還應該加入時間花費的情況。
模擬真實的Handler的情況
我們模擬一下真實的情況,看看Iris框架和Golang內置的Http路由框架的性能如何。
首先使用Iris實現一個Http Server:
packagemainimport(“os””strconv””time””github.com/kataras/iris”)funcmain() { api := iris.New() api.Get(“/rest/hello”,func(c *iris.Context) { sleepTime, _ := strconv.Atoi(os.Args[1])ifsleepTime 0{ time.Sleep(time.Duration(sleepTime) * time.Millisecond) } c.Text(“Hello world”) }) api.Listen(“:8080”)}
我們可以傳遞給它一個時間花費的參數sleepTime,模擬這個Handler在處理業務時要花費的時間,它會讓處理這個Handler的暫停sleepTime毫秒,如果為0,則不需要暫停,這種情況類似上面的測試。
然後我們使用Go內置的路由功能實現一個Http Server:
packagemainimport(“log””net/http””os””strconv””time”)// There are some golang RESTful libraries and mux libraries but i use the simplest to test.funcmain() { http.HandleFunc(“/rest/hello”,func(w http.ResponseWriter, r *http.Request) { sleepTime, _ := strconv.Atoi(os.Args[1])ifsleepTime 0{ time.Sleep(time.Duration(sleepTime) * time.Millisecond) } w.Write([]byte(“Hello world”)) }) err := http.ListenAndServe(“:8080”,nil)iferr !=nil{ log.Fatal(“ListenAndServe: “, err) }}
編譯兩個程序進行測試。
1、首先進行業務邏輯時間花費為0的測試
運行程序 iris 0,然後執行 wrk -t16 -c100 -d30s 進行並發100,持續30秒的測試。
iris的吞吐率為46155 requests/second。
運行程序 gomux 0,然後執行 wrk -t16 -c100 -d30s 進行並發100,持續30秒的測試。
Go內置的路由程序的吞吐率為55944 requests/second。
兩者的吞吐量差別不大,iris略差一點
2、然後進行業務邏輯時間花費為10的測試
運行程序 iris 10,然後執行 wrk -t16 -c100 -d30s 進行並發100,持續30秒的測試。
iris的吞吐率為97 requests/second。
運行程序 gomux 10,然後執行 wrk -t16 -c100 -d30s 進行並發100,持續30秒的測試。
Go內置的路由程序的吞吐率為9294 requests/second。
3、最後進行業務邏輯時間花費為1000的測試
這次模擬一個極端的情況,業務處理很慢,處理一個業務需要1秒的時間。
運行程序 iris 1000,然後執行 wrk -t16 -c100 -d30s 進行並發100,持續30秒的測試。
iris的吞吐率為1 requests/second。
運行程序 gomux 1000,然後執行 wrk -t16 -c100 -d30s 進行並發100,持續30秒的測試。
Go內置的路由程序的吞吐率為95 requests/second。
可以看到,如果加上業務邏輯的處理時間,Go內置的路由功能要遠遠好於Iris, 甚至可以說Iris的路由根本無法應用的有業務邏輯的產品中,隨著業務邏輯的時間耗費加大,iris的吞吐量急劇下降。
而對於Go的內置路由來說,業務邏輯的時間耗費加大,單個client會等待更長的時間,但是並發量大的網站來說,吞吐率不會下降太多。
比如我們用1000的並發量測試 gomux 10和 gomux 1000。
gomux 10: 吞吐率為47664 gomux 1000: 吞吐率為979
這才是Http網站真實的情況,因為我們要應付的網站的並發量,網站應該支持同時有儘可能多的用戶訪問,即使單個用戶得到返回頁面需要上百毫秒也可以接受。
而Iris在業務邏輯的處理時間增大的情況下,無法支持大的吞吐率,即使在並發量很大的情況下(比如1000),吞吐率也很低。
深入了解Go http server的實現
Go http server實現的是每個request對應一個goroutine (goroutine per request), 考慮到Http Keep-Alive的情況,更準確的說是每個連接對應一個goroutine(goroutine per connection)。
因為goroutine是非常輕量級的,不會像Java那樣 Thread per request會導致伺服器資源不足,無法創建很多的Thread, Golang可以創建足夠多的goroutine,所以goroutine per request的方式在Golang中沒有問題。而且這還有一個好處,因為request是在一個goroutine中處理的,不必考慮對同一個Request/Response並發讀寫的問題。
如何查看Handler是在哪一個goroutine中執行的呢?我們需要實現一個函數來獲取goroutine的Id:
funcgoID()int{varbuf[64]byte n := runtime.Stack(buf[:], false) idField := strings.Fields(strings.TrimPrefix(string(buf[:n]),”goroutine “))[0] id, err := strconv.Atoi(idField)iferr !=nil{panic(fmt.Sprintf(“cannot get goroutine id: %v”, err)) }returnid}
然後在handler中列印出當前的goroutine id:
func(c *iris.Context) { fmt.Println(goID()) ……}
和
func(w http.ResponseWriter, r *http.Request) { fmt.Println(goID()) ……}
啟動 gomux 0,然後運行 ab -c 5 -n 5 測試一下,apache的ab命令使用5個並發並且每個並發兩個請求訪問伺服器。
可以看到伺服器的輸出:
因為沒有指定 -k參數,每個client發送兩個請求會創建兩個連接。
你可以加上 -k參數,可以看出會有重複的goroutine id出現,表明同一個持久連接會使用同一個goroutine處理。
以上是通過實驗驗證我們的理論,下面是代碼分析。
net/http/server.go的 第2146行 go c.serve()表明,對於一個http連接,會啟動一個goroutine:
func(srv *Server) Serve(l net.Listener) error {deferl.Close()iffn := testHookServerServe; fn !=nil{ fn(srv, l) }vartempDelay time.Duration// how long to sleep on accept failureiferr := srv.setupHTTP2(); err !=nil{returnerr }for{ rw, e := l.Accept() …… tempDelay =0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can returngoc.serve() }}
而這個 c.serve方法會從連接中 讀取request交由handler處理:
func(c *conn) serve() { ……for{ w, err := c.readRequest() …… req := w.req serverHandler{c.server}.ServeHTTP(w, w.req)ifc.hijacked() {return } w.finishRequest()if!w.shouldReuseConnection() {ifw.requestBodyLimitHit || w.closedRequestBodyEarly() { c.closeWriteAndWait() }return } c.setState(c.rwc, StateIdle) }}
而 ServeHTTP的實現如下,如果沒有配置handler或者路由器,則使用預設的 DefaultServeMux。
func(sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handlerifhandler ==nil{ handler = DefaultServeMux }ifreq.RequestURI ==”*” req.Method ==”OPTIONS”{ handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req)}
可以看出這裡並沒有新開goroutine,而是在同一個connection對應的goroutine中執行的。如果試用Keep-Alive,還是在這個connection對應的goroutine中執行。
正如注釋中所說的那樣:
// HTTP cannot have multiple simultaneous active requests.[*] // Until the server replies to this request, it can’t read another, // so we might as well run the handler in this goroutine. // [*] Not strictly true: HTTP pipelining. We could let them all process // in parallel even if their responses need to be serialized. serverHandler{c.server}.ServeHTTP(w, w.req)
因此業務邏輯的時間花費會影響單個goroutine的執行時間,並且反映到客戶的瀏覽器是是延遲時間latency增大了,如果並發量足夠多,影響的是系統中的goroutine的數量以及它們的調度,吞吐率不會劇烈影響。
Iris的分析
如果你使用Iris查看每個Handler是使用哪一個goroutine執行的,會發現每個連接也會用不同的goroutine執行,可是性能差在哪兒呢?
或者說,是什麼原因導致Iris的性能急劇下降呢?
Iris伺服器的監聽和為連接啟動一個goroutine沒有什麼明顯不同,重要的不同在與Router處理Request的邏輯。
原因在於Iris為了提供性能,緩存了context,對於相同的請求url和method,它會從緩存中使用相同的context。
func(r *MemoryRouter) ServeHTTP(res http.ResponseWriter, req *http.Request) {ifctx := r.cache.GetItem(req.Method, req.URL.Path); ctx !=nil{ ctx.Redo(res, req)return } ctx := r.getStation().pool.Get().(*Context) ctx.Reset(res, req)ifr.processRequest(ctx) {//if something found and served then add it’s clone to the cache r.cache.AddItem(req.Method, req.URL.Path, ctx.Clone()) } r.getStation().pool.Put(ctx)}
由於並發量較大的時候,多個client的請求都會進入到上面的 ServeHTTP方法中,導致相同的請求會進入下面的邏輯:
ifctx := r.cache.GetItem(req.Method, req.URL.Path); ctx !=nil{ ctx.Redo(res, req)return}
ctx.Redo(res, req)導致不斷循環,直到每個請求處理完畢,將context放回到池子中。
所以對於Iris來說,並發量大的情況下,對於相同的請求(req.URL.Path和Method相同)會進入排隊的狀態,導致性能低下。
golang 有哪些比較穩定的 web 開發框架
第一個:Beego框架
Beego框架是astaxie的GOWeb開發的開源框架。Beego框架最大的特點是由八個大的基礎模塊組成,八大基礎模塊的特點是可以根據自己的需要進行引入,模塊相互獨立,模塊之間耦合性低。
相應的Beego的缺點就是全部使用時比較臃腫,通過bee工具來構建項目時,直接生成項目目錄和耦合關係,從而會導致在項目開發過程中受制性較大。
第二個:Gin框架
Gin是一個GOlang的微框架,封裝比較優雅,API友好,源碼注釋比較明確,已經發布了1.0版本;具有快速靈活、容錯方便等特點,其實對於golang而言,web框架的依賴遠比Python、Java更小。
目前在很多使用golang的中小型公司中進行業務開發,使用Gin框架的很多,大家如果想使用golang進行熟練Web開發,可以多關注一下這個框架。
第三個:Iris框架
Iris框架在其官方網站上被描述為GO開發中最快的Web框架,並給出了多框架和多語言之前的性能對比。目前在github上,Iris框架已經收穫了14433個star和1493個fork,可見是非常受歡迎的。
在實際開發中,Iris框架與Gin框架的學習曲線幾乎相同,所以掌握了Gin就可以輕鬆掌握Iris框架。
第四個:Echo框架
也是golang的微型Web框架,其具備快速HTTP路由器、支持擴展中間件,同時還支持靜態文件服務、Websocket以及支持制定綁定函數,制定相應渲染函數,並允許使用任意的HTML模版引擎。
GoLang — Gin框架
• 何為框架:
框架一直是敏捷開發中的利器,能讓開發者很快的上手並做出應用,甚至有的時候,脫離了框架,一些開發者都不會寫程序了。成長總不會一蹴而就,從寫出程序獲取成就感,再到精通框架,快速構造應用,當這些方面都得心應手的時候,可以嘗試改造一些框架,或是自己創造一個。
Gin是一個golang的微框架,封裝比較優雅,API友好,源碼注釋比較明確,已經發布了1.0版本。具有快速靈活,容錯方便等特點。其實對於golang而言,web框架的依賴要遠比Python,Java之類的要小。自身的net/http足夠簡單,性能也非常不錯。框架更像是一些常用函數或者工具的集合。藉助框架開發,不僅可以省去很多常用的封裝帶來的時間,也有助於團隊的編碼風格和形成規範。
(1)首先需要安裝,安裝比較簡單,使用go get即可
go get github.com/gin-gonic/gin
如果安裝失敗,直接去Github clone下來,放置到對應的目錄即可。
(2)代碼中使用:
下面是一個使用Gin的簡單例子:
package main
import (
“github.com/gin-gonic/gin”
)
func main() {
router := gin.Default()
router.GET(“/ping”, func(c *gin.Context) {
c.JSON(200, gin.H{
“message”: “pong”,
})
})
router.Run(“:8080”) // listen and serve on 0.0.0.0:8080
}
簡單幾行代碼,就能實現一個web服務。使用gin的Default方法創建一個路由handler。然後通過HTTP方法綁定路由規則和路由函數。不同於net/http庫的路由函數,gin進行了封裝,把request和response都封裝到gin.Context的上下文環境。最後是啟動路由的Run方法監聽埠。麻雀雖小,五臟俱全。當然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。
Gin可以很方便的支持各種HTTP請求方法以及返回各種類型的數據,詳情可以前往查看。
2.1 匹配參數
我們可以使用Gin框架快速的匹配參數,如下代碼所示:
冒號:加上一個參數名組成路由參數。可以使用c.Param的方法讀取其值。當然這個值是字串string。諸如/user/rsj217,和/user/hello都可以匹配,而/user/和/user/rsj217/不會被匹配。
瀏覽器輸入以下測試:
返回結果為:
其中c.String是gin.Context下提供的方法,用來返回字元串。
其中c.Json是gin.Context下提供的方法,用來返回Json。
下面我們使用以下gin提供的Group函數,方便的為不同的API進行分類。
我們創建了一個gin的默認路由,並為其分配了一個組 v1,監聽hello請求並將其路由到視圖函數HelloPage,最後綁定到 0.0.0.0:8000
C.JSON是Gin實現的返回json數據的內置方法,包含了2個參數,狀態碼和返回的內容。http.StatusOK代表返回狀態碼為200,正文為{“message”: 「welcome”}。
註:Gin還包含更多的返回方法如c.String, c.HTML, c.XML等,請自行了解。可以方便的返回HTML數據
我們在之前的組v1路由下新定義一個路由:
下面我們訪問
可以看到,通過c.Param(「key」)方法,Gin成功捕獲了url請求路徑中的參數。同理,gin也可以捕獲常規參數,如下代碼所示:
在瀏覽器輸入以下代碼:
通過c.Query(「key」)可以成功接收到url參數,c.DefaultQuery在參數不存在的情況下,會由其默認值代替。
我們還可以為Gin定義一些默認路由:
這時候,我們訪問一個不存在的頁面:
返回如下所示:
下面我們測試在Gin裡面使用Post
在測試端輸入:
附帶發送的數據,測試即可。記住需要使用POST方法.
繼續修改,將PostHandler的函數修改如下
測試工具輸入:
發送的內容輸入:
返回結果如下:
備註:此處需要指定Content-Type為application/x-www-form-urlencoded,否則識別不出來。
一定要選擇對應的PUT或者DELETE方法。
Gin框架快速的創建路由
能夠方便的創建分組
支持url正則表達式
支持參數查找(c.Param c.Query c.PostForm)
請求方法精準匹配
支持404處理
快速的返回給客戶端數據,常用的c.String c.JSON c.Data
golang反射框架Fx
Fx是一個golang版本的依賴注入框架,它使得golang通過可重用、可組合的模塊化來構建golang應用程序變得非常容易,可直接在項目中添加以下內容即可體驗Fx效果。
Fx是通過使用依賴注入的方式替換了全局通過手動方式來連接不同函數調用的複雜度,也不同於其他的依賴注入方式,Fx能夠像普通golang函數去使用,而不需要通過使用struct標籤或內嵌特定類型。這樣使得Fx能夠在很多go的包中很好的使用。
接下來會提供一些Fx的簡單demo,並說明其中的一些定義。
1、一般步驟
大致的使用步驟就如下。下面會給出一些完整的demo
2、簡單demo
將io.reader與具體實現類關聯起來
輸出:
3、使用struct參數
前面的使用方式一旦需要進行注入的類型過多,可以通過struct參數方式來解決
輸出
如果通過Provide提供構造函數是生成相同類型會有什麼問題?換句話也就是相同類型擁有多個值呢?
下面兩種方式就是來解決這樣的問題。
4、使用struct參數+Name標籤
在Fx未使用Name或Group標籤時不允許存在多個相同類型的構造函數,一旦存在會觸發panic。
輸出
上面通過Name標籤即可完成在Fx容器注入相同類型
5、使用struct參數+Group標籤
使用group標籤同樣也能完成上面的功能
輸出
基本上Fx簡單應用在上面的例子也做了簡單講解
1、Annotated(位於annotated.go文件) 主要用於採用annotated的方式,提供Provide注入類型
源碼中Name和Group兩個欄位與前面提到的Name標籤和Group標籤是一樣的,只能選其一使用
2、App(位於app.go文件) 提供注入對象具體的容器、LiftCycle、容器的啟動及停止、類型變數及實現類注入和兩者映射等操作
至於Provide和Populate的源碼相對比較簡單易懂在這裡不在描述
具體源碼
3、Extract(位於extract.go文件)
主要用於在application啟動初始化過程通過依賴注入的方式將容器中的變數值來填充給定的struct,其中target必須是指向struct的指針,並且只能填充可導出的欄位(golang只能通過反射修改可導出並且可定址的欄位),Extract將被Populate代替。 具體源碼
4、其他
諸如Populate是用來替換Extract的,而LiftCycle和inout.go涉及內容比較多後續會單獨提供專屬文件說明。
在Fx中提供的構造函數都是惰性調用,可以通過invocations在application啟動來完成一些必要的初始化工作:fx.Invoke(function); 通過也可以按需自定義實現LiftCycle的Hook對應的OnStart和OnStop用來完成手動啟動容器和關閉,來滿足一些自己實際的業務需求。
Fx框架源碼解析
主要包括app.go、lifecycle.go、annotated.go、populate.go、inout.go、shutdown.go、extract.go(可以忽略,了解populate.go)以及輔助的internal中的fxlog、fxreflect、lifecycle
golang性能測試框架k6源碼分析
k6是新興的性能測試框架,比肩jmeter,另外測試腳本使用js,更加適合自動化的架構。
k6啟動的框架是使用golang的cli標準框架cobra,入口函數
進入cobra框架後,我們直接查看getRunCmd,這個是命令run的入口,主要工作都是從這裡開始。
重點關注初始化Runner,這個是通過js腳本,使用goja庫解析後,生成的實際執行單元。
進入js目錄,查看Runner的結構,runner.go
Runner有一些配置屬性,另外還有方法,方法用lib.Runner的介面進行規範。
Runner有一個NewVU方法,裡面定義了連接參數,實現api測試
返回主函數,在初始化完成Runner後,啟動調度器,以及做結果收集
最終封裝成一個engine
啟動測試
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/238235.html