這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
依照我的前一篇文章(超全的Go Http路由架構效能比較)對各種Go http路由架構的比較, Iris明顯勝出,它的效能遠遠超過其它Golang http路由架構。
但是,在真實的環境中,Iris真的就是最快的Golang http路由架構嗎?
2016-04-05 更新: 我已經提交了一個Bug, 作者Makis已經做了一個臨時的解決方案,效能已經恢複,所以準備使用Iris的讀者不必擔心。
根據我的測試,最新的Iris的測試如下:
- 在商務邏輯需要10毫秒時,吞吐率可以達到9281 request/s
- 在商務邏輯需要1000毫秒時,吞吐率可以達到95 request/s
效能已經很不錯了。
我會做一個其它路由架構的測試,看看其它的架構是否也有本文所說的問題。
Benchmark測試分析
在那篇文章中我使用的是Julien Schmidt的測試代碼,他類比了靜態路由、Github API、Goolge+ API、Parse API的各種情況,因為這些API是知名網站的開放的API,看起來測試挺真實可靠的。
但是,這個測試存在著一個嚴重的問題,就是Handler的商務邏輯非常的簡單,各個架構的handler類似,比如Iris的Handler的實現:
123456789 |
func irisHandler(_ *iris.Context) {}func irisHandlerWrite(c *iris.Context) {io.WriteString(c.ResponseWriter, c.Param("name"))}func irisHandlerTest(c *iris.Context) {io.WriteString(c.ResponseWriter, c.Request.RequestURI)} |
幾乎沒有任何的商務邏輯,最多是往Response中寫入一個字串。
這和生產環境中的情況嚴重不符!
實際的產品肯定會有一些業務的處理,比如參數的校正,資料的計算,本地檔案的讀取、遠程服務的調用、緩衝的讀取、資料庫的讀取和寫入等,有些操作可能花費的時間很多,一兩個毫秒就可以搞定,有的卻很耗時,可能需要幾十毫秒,比如:
- 從一個網路連接中讀取資料
- 寫資料到硬碟中
- 調用其它服務,等待服務結果的返回
- ……
這才是我們常用的case,而不是一個簡單的寫字串。
因此那個測試架構的Handler還應該加入時間花費的情況。
類比真實的Handler的情況
我們類比一下真實的情況,看看Iris架構和Golang內建的Http路由架構的效能如何。
首先使用Iris實現一個Http Server:
12345678910111213141516171819202122 |
package mainimport ("os""strconv""time""github.com/kataras/iris")func main() {api := iris.New()api.Get("/rest/hello", func(c *iris.Context) {sleepTime, _ := strconv.Atoi(os.Args[1])if sleepTime > 0 {time.Sleep(time.Duration(sleepTime) * time.Millisecond)}c.Text("Hello world")})api.Listen(":8080")} |
我們可以傳遞給它一個時間花費的參數sleepTime,類比這個Handler在處理業務時要花費的時間,它會讓處理這個Handler的暫停sleepTime毫秒,如果為0,則不需要暫停,這種情況類似上面的測試。
然後我們使用Go內建的路由功能實現一個Http Server:
1234567891011121314151617181920212223242526 |
package mainimport ("log""net/http""os""strconv""time")// There are some golang RESTful libraries and mux libraries but i use the simplest to test.func main() {http.HandleFunc("/rest/hello", func(w http.ResponseWriter, r *http.Request) {sleepTime, _ := strconv.Atoi(os.Args[1])if sleepTime > 0 {time.Sleep(time.Duration(sleepTime) * time.Millisecond)}w.Write([]byte("Hello world"))})err := http.ListenAndServe(":8080", nil)if err != nil {log.Fatal("ListenAndServe: ", err)}} |
編譯兩個程式進行測試。
1、首先進行商務邏輯時間花費為0的測試
運行程式iris 0,然後執行wrk -t16 -c100 -d30s http://127.0.0.1:8080/rest/hello進行並發100,持續30秒的測試。
iris的吞吐率為46155 requests/second。
運行程式gomux 0,然後執行wrk -t16 -c100 -d30s http://127.0.0.1:8080/rest/hello進行並發100,持續30秒的測試。
Go內建的路由程式的吞吐率為55944 requests/second。
兩者的輸送量差別不大,iris略差一點
2、然後進行商務邏輯時間花費為10的測試
運行程式iris 10,然後執行wrk -t16 -c100 -d30s http://127.0.0.1:8080/rest/hello進行並發100,持續30秒的測試。
iris的吞吐率為97 requests/second。
運行程式gomux 10,然後執行wrk -t16 -c100 -d30s http://127.0.0.1:8080/rest/hello進行並發100,持續30秒的測試。
Go內建的路由程式的吞吐率為9294 requests/second。
3、最後進行商務邏輯時間花費為1000的測試
這次類比一個極端的情況,業務處理很慢,處理一個業務需要1秒的時間。
運行程式iris 1000,然後執行wrk -t16 -c100 -d30s http://127.0.0.1:8080/rest/hello進行並發100,持續30秒的測試。
iris的吞吐率為1 requests/second。
運行程式gomux 1000,然後執行wrk -t16 -c100 -d30s http://127.0.0.1:8080/rest/hello進行並發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:
12345678910 |
func goID() int {var buf [64]byten := runtime.Stack(buf[:], false)idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]id, err := strconv.Atoi(idField)if err != nil {panic(fmt.Sprintf("cannot get goroutine id: %v", err))}return id} |
然後在handler中列印出當前的goroutine id:
1234 |
func(c *iris.Context) {fmt.Println(goID())……} |
和
1234 |
func(w http.ResponseWriter, r *http.Request) {fmt.Println(goID())……} |
啟動gomux 0,然後運行ab -c 5 -n 5 http://localhost:8080/rest/hello測試一下,apache的ab命令使用5個並發並且每個並發兩個請求訪問伺服器。
可以看到伺服器的輸出:
12345678910 |
21181719203335363734 |
因為沒有指定-k參數,每個client發送兩個請求會建立兩個串連。
你可以加上-k參數,可以看出會有重複的goroutine id出現,表明同一個持久串連會使用同一個goroutine處理。
以上是通過實驗驗證我們的理論,下面是程式碼分析。
net/http/server.go的第2146行 go c.serve()表明,對於一個http串連,會啟動一個goroutine:
123456789101112131415161718 |
func (srv *Server) Serve(l net.Listener) error {defer l.Close()if fn := testHookServerServe; fn != nil {fn(srv, l)}var tempDelay time.Duration // how long to sleep on accept failureif err := srv.setupHTTP2(); err != nil {return err}for {rw, e := l.Accept()……tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew) // before Serve can returngo c.serve()}} |
而這個c.serve方法會從串連中讀取request交由handler處理:
1234567891011121314151617181920 |
func (c *conn) serve() {……for {w, err := c.readRequest()……req := w.reqserverHandler{c.server}.ServeHTTP(w, w.req)if c.hijacked() {return}w.finishRequest()if !w.shouldReuseConnection() {if w.requestBodyLimitHit || w.closedRequestBodyEarly() {c.closeWriteAndWait()}return}c.setState(c.rwc, StateIdle)}} |
而ServeHTTP的實現如下,如果沒有配置handler或者路由器,則使用預設的DefaultServeMux。
12345678910 |
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerif handler == nil {handler = DefaultServeMux}if req.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。
12345678910111213141516 |
func (r *MemoryRouter) ServeHTTP(res http.ResponseWriter, req *http.Request) {if ctx := 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)if r.processRequest(ctx) {//if something found and served then add it's clone to the cacher.cache.AddItem(req.Method, req.URL.Path, ctx.Clone())}r.getStation().pool.Put(ctx)} |
由於並發量較大的時候,多個client的請求都會進入到上面的ServeHTTP方法中,導致相同的請求會進入下面的邏輯:
1234 |
if ctx := 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相同)會進入排隊的狀態,導致效能低下。
參考資料
- https://blog.golang.org/context
- https://www.reddit.com/r/golang/comments/3xz1f3/go_http_server_and_go_routines/
- http://screamingatmyscreen.com/2013/6/http-request-and-goroutines/
- https://groups.google.com/forum/#!topic/golang-nuts/iwCz_pqu8R4
- https://groups.google.com/forum/#!topic/golang-nuts/ic3FxWZRyHs