你真的瞭解 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 服務端要注意兩個參數:timeouttcp-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