前言
很早以前crypto/tls(TLS長(zhǎng)連接庫(kù))和net/http的性能不敢恭維,因此我們都使用Nginx做反向代理,但是Go1.8將要來(lái)了,這種格局即將被打破了!
我們最近嘗試性的將Go1.8編譯的服務(wù)暴漏到了外網(wǎng),結(jié)果發(fā)現(xiàn)crypto/tls 和net/http都得到了極大的提升:穩(wěn)定性、性能以及服務(wù)的可伸縮性!
crypto/tls
現(xiàn)在已經(jīng)是2016年了,我們不可能再去裸奔在互聯(lián)網(wǎng)了,因此基于TLS是必然的選擇,所以我們需要crypto/tls這個(gè)庫(kù)。好消息就是在1.8下,該庫(kù)的性能得到了很大的提升,性能表現(xiàn)堪稱十分優(yōu)秀,而且安全性也非常出色。
默認(rèn)推薦的配置類似
[Mozilla標(biāo)準(zhǔn)] (https://wiki.mozilla.org/Security/Server_Side_TLS),然而我們應(yīng)該要設(shè)置PreferServerCipherSuits為true,這樣可以使用更安全更快速的密文族;設(shè)置CurvePreferences避免未優(yōu)化的Curve;選擇CurveP256而不是CurveP384,因?yàn)楹笳呖赡軙?huì)為每個(gè)客戶端消耗將近1秒的cpu時(shí)間??!
&tls.Config{
PreferServerCipherSuites: true,
// 僅僅使用擁有匯編實(shí)現(xiàn)的Curve
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519, // Go 1.8 only
},
}
如果可以接受TLS兼容性上可能存在的問(wèn)題(例如版本問(wèn)題,下面的配置建議更現(xiàn)代化,因此對(duì)老版本可能不夠兼容),還可以設(shè)置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,
// 最好禁用下面的參數(shù),因?yàn)闆](méi)有提供向前的安全性,但是對(duì)于部分客戶端可能需要開(kāi)啟
// tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
// tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
}
Go的CBC密碼族實(shí)現(xiàn)在Lucky13攻擊下,不夠穩(wěn)定,因此我們?cè)谏厦娴呐渲弥薪昧薈BC密碼族,雖然go1.8已經(jīng)進(jìn)行了改善。
注意!上述的優(yōu)化只針對(duì)amd64架構(gòu),在此架構(gòu)下,我們甚至可以考慮cloudflare公司的開(kāi)源的性能極高的加密版本(AES-GCM,Chacha20-Poly2305,P256)。
當(dāng)然,我們還需要證書(shū),這里我們可以使用golang.org/x/crypto/acme/autocert和Letss Encrypt,同時(shí)別忘了將http請(qǐng)求重定向到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來(lái)檢查我們的TLS是否正確
net/http
net/http是一個(gè)成熟的HTTP1.1和HTTP2協(xié)議棧,具體怎么用,這里就不贅述了,我們來(lái)講講服務(wù)器端背后的故事。
timeouts
在外網(wǎng)環(huán)境中,這個(gè)參數(shù)是最重要的也是最容易被忽視的之一!你的后端服務(wù)如果不設(shè)置超時(shí),在內(nèi)網(wǎng)環(huán)境可能還Ok,但是到了外網(wǎng)環(huán)境,那就是災(zāi)難,特別是在遇到攻擊時(shí)。
Timeouts的應(yīng)用是一種資源控制,就算goroutine很廉價(jià),但是文件描述符fd很昂貴的,一個(gè)不再工作或者長(zhǎng)閑置的連接是不該去占用寶貴的fd的。
當(dāng)服務(wù)器的fd不夠用時(shí),在accept新連接時(shí)就會(huì)失敗,報(bào)錯(cuò)如下:
http: Accept error: accept tcp [::]:80: accept: too many open files; retrying in 1s
默認(rèn)的net/http的http.Server,可以通過(guò)http.ListenAndServe和http.ListenAndServeTLS創(chuàng)建,是沒(méi)有timeouts的,這完全不是我們想要的。

