這是 Cloudflare 的 Filippo Valsorda 2016年發表在Gopher Academy的一篇文章, 雖然過去兩年了,但是依然很有意義。
先前 crypto/tls
太慢而net/http
也很年輕, 所以對於Go web server來說, 通常我們明智的做法把它放在反向 Proxy的後面, 如nginx等,現在不需要了。
在Cloudflare我們最近實驗了直接暴漏純Go的服務作為主機。 Go 1.8的net/http
和 crypto/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},} |
如果你想配置相容性, 你可以設定MinVersion
和CipherSuites
。
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/autocert
的GetCertificate
函數。
不要忘了將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.1
和 HTTP/2
。你一定已經熟悉了Handler的開發,所以本文不討論它。我們討論區伺服器端背後的一些情境。
Timeout
逾時可能是最容易忽略的危險的情境。你的服務可能在受控網路中倖免於難,但是在互連網上就不會那麼幸運了, 特別是(不僅僅)受到惡意攻擊。
有一系列的資源需要逾時控制。儘管goroutine消耗很少,但檔案描述符總是有限的。卡住的串連、不工作的串連甚至惡意斷掉的串連不應該消耗它們。
一個超過最大檔案符的伺服器總是不能接受新的串連, 會報下面的失敗:
1 |
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
,你可以顯示地設定它們:
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串連時,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的部落格。
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.Listener
給Serve
不同,這個方法使用預設值提供了零保護措施), 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版本。