Go:你真的了解 timeout 嘛?
服務(wù)為什么需要 timeout 呢?提前釋放資源
記得在上家公司時(shí),一個(gè) python 服務(wù)與公網(wǎng)交互,request 庫(kù)發(fā)出去的請(qǐng)求沒有設(shè)置 timeout ... 而且還是個(gè)定時(shí)任務(wù),占用了超多 fd
同時(shí)微服務(wù)場(chǎng)景下某下游的服務(wù)阻塞卡頓,這樣會(huì)造成他的級(jí)聯(lián)上下游都雪崩了。
語(yǔ)言層面:對(duì)于使用線程池的語(yǔ)言,會(huì)消耗所有線程,work 不夠用。其實(shí)對(duì)于 go 來說,創(chuàng)建大量 goroutine 也會(huì)有 runtime 開銷的, 只是慢性死亡罷了
內(nèi)核層面:還有一點(diǎn)超時(shí)配置的必要性,如果某服務(wù)掛了,那么內(nèi)核會(huì)幫忙收尾,根據(jù)情況或走 RST 或走 FIN,訪問者就知道鏈接關(guān)了。但如果主機(jī)掛了,或者中間網(wǎng)絡(luò)設(shè)備掛了,客戶端沒有超時(shí)配置,就只能 tcp keepalive 來判斷死鏈接,按照默認(rèn)內(nèi)核配置語(yǔ)言兩個(gè)多小時(shí)。文末提到 redis 就是例子
Latency
業(yè)界都用 P99 分位來衡量服務(wù)的 latency, 即使這樣如果 QPS 非常高,另外 1% 的請(qǐng)求也會(huì)出現(xiàn) long tail. 再來看幾個(gè)不同側(cè)重點(diǎn)的概念:

Server Side P99 統(tǒng)計(jì)的只是 server handler 處理時(shí)間
Client P99 = client framework 時(shí)間 + client 內(nèi)核處理時(shí)間 + 網(wǎng)絡(luò)傳輸時(shí)間 + server 處理時(shí)間
當(dāng)你發(fā)現(xiàn) latency 比較高,想去 challenge 下游時(shí),請(qǐng)對(duì)好口徑。通常 client p99 > server p99
這還是普通的 server/client 模式,如果中間涉及了 lb, 或是 mesh 排查問題很要命
可觀測(cè)性
現(xiàn)在都是微服務(wù)場(chǎng)景,一個(gè)訂單全鏈路涉及幾十個(gè)服務(wù),查起問題非常困難,所以分布式的 tracing 系統(tǒng)非常重要
另外現(xiàn)在也都擁抱云原生環(huán)境,如果引入 service mesh 的話更難以排查問題
一般 tracing 系統(tǒng)都是根據(jù) google 論文 Dapper, a Large-Scale Distributed Systems Tracing Infrastructure[1] 發(fā)展而來的
除了自己造輪子,主流的有 zipkin[2], opentelemetry[3]

底層實(shí)現(xiàn)
定時(shí)器這塊業(yè)務(wù)早有標(biāo)準(zhǔn)實(shí)現(xiàn):小頂堆, 紅黑樹 和 時(shí)間輪. 感興趣的同學(xué)可以搜索相關(guān)文章
原理不難,但是有公司面試都要求手寫紅黑樹!!!這就過份了吧
Linux 內(nèi)核和 Nginx 的定時(shí)器采用了 紅黑樹 實(shí)現(xiàn),好多長(zhǎng)連接系統(tǒng)多采用 時(shí)間輪
Go 使用 小頂堆, 四叉堆,比較矮胖,不是最樸素的二叉堆。
最早版本只有一個(gè) timer 堆,所以性能非常差,精度也有問題。一般都用戶實(shí)現(xiàn)多堆,或是用時(shí)間輪實(shí)現(xiàn)。這方面的輪子比寫公眾號(hào)的碼農(nóng)都多 ^_^
后來經(jīng)過優(yōu)化 Go 內(nèi)置多堆實(shí)現(xiàn),每個(gè) P 一個(gè) timer 堆,性能好了很多。注意,Go 的 conn timeout 是通過用戶層 timer 實(shí)現(xiàn)的,而不是內(nèi)核的 setsockopt
HTTP
這里要區(qū)分 http1 和 http2, 以前寫過一篇 HOL blocking 的文章,感興趣可以翻下歷史
Http1 如果超時(shí)到了,那么底層庫(kù)是要關(guān)閉 tcp connection 的,強(qiáng)制丟棄未讀到的數(shù)據(jù),這時(shí)會(huì)產(chǎn)生大量的 timewait, 要注意
但是對(duì)于 Http2 來說,虛擬出來了 stream, 做到了多路復(fù)用,只要關(guān)閉 stream 即可,底層 socket 還可以正常使用
對(duì)于 go http 還有一個(gè)坑,可以參考 i/o timeout , 希望你不要踩到這個(gè)net/http包的坑[4]
func init() {
tr = &http.Transport{
MaxIdleConns: 100,
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*2) //設(shè)置建立連接超時(shí)
if err != nil {
return nil, err
}
err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設(shè)置發(fā)送接受數(shù)據(jù)超時(shí)
if err != nil {
return nil, err
}
return conn, nil
},
}
}
上面代碼是錯(cuò)誤使用,這個(gè)導(dǎo)致每次 conn 連接后只設(shè)置一次超時(shí)時(shí)間
client := &http.Client{
Transport: tr,
Timeout: 3*time.Second, // 超時(shí)加在這里,是每次調(diào)用的超時(shí)
}
正確的應(yīng)該在 http.Client 結(jié)構(gòu)體里設(shè)置,感興趣的去參考全文吧
另外服務(wù)端也要設(shè)置 timeout, 以防把服務(wù)端壓跨,請(qǐng)參考 So you want to expose Go on the Internet[5]

srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
TLSConfig: tlsConfig,
Handler: serveMux,
}
log.Println(srv.ListenAndServeTLS("", ""))
數(shù)據(jù)庫(kù)相關(guān)
做為 CRUD Boy, 經(jīng)常和 DB 打交道,讓我們來看下常見的超時(shí)設(shè)置與坑
Redis 服務(wù)端要注意兩個(gè)參數(shù):timeout 和 tcp-keepalive
其中 timeout 用于關(guān)閉 idle client conn, 默認(rèn)是 0 不關(guān)閉,為了減少服務(wù)端 fd 占用,建議設(shè)置一個(gè)合理的值
tcp-keepalive 在很早的 redis 版本是不開啟的,這樣經(jīng)常會(huì)遇到因?yàn)榫W(wǎng)格抖動(dòng)等原因,socket conn 一直存在,但實(shí)際上 client 早己經(jīng)不存在的情況
Redis Client 實(shí)現(xiàn)有一個(gè)重大問題,對(duì)于集群環(huán)境下,有些請(qǐng)求會(huì)做 Redirect 跳轉(zhuǎn),默認(rèn)是 16 次,如果 tcp read timeout 設(shè)置了 100ms, 那總時(shí)間很可能超過了 1s
這就是一直強(qiáng)調(diào)的問題,tcp timeout 設(shè)置不代表實(shí)際的調(diào)用時(shí)間,因?yàn)闃I(yè)務(wù)層會(huì)多次調(diào)用 socket 讀寫。最好外面包一層 context 或是 circuit breaker
MySQL 也同樣服務(wù)端可以設(shè)置 MAX_EXECUTION_TIME 來控制 sql 執(zhí)行時(shí)間。不同發(fā)行版本還不一樣,有的只支持 select, 有的同時(shí)支持 dml ddl ...
其它
Q: 有同事問 timeout 與 sla 什么關(guān)系?
A: 要大于 sla. 沒有經(jīng)過 toB 業(yè)務(wù)的重錘,感觸不深,有朋友了解的可以留言講講 toB 業(yè)務(wù)的玩法
Q: 如何傳遞 timeout ?
A: 一般都是框架層傳遞的,比如 grpc 會(huì)在 header 里傳遞服務(wù)的 timeout, 每經(jīng)過一個(gè) backend, 減去相應(yīng)的耗時(shí)
Q: 依賴的下游出現(xiàn)大量超時(shí),應(yīng)該如何處理?
A: 要做到 fast fail, 一定得有降級(jí) (circuit breaker 熔斷)措施,否則會(huì)拖垮整條鏈路。
小結(jié)
這次分享就這些,以后面還會(huì)分享更多的內(nèi)容,如果感興趣,可以關(guān)注并點(diǎn)擊左下角的分享轉(zhuǎn)發(fā)哦(:
參考資料
Dapper, a Large-Scale Distributed Systems Tracing Infrastructure: https://research.google/pubs/pub36356/,
[2]zipkin: https://zipkin.io/,
[3]opentelemetry: https://opentelemetry.io/docs/concepts/distributions/,
[4]i/o timeout , 希望你不要踩到這個(gè)net/http包的坑: https://mp.weixin.qq.com/s/UBiZp2Bfs7z1_mJ-JnOT1Q,
[5]So you want to expose Go on the Internet: https://blog.cloudflare.com/exposing-go-on-the-internet/,
推薦閱讀
