Go HttpServer 最佳實踐

tianxiaoxu發表於2018-07-31

這是 Cloudflare 的 Filippo Valsorda 2006年發表在Gopher Academy的一篇文章, 雖然過去兩年了,但是依然很有意義。

先前 crypto/tls 太慢而net/http也很年輕, 所以對於Go web server來說, 通常我們明智的做法把它放在反向代理的後面, 如nginx等,現在不需要了。

在Cloudflare我們最近試驗了直接暴漏純Go的服務作為主機。 Go 1.8的net/http 和 crypto/tls 提供了穩定的、高效能並且靈活的功能。

然後,需要做一些調優的工作,本文我們將展示怎麼去調優和使web伺服器更穩定。

crypto/tls

2016年了,你不會再執行一個不加密的HTTP Server,所以你需要crypto/tls。好訊息使這個庫已經非常快了(我們的測試),目前他的安全攻擊追蹤也很優秀。

預設配置是使用Mozilla參考中的中級推薦配置,但是 你仍然應該設定PreferServerCipherSuites以確保採用更快更安全的密碼庫, CurvePreferences避免未優化的曲線。 客戶端如果使用CurveP384演算法回導致我們的機器多達1秒的cpu消耗。

如果你想配置相容性, 你可以設定MinVersion和CipherSuites。

注意Go的CBC加密套件的實現(上面我們禁用了)很容易收到 Lucky13攻擊, 即使Go 1.8實現了部分的處理。

最後需要注意的是, 所有這些建議僅適用 amd64架構因為它可以實現快速的常數級的加密原語(AES-GCM, ChaCha20-Poly1305, P256), 其它架構可能不適合產品級應用。

既然是服務要暴漏帶網際網路上, 它需要一個公開的可信的證書。通過Let’s Encrypt很容易申請, 可以使用golang.org/x/crypto/acme/autocert的GetCertificate函式。

不要忘了將HTTP重定向到HTTPS, 如果你的客戶端是瀏覽器的話,可以考慮 HSTS。

你可以使用SSL Labs test檢查配置是否正確。

net/http

net/http 包含 HTTP/1.1 和 HTTP/2。你一定已經熟悉了Handler的開發,所以本文不討論它。我們討論伺服器端背後的一些場景。

1、Timeout

超時可能是最容易忽略的危險的場景。你的服務可能在受控網路中倖免於難,但是在網際網路上就不會那麼幸運了, 特別是(不僅僅)受到惡意攻擊。

有一系列的資源需要超時控制。儘管goroutine消耗很少,但檔案描述符總是有限的。卡住的連線、不工作的連線甚至惡意斷掉的連線不應該消耗它們。

一個超過最大檔案符的伺服器總是不能接受新的連線, 會報下面的失敗:

http: Accept error: accept tcp [::]:80: accept: too many open files; retrying in 1s

一個預設的 http.Server, 就像包文件中的例子http.ListenAndServe 和 http.ListenAndServeTLS, 沒有設定任何超時控制, 你肯定不是你想要的。

在http.Server有三個引數控timeout: ReadTimeout, WriteTimeout 和 IdleTimeout,你可以顯示地設定它們:

srv := &http.Server{2ReadTimeout:5 * time.Second,3WriteTimeout: 10 * time.Second,4IdleTimeout:120 * time.Second,5TLSConfig:tlsConfig,6Handler:serveMux,7}8log.Println(srv.ListenAndServeTLS("", ""))

ReadTimeout的時間範圍起自連線備接受,止於請求的div完全讀出。在net/http的實現中它在連線Accept後通過SetReadDeadline設定。

ReadTimeout最大的問題它不允許伺服器給客戶端更多的時間去請求的div stream。 go 1.8新引入了一個引數ReadHeaderTimeout,它止於讀完請求頭。然後一直有一些不清楚的方式去設定讀超時,相關的設計討論可以參考#16100。

WriteTimeout超時正常起自讀完請求頭, 止於response寫完(也就是ServeHTTP的生命週期), 通過readRequest的結尾處的SetWriteDeadline設定。

然後,當通過HTTPS連線時,SetWriteDeadline在Accept後立即設定, 所以它也包含TLS握手的packet的寫。討厭的是,這意味著WriteTimeout包含http頭的讀以及第一個位元組的等待。

