iris 真的是最快的Golang 路由架構嗎?

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

依照我的前一篇文章(超全的Go Http路由架構效能比較)對各種Go http路由架構的比較, Iris明顯勝出,它的效能遠遠超過其它Golang http路由架構。

但是,在真實的環境中,Iris真的就是最快的Golang http路由架構嗎?

2016-04-05 更新: 我已經提交了一個Bug, 作者Makis已經做了一個臨時的解決方案,效能已經恢複,所以準備使用Iris的讀者不必擔心。
根據我的測試,最新的Iris的測試如下:

  1. 在商務邏輯需要10毫秒時,吞吐率可以達到9281 request/s
  2. 在商務邏輯需要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 10gomux 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相同)會進入排隊的狀態,導致效能低下。

參考資料

  1. https://blog.golang.org/context
  2. https://www.reddit.com/r/golang/comments/3xz1f3/go_http_server_and_go_routines/
  3. http://screamingatmyscreen.com/2013/6/http-request-and-goroutines/
  4. https://groups.google.com/forum/#!topic/golang-nuts/iwCz_pqu8R4
  5. https://groups.google.com/forum/#!topic/golang-nuts/ic3FxWZRyHs

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.