Go語(yǔ)言·Web調(diào)優(yōu)詳解

遷移自CSDN:http://blog.csdn.net/erlib/article/details/53761116

前言

很早以前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 孫飛

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容