[譯]Go HttpServer 最佳實務

來源:互聯網
上載者:User

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

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

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

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

crypto/tls

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

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

12345678910
&tls.Config{// Causes servers to use Go's default ciphersuite preferences,// which are tuned to avoid attacks. Does nothing on clients.PreferServerCipherSuites: true,// Only use curves which have assembly implementationsCurvePreferences: []tls.CurveID{tls.CurveP256,tls.X25519, // Go 1.8 only},}

如果你想配置相容性, 你可以設定MinVersionCipherSuites

1234567891011121314
MinVersion: tls.VersionTLS12,CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 onlytls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,   // Go 1.8 onlytls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,// Best disabled, as they don't provide Forward Secrecy,// but might be necessary for some clients// tls.TLS_RSA_WITH_AES_256_GCM_SHA384,// tls.TLS_RSA_WITH_AES_128_GCM_SHA256,   },

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

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

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

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

12345678910
srv := &http.Server{ReadTimeout:  5 * time.Second,WriteTimeout: 5 * time.Second,Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {w.Header().Set("Connection", "close")url := "https://" + req.Host + req.URL.String()http.Redirect(w, req, url, http.StatusMovedPermanently)}),}go func() { log.Fatal(srv.ListenAndServe()) }()

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

net/http

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

Timeout

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

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

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

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

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

http.Server有三個參數控制timeout: ReadTimeout, WriteTimeoutIdleTimeout,你可以顯示地設定它們:

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

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

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

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

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

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

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

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

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

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

HTTP/2

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

  • 請求通過TLS/HTTPS
  • Server.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。

TCP Keep-Alives

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

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

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

最後,我的經驗是串連往往會導致泄漏,知道逾時起作用。

ServeMux

包層級的http.Handle[Func] (和你的web架構)註冊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

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)

Metrics

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

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

結論

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.