你真的瞭解 timeout 嗎?
本文來自董神,一個和曹大談笑風生的男人:
公衆號
服務爲什麼需要 timeout 呢?提前釋放資源。
記得在上家公司時,一個 python 服務與公網交互,request 庫發出去的請求沒有設置 timeout... 而且還是個定時任務,佔用了超多 fd。
同時微服務場景下某下游的服務阻塞卡頓,這樣會造成他的級聯上下游都雪崩了。
語言層面:對於使用線程池的語言,會消耗所有線程,worker 不夠用。其實對於 Go 來說,創建大量 goroutine 也會有 runtime 開銷的, 只是慢性死亡罷了。
內核層面:還有一點超時配置的必要性,如果某服務掛了,那麼內核會幫忙收尾,根據情況或走 RST 或走 FIN,訪問者就知道連接關了。但如果主機掛了,或者中間網絡設備掛了,客戶端沒有超時配置,就只能通過 tcp keepalive 來判斷死鏈接,按照默認內核配置語言兩個多小時,文末提到 redis 就是例子。
Latency
業界都用 P99 分位來衡量服務的 latency,即使這樣如果 QPS 非常高,另外 1% 的請求也會出現 long tail。再來看幾個不同側重點的概念:
Server Side P99 統計的只是 server handler 處理時間。
Client P99 = client framework 時間 + client 內核處理時間 + 網絡傳輸時間 + server 處理時間。
當你發現 latency 比較高,想去 challenge 下游時,請對好口徑。通常 client p99 > server p99。
這還是普通的 server/client 模式,如果中間涉及了 lb, 或是 mesh 排查問題更要命。
可觀測性
現在都是微服務場景,一個訂單全鏈路涉及幾十個服務,查起問題非常困難,所以分佈式的 tracing 系統非常重要。
另外現在也都擁抱雲原生環境,如果引入 service mesh 的話更難以排查問題。
一般 tracing 系統都是根據 google 論文 Dapper, a Large-Scale Distributed Systems Tracing Infrastructure[1] 發展而來的。
除了自己造輪子,主流的有 zipkin[2], opentelemetry[3]
底層實現
定時器這塊業務早有標準實現:小頂堆
, 紅黑樹
和 時間輪
。感興趣的同學可以搜索相關文章
原理不難,但是有公司面試都要求手寫紅黑樹,這就過份了吧。
Linux 內核和 Nginx 的定時器採用了 紅黑樹
實現,好多長連接系統多采用 時間輪。
Go 使用 小頂堆
,四叉堆,比較矮胖,不是最樸素的二叉堆。
最早版本只有一個 timer 堆,所以性能非常差,精度也有問題。一般都用戶實現多堆,或是用時間輪實現。這方面的輪子比寫公衆號的碼農都多 ^_^
後來經過優化 Go 內置多堆實現,每個 P 一個 timer 堆,性能好了很多。注意,Go 的 conn timeout 是通過用戶層 timer 實現的,而不是內核的 setsockopt。
HTTP
這裏要區分 http1 和 http2,以前寫過一篇 HOL blocking 的文章,感興趣可以翻下歷史
Http1 如果超時到了,那麼底層庫是要關閉 tcp connection 的,強制丟棄未讀到的數據,這時會產生大量的 timewait,要注意。
但是對於 Http2 來說,虛擬出來了 stream,做到了多路複用,只要關閉 stream 即可,底層 socket 還可以正常使用。
對於 Go Http 還有一個坑,可以參考 i/o timeout,希望你不要踩到這個 net/http 包的坑。
func init() {
tr = &http.Transport{
MaxIdleConns: 100,
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*2) //設置建立連接超時
if err != nil {
return nil, err
}
err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設置發送接受數據超時
if err != nil {
return nil, err
}
return conn, nil
},
}
}
上面代碼是錯誤使用,這個導致每次 conn 連接後只設置一次超時時間:
client := &http.Client{
Transport: tr,
Timeout: 3*time.Second, // 超時加在這裏,是每次調用的超時
}
正確的應該在 http.Client
結構體裏設置,感興趣的去參考全文吧。
另外服務端也要設置 timeout,以防把服務端壓跨,請參考 So you want to expose Go on the Internet[4]
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
TLSConfig: tlsConfig,
Handler: serveMux,
}
log.Println(srv.ListenAndServeTLS("", ""))
數據庫相關
做爲 CRUD Boy,經常和 DB 打交道,讓我們來看下常見的超時設置與坑。
Redis 服務端要注意兩個參數:timeout
和 tcp-keepalive。
Redis Client 實現有一個重大問題,對於集羣環境下,有些請求會做 Redirect 跳轉,默認是 16 次,如果 tcp read timeout 設置了 100ms,那總時間很可能超過了 1s。
這就是一直強調的問題,tcp timeout 設置不代表實際的調用時間,因爲業務層會多次調用 socket 讀寫。最好外面包一層 context 或是 circuit breaker。
MySQL 也同樣服務端可以設置 MAX_EXECUTION_TIME
來控制 sql 執行時間。不同發行版本還不一樣,有的只支持 select,有的同時支持 dml ddl...
其它
Q: timeout 與 sla 什麼關係?
A: 要大於 sla。沒有經過 toB 業務的重錘,感觸不深,有朋友瞭解的可以留言講講 toB 業務的玩法。
Q: 如何傳遞 timeout?
A: 一般都是框架層傳遞的,比如 grpc 會在 header 裏傳遞服務的 timeout, 每經過一個 backend,減去相應的耗時。
Q: 依賴的下游出現大量超時,應該如何處理?
A: 要做到 fast fail,一定得有降級 (circuit breaker 熔斷)措施,否則會拖垮整條鏈路。
這次分享就這些,以後面還會分享更多的內容,如果感興趣,可以關注並點擊左下角的分享
轉發哦~
參考資料
[1]
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]
So you want to expose Go on the Internet: https://blog.cloudflare.com/exposing-go-on-the-internet/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SllMlL_94cir-x8Hw9D-8g