有同事報客戶端請求某核心服務(wù)出現(xiàn)大量connection reset by peer。線上故障,趕緊高優(yōu)定位處理。
no.1
及時止損
看現(xiàn)象是個別實(shí)例集中出現(xiàn),不是全部實(shí)例出現(xiàn),那就和運(yùn)行環(huán)境、流量、或者某個資源有關(guān)系。按照及時止損的原則,首先驗證重啟能否恢復(fù),驗證重啟可以恢復(fù),聯(lián)系OPS快速操作重啟,服務(wù)恢復(fù)。由于不是穩(wěn)定復(fù)現(xiàn)問題,需要保留現(xiàn)場用于問題定位,讓OPS保留兩個故障實(shí)例,用作問題定位(保留的實(shí)例臨時屏蔽流量)。
重啟大法快速完成止損,服務(wù)恢復(fù),觀察段時間運(yùn)行穩(wěn)定。然后可以不慌不忙定位問題了。
no.2
問題定位
1.客戶端請求出現(xiàn)connection reset by peer,驗證問題實(shí)例穩(wěn)定復(fù)現(xiàn)。
curl -v 'http://10.xx.xx.35:2133/xx/xx/checkalive'
2.查看日志,并沒有access日志輸出,而且響應(yīng)connection reset by peer。
tail -f ./log/xxx.log
3.通過tcpdump查看請求詳細(xì)數(shù)據(jù)包情況(有些機(jī)器tcpdump按照路徑?jīng)]有在PATH里,可以通過whereis檢索下具體按照路徑使用,通過ifconfig查看網(wǎng)絡(luò)設(shè)備名)。通過tcpdump結(jié)果發(fā)現(xiàn),TCP三次握手完成,在發(fā)送數(shù)據(jù)時服務(wù)端沒有響應(yīng)ACK,而響應(yīng)了reset,導(dǎo)致客戶端http請求響應(yīng)connection reset by peer。
whereis tcpdump</code>`tcpdump: /usr/sbin/tcpdump /usr/share/man/man8/tcpdump.8.gz /usr/share/man/man1/tcpdump.1 /usr/share/man/man1/tcpdump.1.gz``
``ifconfig``eth0 Link encap:Ethernet``lo Link encap:Local Loopback``
``/usr/sbin/tcpdump -i eth0 -n -nn host 10.xx.xx.35``
`<code style='border-radius: 0px;white-space: pre;display: flex;font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;'>發(fā)現(xiàn)TCP三次握手完成,在發(fā)送數(shù)據(jù)時服務(wù)端沒有響應(yīng)ACK,而響應(yīng)了reset,導(dǎo)致客戶端http請求響應(yīng)connection reset by peer。
4.服務(wù)端通過listen(sockfd, backlog)方法告訴內(nèi)核監(jiān)聽該socket并設(shè)置隊列大?。ㄎ赐瓿涉溄雨犃?已完成連接隊列),然后當(dāng)客戶端通過connect()方法請求鏈接時,由系統(tǒng)內(nèi)核完成TCP三次握手,并把請求放入已完成連接隊列,等待調(diào)用accept()方法取走,accept()需要先通過socket()創(chuàng)建新的句柄。
golang實(shí)現(xiàn)是:框架通過net/http包Server.Serve()方法開啟服務(wù),標(biāo)準(zhǔn)庫中通過net包TCPListener.AcceptTCP()等待獲取新的鏈接,最終通過internal/poll包的accept()發(fā)起系統(tǒng)調(diào)用accept4() or accept(),golang這個accept和c的accept()還不一樣,golang不需要提前創(chuàng)建套接字句柄傳入,而且由accept()直接返回新套接字句柄。
也就是客戶端請求時,內(nèi)核完成了TCP三次握手,并把請求放入已完成連接隊列,但是accept時發(fā)生了錯誤,直接響應(yīng)了客戶端reset。accept發(fā)生錯誤最常見就是句柄被打滿了,查看進(jìn)程監(jiān)聽端口鏈接情況和進(jìn)程句柄使用情況。
net/http/server.go func (srv *Server) Serve(l net.Listener) error</code>`net/tcpsock.go func (l *TCPListener) AcceptTCP() (*TCPConn, error)``net/tcpsock_posix.go func (ln *TCPListener) accept() (*TCPConn, error)``net/fd_unix.go func (fd *netFD) accept() (netfd *netFD, err error)``internal/poll/fd_unix.go func (fd *FD) Accept() (int, syscall.Sockaddr, string, error)`<code style='border-radius: 0px;white-space: pre;display: flex;font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;'>internal/poll/sock_cloexec.go func accept(s int) (int, syscall.Sockaddr, string, error)
5.通過netstat or ss查看監(jiān)聽端口的鏈接情況,通過lsof查看進(jìn)程句柄占用情況,通過ulimit查看系統(tǒng)限制。發(fā)現(xiàn)果然進(jìn)程句柄被打滿了,超過了10240的限制。確認(rèn)是由于進(jìn)程句柄被打滿導(dǎo)致客戶端請求響應(yīng)connection reset by peer。同時通過netstat的統(tǒng)計信息還發(fā)現(xiàn),處于CLOSE_WAIT狀態(tài)的鏈接很多,但是也遠(yuǎn)小于打開的句柄數(shù)。至此,雖然明確了客戶端請求會響應(yīng)connection reset by peer是由于服務(wù)進(jìn)程句柄被打滿導(dǎo)致的,但是依然不知道什么原因?qū)е铝朔?wù)進(jìn)程句柄被打滿。
netstat -an | grep port 或者 ss -ant | grep port</code>`lsof -p port`<code style='border-radius: 0px;white-space: pre;display: flex;font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;'>ulimit -a
6.CLOSE_WAIT狀態(tài)鏈接太多,可能會占用大量句柄,從CLOSE_WAIT狀態(tài)入手分析。結(jié)合TCP狀態(tài)機(jī),四次揮手過程中,被動關(guān)閉的一方收到第一次斷開鏈接的FIN包后進(jìn)入CLOSE_WAIT狀態(tài),等待發(fā)送完數(shù)據(jù),然后發(fā)出第二次FIN包后進(jìn)入LAST_ACK狀態(tài),收到對端ACK后進(jìn)入CLOSED狀態(tài)完成,另外CLOSE_WAIT狀態(tài)有超時時間(一般默認(rèn)是2H),超時會被系統(tǒng)關(guān)閉。三次握手是在系統(tǒng)內(nèi)核完成的,但是四次揮手由于要等待數(shù)據(jù)發(fā)送完成,是和應(yīng)用程序相關(guān)的,內(nèi)核收到第一個FIN后會通知應(yīng)用程序,應(yīng)該是應(yīng)用程序要響應(yīng)后才能再發(fā)送第二個FIN。
結(jié)合這些信息猜測:服務(wù)句柄是被逐漸累積打滿的,出現(xiàn)大量CLOSE_WAIT是由于客戶端先斷開鏈接(很可能是請求超時),服務(wù)端在收到客戶端超時端口請求后,由于用戶態(tài)請求處理阻塞,導(dǎo)致第二次FIN無法發(fā)送,而且應(yīng)該是出現(xiàn)了死鎖等問題,持久阻塞(句柄一致沒有被釋放)??蛻舳藨?yīng)該是先有大量io timeout,等服務(wù)端句柄被打滿后才出現(xiàn)connect reset by peer的,而客戶端io timeout增多很可能是服務(wù)端處理請求耗時突增或者阻塞導(dǎo)致。
理論上能解釋通了,線下模擬實(shí)現(xiàn)驗證,在接口中sleep(100s),壓測很快就復(fù)現(xiàn)了connect reset by peer,現(xiàn)象和線上問題case完全一致,確認(rèn)猜想。那么接下來定位的重點(diǎn)就是為什么服務(wù)端會突然出現(xiàn)阻塞?由于不穩(wěn)定復(fù)現(xiàn),是什么觸發(fā)了阻塞?
SOCKET工作流程
epoll
TCP狀態(tài)流轉(zhuǎn)圖:
TCP SOCKET狀態(tài)表:
·CLOSED: 關(guān)閉狀態(tài),沒有連接活動
·LISTEN: 監(jiān)聽狀態(tài),服務(wù)器正在等待連接進(jìn)入
·SYN_SENT: 已經(jīng)發(fā)出連接請求,等待確認(rèn)
·SYN_RCVD: 收到一個連接請求,尚未確認(rèn)
·ESTABLISHED: 連接建立,正常數(shù)據(jù)傳輸狀態(tài)
·FIN_WAIT_1:(主動關(guān)閉)已經(jīng)發(fā)送關(guān)閉請求,等待確認(rèn)
·FIN_WAIT_2:(主動關(guān)閉)收到對方關(guān)閉確認(rèn),等待對方關(guān)閉請求
·CLOSE_WAIT:(被動關(guān)閉)收到對方關(guān)閉請求,已經(jīng)確認(rèn)
·LAST_ACK: (被動關(guān)閉)等待最后一個關(guān)閉確認(rèn),并等待所有分組死掉
·TIMED_WAIT: 完成雙向關(guān)閉,等待所有分組死掉
·CLOSING: 雙方同時嘗試關(guān)閉,等待對方確認(rèn)
三次握手
四次揮手
7.到了應(yīng)用程序?qū)用?,要分析進(jìn)程過去發(fā)生了什么,只能從應(yīng)用日志和服務(wù)監(jiān)控入手了,從歷史監(jiān)控曲線(內(nèi)存、句柄、流量、耗時等)查找可能出現(xiàn)異常的時間點(diǎn),再找關(guān)鍵時間點(diǎn)的日志仔細(xì)分析。發(fā)現(xiàn)剛開始是處理耗時增長,然后只能輸出access_log,最后才到請求無日志輸出,從日志完成驗證上面的分析猜想。發(fā)現(xiàn)耗時突增是關(guān)鍵點(diǎn),仔細(xì)分析業(yè)務(wù)日志,發(fā)現(xiàn)是請求DB耗時增加,再進(jìn)一步看訪問DB的統(tǒng)計信息,發(fā)現(xiàn)DB連接池一直在被打滿,請求排隊等空閑待鏈接,導(dǎo)致請求處理耗時增加,然后排隊請求越來越多,直到句柄數(shù)被打滿。由于DB連接池新建鏈接需要句柄,句柄被排隊等空閑鏈接的請求給打滿了,形成了死鎖。也就出現(xiàn)了從超時到句柄被打滿還無法釋放的情況。線上環(huán)境修改DB連接池配置,壓測果然很快復(fù)現(xiàn)了。至此,終于發(fā)現(xiàn)了真相(哭暈,再次證明了完善的日志和監(jiān)控的重要性)。
type DBStats struct {</code>` MaxOpenConnections int // Maximum number of open connections to the database; added in Go 1.11``
`` // Pool Status`` OpenConnections int // The number of established connections both in use and idle.`` InUse int // The number of connections currently in use; added in Go 1.11`` Idle int // The number of idle connections; added in Go 1.11``
`` // Counters`` WaitCount int64 // The total number of connections waited for; added in Go 1.11`` WaitDuration time.Duration // The total time blocked waiting for a new connection; added in Go 1.11`` MaxIdleClosed int64 // The total number of connections closed due to SetMaxIdleConns; added in Go 1.11`` MaxLifetimeClosed int64 // The total number of connections closed due to SetConnMaxLifetime; added in Go 1.11`<code style='border-radius: 0px;white-space: pre;display: flex;font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;'>}
no.3
故障原因
為了防止DB鏈接數(shù)被打滿,剛開始DB連接池最大連接數(shù)配置的比較小,流量慢慢上漲逼近平衡點(diǎn)。當(dāng)天由于運(yùn)營活動稍微增加的點(diǎn)流量就成了壓死駱駝的最后一根稻草,導(dǎo)致查詢DB請求排隊等待空閑鏈接,排隊時間越長積壓的請求越多,請求處理耗時越大,直到積壓請求太多把句柄打滿,出現(xiàn)了死鎖。
復(fù)現(xiàn)驗證
修改DB最大連接數(shù)配置壓測,很快就能復(fù)現(xiàn)。
問題修復(fù)
去掉DB連接池最大連接數(shù)限制。
no.4
總結(jié)反思
線上故障處理原則
1.及時通報,及時止損。
2.保留現(xiàn)場,定位問題。
3.徹底修復(fù),故障總結(jié)。
線上問題分析
先初步收集信息,再根據(jù)收集到的有限的信息推斷可能的真相,再定向?qū)ふ易C據(jù)證明自己的推斷,再設(shè)計實(shí)驗?zāi)M復(fù)現(xiàn)確認(rèn)自己的推斷。
絕大部分問題都是可以通過模擬復(fù)現(xiàn)的,只是有些問題找到一條正確復(fù)現(xiàn)的路徑比較費(fèi)勁,找到這條復(fù)現(xiàn)路徑也就基本能發(fā)現(xiàn)問題了。通常是應(yīng)用系統(tǒng)提供的相應(yīng)工具分析問題case,獲取詳細(xì)的信息,根據(jù)這些信息結(jié)合相關(guān)知識,推斷造成這個現(xiàn)象可能的原因,設(shè)計復(fù)現(xiàn)的途徑,然后開發(fā)機(jī)模擬實(shí)驗確認(rèn)問題。
不能復(fù)現(xiàn)的問題可能和流量、機(jī)器的瞬時環(huán)境、依賴服務(wù)的瞬時抖動等有關(guān)系,處理這類問題完善的監(jiān)控和日志就非常重要了,服務(wù)上線后要接入相關(guān)機(jī)器資源、流量、錯誤的監(jiān)控,開發(fā)時日志記錄要完善。日志通常是定位線上問題最重要也最高效的方式,開發(fā)階段一定要重視日志。
一般是從問題表象切入,結(jié)合問題表象和相關(guān)知識,尋找方向,逐個深入分析確認(rèn)疑點(diǎn),逐步找到那個最可能的原因。
后續(xù)注意點(diǎn)
1.線上服務(wù)問題,優(yōu)先止損,重啟雖然暴力,但是有用,關(guān)鍵時刻別忘記了,止損最重要。
2.重要服務(wù)日志、統(tǒng)計、監(jiān)控一定要全,日志最少要保留7天,核心錯誤和統(tǒng)計信息一定要輸出(比如DB連接池的統(tǒng)計信息),統(tǒng)計和監(jiān)控要持久保存可以追溯,cpu、內(nèi)存、句柄、磁盤占用、磁盤io、網(wǎng)絡(luò)io等機(jī)器資源這些一定要有監(jiān)控,各關(guān)鍵請求的耗時一定要輸出到日志,請求整體耗時要監(jiān)控起來。
3.服務(wù)相關(guān)配置,包括機(jī)器相關(guān)配置也要跟得上流量上漲。
4.對系統(tǒng)底層知識(內(nèi)功)和常用的系統(tǒng)工具(招式)要熟練,要不遇到網(wǎng)絡(luò)類問題特別容易抓瞎。
5.要從開發(fā)階段重視日志的重要性,完備又不多余的輸出日志。
6.定位線上問題時,結(jié)合監(jiān)控+系統(tǒng)工具+日志定位,優(yōu)先仔細(xì)分析日志,如果日志完備,大部分問題都能從日志中發(fā)現(xiàn)。
常用工具
curl -v 'http://10.xx.xx.35:21xx/xx/xx/checkalive'</code>`whereis tcpdump``ifconfig``/usr/sbin/tcpdump -i eth0 -n -nn host 10.xx.xx.35``netstat -an | grep xxxx``ps -ef | grep xxx``lsof -p xxx``ulimit -a``pmap -x xxx``cat /proc/$pid/smaps``strace -p $pid``pstack $pid`<code style='border-radius: 0px;white-space: pre;display: flex;font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;'>ls /proc/$pid/fd/ | wc -l
參考文獻(xiàn)文章來源:http://www.zghlxwxcb.cn/news/detail-454631.html
·[TCP協(xié)議總結(jié)]https://www.jianshu.com/p/4697b2781e86
·[淺談TCP四次揮手]https://blog.csdn.net/yeweilei/article/details/79279963
·[線上大量CLOSE_WAIT原因深入分析]https://segmentfault.com/a/1190000017313251?utm_source=tag-newest
·[Socket之a(chǎn)ccept與三次握手的關(guān)系]https://blog.csdn.net/smart55427/article/details/8431827?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
·[epoll原理剖析]https://medium.com/@heshaobo2012/epoll%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90-3-epoll-bf9cdcf5e50
·[epoll原理圖解]https://blog.csdn.net/qq_35433716/article/details/85345907
·[TCP網(wǎng)絡(luò)編程中connect()、listen()和accept()三者之間的關(guān)系]https://blog.csdn.net/dengjin20104042056/article/details/52357452
·[從源碼角度看Golang的TCP Socket(epoll)實(shí)現(xiàn)]https://www.jianshu.com/p/3ff0751dfa04
更多技術(shù)文章文章來源地址http://www.zghlxwxcb.cn/news/detail-454631.html
到了這里,關(guān)于connection-reset-by-peer問題定位的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!