如上圖所示,http.Server主要有三種timeouts,ReadTimeout,WriteTimeout,IdleTimeout,我們可以這樣設(shè)置:
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一直到所有請(qǐng)求的body被完全讀取(如果不讀body,那就是所有header被讀取)。該超時(shí)是net/http包在連接accept之后直接設(shè)置SetReadDeadline的。
ReadTimeout存在一個(gè)問(wèn)題,服務(wù)器沒(méi)有給更多的時(shí)間來(lái)流式處理來(lái)自客戶端的數(shù)據(jù)。因此Go1.8引入了ReadHeaderTimeout,這里的超時(shí)僅僅針對(duì)Header的讀取超時(shí),當(dāng)然這個(gè)沒(méi)有解決根本問(wèn)題,因此新的解決方案在issue#16100有進(jìn)一步的討論,關(guān)于怎么在Handler中處理ReadTimeout。
WriteTimeout是從包頭讀取成功開(kāi)始,一直到回復(fù)(response)的寫(xiě)入,是在readRequest的末尾調(diào)用SetWriteDeadline函數(shù)實(shí)現(xiàn)的。
當(dāng)連接是HTTPS時(shí),SetWriteDeadline會(huì)在連接accept后立刻調(diào)用一次,這里是處理TLS的握手超時(shí)。因此,這次超時(shí)是在HTTP包頭讀取或者等待第一個(gè)字節(jié)傳輸之前結(jié)束。
和ReadTimeout一樣,WriteTimeout也無(wú)法從Handler中進(jìn)行相對(duì)控制:issue#16100
最后是IdleTimeout,這個(gè)是在Go1.8引入的一個(gè)很有用的參數(shù),用來(lái)控制服務(wù)器端KeepAlive的連接允許空閑的最大時(shí)間。在go1.8之前,ReadTimeout有一個(gè)很大的問(wèn)題,對(duì)于Keepalive的連接是不友好的(盡管可以在應(yīng)用層來(lái)解決Idle的超時(shí)問(wèn)題):因?yàn)樵谏弦粋€(gè)請(qǐng)求的讀取完畢后,下一個(gè)請(qǐng)求的ReadTimeout會(huì)立即開(kāi)始重新計(jì)時(shí),這樣連接空閑的時(shí)間也算在ReadTimeout內(nèi),造成了連接的過(guò)早斷開(kāi)。
綜上所述,當(dāng)我們?cè)贕o1.8中處理外部不受信任的連接時(shí),我們要設(shè)置上這三個(gè)超時(shí),這樣客戶端就不會(huì)因?yàn)楦鞣N過(guò)慢的寫(xiě)或者讀,一直霸占連接了。
http2
在Go1.6版本及之后,HTTP2會(huì)自動(dòng)開(kāi)啟,當(dāng)且僅當(dāng):
- 請(qǐng)求是基于TLS/HTTPS的
- Server.TLSNextProto設(shè)置為nil(注意,如果設(shè)置為空map,那會(huì)禁用HTTP2)
- Server.TLSConfig被設(shè)置并且ListenAndServerTLS被使用;或者,使用Serve,同時(shí)tls.Config.NextProtos包含了"h2",例如[]string{"h2","http/1.1"}
同時(shí)在Go1.8版本修復(fù)了一個(gè)關(guān)于HTTP2的ReadTimeout的Bug,再結(jié)合1.8的其它特性,我的建議是盡快升級(jí)1.8。
tcp keepalive
如果你在用ListenAndServe(與此相對(duì)的是給Serve傳一個(gè)net.Listener參數(shù),但是這種方式?jīng)]有做任何防護(hù)),那么三分鐘長(zhǎng)的TCP keepalive時(shí)間將自動(dòng)被設(shè)置
如果你用的是TCP長(zhǎng)連接服務(wù),那么你該使用net.ListenTCP,同時(shí)設(shè)置keepalive時(shí)間,根據(jù)我的經(jīng)驗(yàn),如果不設(shè)置這個(gè),那么長(zhǎng)連接存在泄漏的風(fēng)險(xiǎn),后面我會(huì)詳細(xì)寫(xiě)一篇文章分析TCP連接泄漏的問(wèn)題。
metrics
我們可以用Server.ConnState來(lái)獲取連接的狀態(tài),注意,我們要自己維護(hù)map[net.Conn]ConnState。
總結(jié)
以后再也不用在Go的Web服務(wù)前再前置一個(gè)Nginx了,節(jié)省了服務(wù)器同時(shí)也降低了請(qǐng)求的延遲,前提是,我們使用了Go1.8。
如果您喜歡這篇文章,請(qǐng)點(diǎn)擊喜歡;如果想及時(shí)獲得最新的咨詢,請(qǐng)點(diǎn)擊關(guān)注。您的支持是對(duì)作者都是最大的激勵(lì),萬(wàn)分感激!By 孫飛