ReadTimeout和WriteTimeout是絕對值,無法在Handler中更改它(#16100)。

Go 1.8還新引入了IdleTimeout引數, 用來限制服務端Keep-Alive連線在重用前idle的數量。

Go 1.8之前的版本, ReadTimeout在請求完成後又立即開始滴答(tick),這對Keep-Alive連線是不合適的: idle time會消耗客戶端允許傳送請求的時間,導致一些快的客戶端會有不期望的超時。

對於不可信的客戶端和網路,你應該設定Read, Write 和 Idle超時, 這樣一個讀或者寫很慢的客戶端不會長時間佔用一個連線。

對於go 1.8之前的 HTTP/1.1超時的背景知識, 你可以參考Cloudflare的部落格。

2、HTTP/2

HTTP/2在 Go 1.6+中回自動啟用, 只要它滿足下面的條件:

請求通過TLS/HTTPSServer.TLSNextProto為nil (如果設定一個空的map,則禁止HTTP/2)Server.TLSConfig已被設定,ListenAndServeTLS被呼叫或者下一條Serve被呼叫,並且tls.Config.NextProtos包含h2 (比如[]string{"h2", "http/1.1")HTTP/2 和 HTTP/1.1有些不同,因為同一個連線同時會服務多個請求,但是Go抽象了統一的超時控制介面。

遺憾的是, Go 1.7中的ReadTimeout會打斷 HTTP/2 連線,它不會為每一個連線重置,而是在連線初次建立時就設定而不會重置,當超時後就會斷掉 HTTP/2連線。 Go 1.8 修復了這個問題。

基於此和ReadTimeout的idle time問題,我強烈建議你儘快升級到1.8。

3、TCP Keep-Alives

如果你使用ListenAndServe(與傳入net.Listener給Serve不同,這個方法使用預設值提供了零保護措施), 3分鐘的TCP Keep-Alive會自動設定,它會讓徹底消失的client有機會放棄連線, 我的經驗是不要完全相信它, 無論如何也要設定超時。

首先, 3分鐘太長了,你可以使用你自己的tcpKeepAliveListener調整它。、

更重要的是,Keep-Alive只是保證client還活著,但不會設定連線存活的上限。惡意攻擊的客戶端會開啟非常多的連線,導致你的伺服器開啟很多檔案描述符, 通過未完成的請求, 會導致你的服務拒絕服務。

最後,我的經驗是連線往往會導致洩漏,知道超時起作用。

4、ServeMux

包級別的http.HandleFunc註冊handler到全域性的http.DefaultServeMux, 如果Server.Handler是nil的話, 你應該避免這樣做。

任何你輸入的包,不管是直接的還是間接的,都可以訪問http.DefaultServeMux,可能會註冊你不期望的route。

例如,包依賴中有任何一個庫匯入了net/http/pprof,客戶端都能得到你的應用的CPU的profile。 你可以使用net/http/pprof手工註冊。

正確的是,初始化你自己的http.ServeMux,把handler註冊到它的上面, 設定它為Server.Handler, 或者設定你自己的web框架為Server.Handler。

5、Logging

net/http在呼叫你的handler之前做了大量的工作, 比如接受連線https://github.com/golang/go/blob/1106512db54fc2736c7a9a67dd553fc9e1fca742/src/net/http/server.go#L2631-L2653, TLS握手等等……

當任何一個步驟出錯,它會寫一行日誌到Server.ErrorLog。其中一些錯誤, 比如超時和連線重置, 在網際網路上是正常的。你可以連線大部分錯誤並把它們加入到metric中,這要歸功於這個保證:

Each logging operation makes a single call to the Writer’s Write method.

如果在handler中你不想輸出堆疊log, 你可以使用panic(nil)或者使用Go 1.8的panic(http.ErrAbortHandler)。

6、Metrics

metric可以幫助你監控開啟的檔案描述符。Prometheus使用proc檔案系統來幫助你完成這些。

如果你需要調研洩漏問題, 你可以使用Server.ConnState鉤子來得到更多的連線的細節metric。注意,不保持state就沒有方式能保持一個正確的StateActive數量,所以你需要維護一個map[net.Conn]ConnState。

7、結論

使用Nginx做Go服務前端的日誌一去不復返了, 但是面對網際網路你仍然需要做一些額外的防護措施, 可能需要升級到新的Go 1.8版本。

本文作者:Filippo Valsorda

本文來自雲棲社群合作伙伴“Golang語言社群”,瞭解相關資訊可以關注“Golang語言社群”

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31137683/viewspace-2168745/,如需轉載,請註明出處,否則將追究法律責任。