本文的技術(shù)細(xì)節(jié)并不具通用性簿寂,但是排查的思路過程值得自己mark下歉铝,上次遇到這樣的刺激線上故障已經(jīng)是今年3月了~~~這次排查花了3個(gè)小時(shí),還算快啦碘裕,也算最近的一個(gè)小彩蛋携取。
昨天在公司coding,測(cè)試組的同事突然來找我帮孔,告知預(yù)發(fā)環(huán)境的系統(tǒng)掛了雷滋,請(qǐng)求都是異常不撑,打開業(yè)務(wù)日志和tomcat的日志排查,滿屏的Too many open files晤斩,果然 lsof -p pid 預(yù)發(fā)機(jī)器焕檬,已經(jīng)打滿到了65535個(gè),很多都是anon_inode pipe FIFO REG澳泵,真的不知道這是什么鬼:
內(nèi)心一緊实愚,趕緊用lsof -p pid 查看了下線上所有生產(chǎn)機(jī)器的文件描述符個(gè)數(shù),所幸都在正常范圍兔辅,這才放下心對(duì)預(yù)發(fā)環(huán)境的故障進(jìn)行排查腊敲。
首先保存事故現(xiàn)場(chǎng),用新端口給qa緊急部署了一個(gè)新的環(huán)境维苔,nginx轉(zhuǎn)發(fā)到新的端口碰辅。
接下來就是jstack對(duì)堆棧進(jìn)行檢查。jstack的結(jié)果是有10個(gè)左右的線程處于innative的狀態(tài)介时,其他線程都是處于blocked的狀態(tài)没宾。處于innative狀態(tài)的線程,有一個(gè)明顯是tomcat的主線程潮尝,有6榕吼,7個(gè)處于epoll的epoll_wait狀態(tài)饿序,還有一個(gè)是我們開啟的內(nèi)部端口監(jiān)聽勉失,也是wait狀態(tài),剩下的數(shù)百個(gè)blocked線程都在tomcat的quene take的wait狀態(tài)原探。我想了想乱凿,又谷歌了下,會(huì)不會(huì)是epoll_wait的bug呢咽弦,記得jdk1.6有一個(gè)epoll_wait的bug一直沒解決徒蟆。于是定位的天平往jdk bug的方向傾斜,應(yīng)該是這樣想會(huì)輕松一點(diǎn)吧型型。
轉(zhuǎn)念一想段审,如果是epoll_wait的bug,不會(huì)導(dǎo)致文件描述符被打滿呀闹蒜。又陷入了焦灼狀態(tài)寺枉。。
接下來開始嘗試翻日志绷落,業(yè)務(wù)日志是看不出什么問題了姥闪,但是想起來tomcat報(bào)錯(cuò)的時(shí)間是下午2點(diǎn),2點(diǎn)有什么操作么砌烁?查看業(yè)務(wù)日志筐喳,2點(diǎn)有刷新緩存的操作,看起來有了一條靠譜的線!
快速搭建了一個(gè)測(cè)試環(huán)境避归。先lsof一下荣月,大概是400個(gè),刷了一下緩存梳毙,變成600個(gè)喉童,再刷一下,800個(gè)......65535個(gè)顿天,問題到這里已經(jīng)能夠復(fù)現(xiàn)了堂氯,能復(fù)現(xiàn)就好辦。查到了這里牌废,我給自己泡了一杯普洱茶咽白。
測(cè)試了刷新緩存的各個(gè)接口,問題基本上已經(jīng)定位到了刷新通訊緩存鸟缕,我們作為server端采用mina代碼部分的問題晶框,這個(gè)時(shí)候遠(yuǎn)程debug就派上了用場(chǎng)《樱基本上是在懷疑的代碼塊附近授段,每debug一步,我就lsof一下文件描述符的個(gè)數(shù)番甩,終于找到一句可疑的話:
acceptor = new NioSocketAcceptor(CommunicationThreadPool.getSeperatorPool(networkConfig),
new SimpleIoProcessorPool(NioProcessor.class, availableProcessors +1));
調(diào)試的時(shí)候發(fā)現(xiàn)侵贵,每次執(zhí)行到 new SimpleIoProcessorPool(NioProcessor.class, availableProcessors +1)) ,文件描述符會(huì)增加20缘薛,在刷新緩存的時(shí)候窍育,會(huì)有10次這樣的動(dòng)作,加起來就是180個(gè)描述符的增長宴胧,基本符合我們觀察到的現(xiàn)象.接著谷歌了一下這個(gè)類漱抓,果然我們的代碼里面對(duì)mina的使用是不正確的,我們?cè)谒⑿戮彺娴臅r(shí)候恕齐,對(duì)上一次的SimpleIoProcessorPool需要進(jìn)行卸載乞娄。也即,我們需要將SimpleIoProcessorPool記錄下來显歧,再調(diào)用sessionSimpleIoProcessorPool.dispose();進(jìn)行卸載仪或。
最后的代碼長成這樣:
/**
* 停止監(jiān)聽服務(wù)
*/
public void dispose() {
if (acceptor !=null) {
acceptor.unbind();
acceptor.dispose();
CommunicationConfig networkConfig =configs.get(0);
LoggerUtil.info(logger, "銷毀channelCode:" + networkConfig.getChannelCode() +"端口:"
+ networkConfig.getPort() +"成功!");
}
if (sessionSimpleIoProcessorPool !=null) {
sessionSimpleIoProcessorPool.dispose();
}
}
/**
* 啟動(dòng)服務(wù)端監(jiān)聽
*
* @param listener
* @throws IOException
*/
public void startup()throws IOException {
CommunicationConfig networkConfig =configs.get(0);
/* 設(shè)置內(nèi)存獲取方式 */
IoBuffer.allocate(1024, false).setAutoExpand(true);
int availableProcessors = Runtime.getRuntime().availableProcessors();
sessionSimpleIoProcessorPool =new SimpleIoProcessorPool(NioProcessor.class, availableProcessors +1);
acceptor =new NioSocketAcceptor(CommunicationThreadPool.getSeperatorPool(networkConfig),
sessionSimpleIoProcessorPool);
// SSL連接屬性設(shè)置,責(zé)任鏈模式追迟,必須在解碼器鏈前面
initSSLConfig(networkConfig);
// 初始化Socket接收連接
initSocketAcceptorConfig(networkConfig);
acceptor.setReuseAddress(true);
acceptor.setHandler(TcpServerListenerContext.getListener(ConfigUtil
.getTcpKey(networkConfig)));
acceptor.bind(new InetSocketAddress(networkConfig.getPort()));
LoggerUtil.info(logger, "啟動(dòng)channelCode:" + networkConfig.getChannelCode() +"端口:"
+ networkConfig.getPort() +"成功溶其!");
}