如何更好的設(shè)置timeout
為什么會(huì)有timeout
百度了一下timeout的字面意思,就是簡(jiǎn)單的“超時(shí)”,那么timeout為什么跟我們編程息息相關(guān),我沒有找到timeout的最初的出處,但是我自己想了一下,這個(gè)應(yīng)該是跟tcp/ip協(xié)議一起出現(xiàn)的,timeout應(yīng)該是伴隨著io出現(xiàn)的,io又分為網(wǎng)絡(luò)io和磁盤io,當(dāng)時(shí)我們不太關(guān)注磁盤io,主要關(guān)注的是網(wǎng)絡(luò)io,所以我感覺是跟tcp/ip協(xié)議一起出現(xiàn)的,這個(gè)具體還要在查一下。如果網(wǎng)絡(luò)交互沒有timeout會(huì)出現(xiàn)一個(gè)什么情況?我不知道對(duì)端是否存活,那么有人說了我可以通過heartbeat來保持長連接,那么問題又來了,心跳間隔要設(shè)置多長?心跳間隔設(shè)置了我怎么來確定是否有心跳過來,那么就設(shè)計(jì)到了read心跳包,read心跳包有回到了最初的問題,如果我們不設(shè)置timeout會(huì)有啥結(jié)果?
不設(shè)置timeout的危害
如果我們不設(shè)置timeout會(huì)有什么影響呢?
例子1:以前在做長連接push服務(wù)的時(shí)候遇到了一個(gè)問題,就是一臺(tái)服務(wù)器的長連接服務(wù)的內(nèi)存在承載了100w連接的時(shí)候內(nèi)存使用非常高,當(dāng)時(shí)是用golang語言開發(fā)的,每條連接會(huì)有兩個(gè)goroutine(讀和寫)來維護(hù)長連接的交互,每個(gè)goroutine占用4KB(golang1.4以后已經(jīng)變?yōu)?KB,輕量的goroutine+channel是golang語言適合并發(fā)開發(fā)的優(yōu)勢(shì))的大小,那么我們可以估算一下不到4G的內(nèi)存使用,加上其他的一些信息總共不應(yīng)該超過5G,但是當(dāng)時(shí)的內(nèi)存使用都是10多G,一直比較納悶,好在golang有比較好的runtime的pprof的監(jiān)測(cè),能夠很容易的dump出來整個(gè)goroutine的運(yùn)行情況(類似于java的jstack),當(dāng)拿到這些信息的時(shí)候,通過分析看到有很多的goroutine停留在socket的read上,而且是剛剛建立連接,等到握手信息的read上邊,由于golang的并發(fā)比較簡(jiǎn)單,我們對(duì)于read采用了阻塞read,看到這個(gè)異常以后就開始檢查代碼,發(fā)現(xiàn)read的時(shí)候沒有設(shè)置timeout,如果沒有設(shè)置read會(huì)有什么結(jié)果呢,這個(gè)等待read的goroutine就會(huì)一直在阻塞read(一直到操作系統(tǒng)層面的tcp超時(shí),一般默認(rèn)是兩個(gè)小時(shí)),那么就造成了goroutine(java里邊可能是thread)的泄露,從而造成了內(nèi)存泄露,也就能理解為什么內(nèi)存過高了。找到問題修改代碼,只加了一行代碼就解決了這個(gè)問題。
例子2:我們的線上系統(tǒng),經(jīng)常會(huì)遇到一個(gè)錯(cuò)誤,就是redis獲取連接超時(shí),遇到這種問題,很多人就來找我說是不是redis掛了,我看到這個(gè)問題一般的反應(yīng):
在使用redis的時(shí)候取到連接用完沒有放回,這也是一種連接泄露的情況
還有在并發(fā)很高,連接池的連接設(shè)置的比較小,不夠用了,比如你的redis的連接設(shè)置的是10條,每個(gè)redis的請(qǐng)求是10ms,那么1s最多能夠完成1000個(gè)請(qǐng)求,所以當(dāng)你的qps超過1000的時(shí)候會(huì)出現(xiàn)什么情況?就是剛才提到的獲取連接超時(shí)。
還有一種情況是redis的卡頓(redis是單線程工作的,所以決定它的一些應(yīng)用場(chǎng)景,具體可以自己網(wǎng)上查看相應(yīng)的資料,或者有興趣下來再單獨(dú)溝通redis的問題),redis的卡頓會(huì)導(dǎo)致所有的請(qǐng)求都會(huì)耗時(shí)比較高,那么也會(huì)遇到獲取連接超時(shí)的問題。
重點(diǎn)要說的是最后一種情況,我們假設(shè)最后一種情況我們沒有設(shè)置超時(shí)時(shí)間,那么會(huì)有一個(gè)什么樣的結(jié)果,所有的連接都在等待redis的響應(yīng)結(jié)果,所有的業(yè)務(wù)線程都在等待redis的連接,那么請(qǐng)求越堆積越多,一種是造成整個(gè)系統(tǒng)的雪崩,一種是如果沒有控制好thread的數(shù)量,thread會(huì)一直增加最后導(dǎo)致oom。
例子3:某核心模塊A,出現(xiàn)過兩次獲取mysql連接超時(shí),當(dāng)時(shí)看到以后由于影響比較大,直接重啟服務(wù),然后開始追查問題,首先是檢查mysql的慢日志,發(fā)現(xiàn)沒有慢日志,然后檢查mysql事務(wù)里邊除了mysql的操作,還有沒有其他的阻塞操作,發(fā)現(xiàn)很多的redis的操作,而且redis的value比較大,操作比較復(fù)雜,而且當(dāng)時(shí)redis沒有設(shè)置超時(shí)時(shí)間,第一反應(yīng)就是redis的阻塞導(dǎo)致了mysql的事務(wù)的阻塞,從而導(dǎo)致了連接不夠用。商戶中心改掉了redis的處理,增加了超時(shí)時(shí)間,結(jié)果沒過幾天又出現(xiàn)了,這次出現(xiàn)問題以后,直接先jstack了一份信息,然后重啟恢復(fù),分析jstack的信息,發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象,很多的線程都在等著寫文件的鎖。分析代碼,發(fā)現(xiàn)一個(gè)問題,就是mybatis開啟了debug模式,有大量的mysql日志輸出,而且是輸出到了console,默認(rèn)的docker容器的console就是linux操作系統(tǒng)的messages文件,由于寫的數(shù)量巨大,導(dǎo)致了磁盤io的卡頓,從而導(dǎo)致了mysql事務(wù)的阻塞,從而獲取連接失敗,那么找到問題,修改就是改個(gè)配置的事情,把mybatis的debug關(guān)閉。從這個(gè)問題我們看到的是磁盤io的問題,磁盤io的問題遇到的很少不過也會(huì)遇到,這種情況我們應(yīng)該如何避免呢?
- debug日志慎用
- 不要亂輸出日志,輸出有用日志
- 學(xué)會(huì)使用異步日志(掌握的情況下可以使用,必然會(huì)出現(xiàn)比同步日志更惡化的情況,還可能出現(xiàn)丟日志)
如何更好的設(shè)置自己的timeout
如何設(shè)置好timeout,這個(gè)題目比較大,我大概結(jié)合著自己的經(jīng)驗(yàn)提供一些參考,當(dāng)時(shí)我不是說這個(gè)請(qǐng)求就是設(shè)置多少s,或者多少ms,這個(gè)是沒有意義的,也不能拍著腦袋說。
- 中間件+存儲(chǔ)的timeout設(shè)計(jì),除了mysql其他的都還好,可以根據(jù)系統(tǒng)的運(yùn)行情況來調(diào)整,mysql的相對(duì)比較麻煩,我們能設(shè)置的就是獲取連接的超時(shí)時(shí)間,那么設(shè)置多長時(shí)間合適呢?這個(gè)一般來說都是10s,當(dāng)然這個(gè)需要根據(jù)整個(gè)系統(tǒng)的吞吐和連接池的大小來做相應(yīng)的設(shè)置。
- 業(yè)務(wù)超時(shí),grpc+http的,grpc一般都是內(nèi)部服務(wù)的請(qǐng)求,那么我們也需要根據(jù)系統(tǒng)的吞吐和業(yè)務(wù)的情況來做相應(yīng)的調(diào)整,比如現(xiàn)在絕大部分設(shè)置的grpc的timeout都是30s,大部分情況下是滿足需求的,但是也有風(fēng)險(xiǎn),就是剛才提到的thread泄露,導(dǎo)致系統(tǒng)的oom,所以說整個(gè)設(shè)計(jì)需要根據(jù)系統(tǒng)的情況來做調(diào)整
- 請(qǐng)求第三方的接口的超時(shí),我們一般請(qǐng)求第三方都是http的接口請(qǐng)求,那么http的超時(shí)設(shè)計(jì)也會(huì)設(shè)計(jì)到第三方系統(tǒng)的一個(gè)性能的情況,這種一般都需要自己根據(jù)系統(tǒng)運(yùn)行情況來做統(tǒng)計(jì)分析,最后得出一個(gè)比較合理的值,那么如果不設(shè)計(jì)timeout會(huì)出現(xiàn)什么情況?必然是thread泄露,從而導(dǎo)致oom或者整個(gè)系統(tǒng)hang住,我們老的微信餐廳,就是因?yàn)檎?qǐng)求支付寶和微信沒有設(shè)置timeout導(dǎo)致tomcat的線程數(shù)打滿,從而導(dǎo)致整個(gè)系統(tǒng)hang住,服務(wù)不可用。
總結(jié)一下:有io的地方就必須有timeout,這個(gè)可以當(dāng)成是一個(gè)編程的慣例或者一個(gè)規(guī)則。好的timeout的設(shè)計(jì)可以使系統(tǒng)更加的健壯,對(duì)用戶更加的優(yōu)雅。timeout其實(shí)也是對(duì)系統(tǒng)的一種熔斷降級(jí)。