[Go語言] 面向外網的Web調優詳解(go1.8)

很早以前crypto/tls(TLS長連接庫)和net/http的性能不敢恭維,因此我們都使用Nginx做反向代理,但是Go1.8將要來了,這種格局即將被打破了!

我們最近嘗試性的將Go1.8編譯的服務暴漏到了外網,結果發現crypto/tls 和net/http都得到了極大的提升:穩定性、性能以及服務的可伸縮性!


crypto/tls

現在已經是2016年了,我們不可能再去裸奔在互聯網了,因此基於TLS是必然的選擇,所以我們需要crypto/tls這個庫。好消息就是在1.8下,該庫的性能得到了很大的提升,性能表現堪稱十分優秀,而且安全性也非常出色。

默認推薦的配置類似Mozilla標準,然而我們應該要設置PreferServerCipherSuits爲true,這樣可以使用更安全更快速的密文族;設置CurvePreferences避免未優化的Curve;選擇CurveP256而不是CurveP384,因爲後者可能會爲每個客戶端消耗將近1秒的cpu時間!!

&tls.Config{
	PreferServerCipherSuites: true,
	// 僅僅使用擁有彙編實現的Curve
	CurvePreferences: []tls.CurveID{
		tls.CurveP256,
		tls.X25519, // Go 1.8 only
	},
}
如果可以接受TLS兼容性上可能存在的問題(例如版本問題,下面的配置建議更現代化,因此對老版本可能不夠兼容),還可以設置MinVersion和CipherSuites
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 only
		tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,   // Go 1.8 only
		tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
		tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,

		// 最好禁用下面的參數,因爲沒有提供向前的安全性,但是對於部分客戶端可能需要開啓
		// tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
		// tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
	},

Go的CBC密碼族實現在Lucky13攻擊下,不夠穩定,因此我們在上面的配置中禁用了CBC密碼族,雖然go1.8已經進行了改善。


注意!上述的優化只針對amd64架構,在此架構下,我們甚至可以考慮cloudflare公司的開源的性能極高的加密版本(AES-GCM,Chacha20-Poly2305,P256)。


當然,我們還需要證書,這裏我們可以使用golang.org/x/crypto/acme/autocert和Letss Encrypt,同時別忘了將http請求重定向到https,如果你的客戶端是瀏覽器,還可以考慮HSTS.

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來檢查我們的TLS是否正確。


net/http

net/http是一個成熟的HTTP1.1和HTTP2協議棧,具體怎麼用,這裏就不贅述了,我們來講講服務器端背後的故事。

TImeouts

在外網環境中,這個參數是最重要的也是最容易被忽視的之一!你的後端服務如果不設置超時,在內網環境可能還Ok,但是到了外網環境,那就是災難,特別是在遇到攻擊時。

Timeouts的應用是一種資源控制,就算goroutine很廉價,但是文件描述符fd很昂貴的,一個不再工作或者長閒置的連接是不該去佔用寶貴的fd的。

當服務器的fd不夠用時,在accept新連接時就會失敗,報錯如下:

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

默認的net/http的http.Server,可以通過http.ListenAndServe和http.ListenAndServeTLS創建,是沒有timeouts的,這完全不是我們想要的。

如上圖所示,http.Server主要有三種timeouts,ReadTimeout,WriteTimeout,IdleTimeout,我們可以這樣設置:

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

ReadTimeout是從連接accept一直到所有請求的body被完全讀取(如果不讀body,那就是所有header被讀取)。該超時是net/http包在連接accept之後直接設置SetReadDeadline的。

ReadTimeout存在一個問題,服務器沒有給更多的時間來流式處理來自客戶端的數據。因此Go1.8引入了ReadHeaderTimeout,這裏的超時僅僅針對Header的讀取超時,

當然這個沒有解決根本問題,因此新的解決方案在issue#16100有進一步的討論,關於怎麼在Handler中處理ReadTimeout。


WriteTimeout是從包頭讀取成功開始,一直到回覆(response)的寫入,是在readRequest的末尾調用SetWriteDeadline函數實現的。


當連接是HTTPS時,SetWriteDeadline會在連接accept後立刻調用一次,這裏是處理TLS的握手超時。因此,這次超時是在HTTP包頭讀取或者等待第一個字節傳輸之前結束。

和ReadTimeout一樣,WriteTimeout也無法從Handler中進行相對控制:issue#16100


最後是IdleTimeout,這個是在Go1.8引入的一個很有用的參數,用來控制服務器端KeepAlive的連接允許空閒的最大時間。在go1.8之前,ReadTimeout有一個很大的問題,對於Keepalive的連接是不友好的(儘管可以在應用層來解決Idle的超時問題):因爲在上一個請求的讀取完畢後,下一個請求的ReadTimeout會立即開始重新計時,這樣連接空閒的時間也算在ReadTimeout內,造成了連接的過早斷開。


綜上所述,當我們在Go1.8中處理外部不受信任的連接時,我們要設置上這三個超時,這樣客戶端就不會因爲各種過慢的寫或者讀,一直霸佔連接了。


HTTP2

在Go1.6版本及之後,HTTP2會自動開啓,當且僅當:

1.請求是基於TLS/HTTPS的

2.Server.TLSNextProto設置爲nil(注意,如果設置爲空map,那會禁用HTTP2)

3.Server.TLSConfig被設置並且ListenAndServerTLS被使用;或者,使用Serve,同時tls.Config.NextProtos包含了"h2",例如[]string{"h2","http/1.1"}


在Go1.8版本修復了一個關於HTTP2的ReadTimeout的Bug,再結合1.8的其它特性,我的建議是儘快升級1.8。


TCP KeepAlive

如果你在用ListenAndServe(與此相對的是給Serve傳一個net.Listener參數,但是這種方式沒有做任何防護),那麼三分鐘長的TCP keepalive時間將自動被設置

,該Keepalive值是爲了防止客戶端莫名其妙消失後,連接再也不會被關閉的bug。我的建議是將該時間設置得更短,防止服務器被攻擊。

如果你用的是TCP長連接服務,那麼你該使用net.ListenTCP,同時設置keepalive時間,根據我的經驗,如果不設置這個,那麼長連接存在泄漏的風險,後面我會詳細寫一篇文章分析TCP連接泄漏的問題。



Metrics

我們可以用Server.ConnState來獲取連接的狀態,注意,我們要自己維護map[net.Conn]ConnState。



總結

以後再也不用在Go的Web服務前再前置一個Nginx了,節省了服務器同時也降低了請求的延遲,前提是,我們使用了Go1.8。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章