天黑請(qǐng)閉眼 : 問(wèn)題背景
在這樣一個(gè)難得陽(yáng)光燦爛的上午,大家都沐浴在清晨的清爽之中的時(shí)候……
好吧,我編不下去了,其實(shí)就是大清早被通知說(shuō)我們的消息顯示的不正確颅围,然后開(kāi)始排查問(wèn)題,經(jīng)由定位之后發(fā)現(xiàn)問(wèn)題在于MQ消息沒(méi)有發(fā)送成功恨搓,導(dǎo)致生成的信息有缺失,然而悲劇的是禍不單行筏养,由于之前配置日志的問(wèn)題導(dǎo)致了歷史日志的丟失斧抱。唯一的線索就是當(dāng)時(shí)的一個(gè)同事給另一個(gè)同事的錯(cuò)誤信息截圖,如下:
于是就此,我們展開(kāi)了調(diào)查……
狼人請(qǐng)殺人 : 錯(cuò)誤位置
首先我們開(kāi)始對(duì)可疑的問(wèn)題點(diǎn)開(kāi)始反推茎辐,鑒于存在錯(cuò)誤信息的線索宪郊,我們分析了MQ Client的源碼,發(fā)現(xiàn)了拋出這個(gè)異常的位置:
/**
* Check if the session is closed. It is used for ensuring that the session
* is open before performing various operations.
*
* @throws IllegalStateException if the Session is closed
*/
protected void checkClosed() throws IllegalStateException {
if (closed) {
throw new IllegalStateException("The Session is closed");
}
}
結(jié)合我們具體的業(yè)務(wù)日志代碼拖陆,以及數(shù)據(jù)庫(kù)記錄弛槐,反推具體的問(wèn)題發(fā)生點(diǎn)應(yīng)該在于我們對(duì)MQ發(fā)送消息的功能封裝類位置:
/**
* 注意提交事務(wù) 方法commit()
*
* queueName此處用于初始化,防止static對(duì)象先初始化時(shí)候出現(xiàn)空指針的問(wèn)題
*
* @param queueName
* @param msg
* @throws JMSException
*/
public void sendMsg(String queueName, JSONObject messageJson) {
try {
if (null == producer) {
init(queueName);// 每一個(gè)queue分別實(shí)例化自己的參數(shù)
}
if (null != producer) {
//統(tǒng)一增加queueName
messageJson.put("queueName", queueName);
String msg = messageJson.toString();
// **警察叔叔,就是這里**
message = session.createTextMessage(msg);
if (null != message) {
producer.send(message);
mqLog.info("MQProducer, sendMsg succ, queueName:" + queueName + ", msg:" + msg);
} else {
throw new JMSException(" 消息未成功發(fā)送: " + msg);
}
} else {
throw new JMSException(" 消息未成功發(fā)送: " + messageJson.toString());
}
} catch (Exception exp) {
exp.printStackTrace();
Log4jUtil.exception(exp);
mqLog.error(messageJson.toString());
}
}
預(yù)言家請(qǐng)驗(yàn)人 : 錯(cuò)誤原因
鑒于這里就明確了是Session的調(diào)用時(shí)依啰,由于內(nèi)部的closed值為TRUE導(dǎo)致了這個(gè)異常信息的打印乎串,因此我們繼續(xù)通過(guò)源碼排查了這個(gè)值會(huì)被寫為TRUE的情況,發(fā)現(xiàn)是在于如下釋放(類似于析構(gòu)的情況)的時(shí)候才會(huì)導(dǎo)致這個(gè)值的變化:
public synchronized void dispose() throws JMSException {
if (!closed) {
try {
executor.close();
for (Iterator<ActiveMQMessageConsumer> iter = consumers.iterator(); iter.hasNext();) {
ActiveMQMessageConsumer consumer = iter.next();
consumer.setFailureError(connection.getFirstFailureError());
consumer.dispose();
lastDeliveredSequenceId = Math.max(lastDeliveredSequenceId, consumer.getLastDeliveredSequenceId());
}
consumers.clear();
for (Iterator<ActiveMQMessageProducer> iter = producers.iterator(); iter.hasNext();) {
ActiveMQMessageProducer producer = iter.next();
producer.dispose();
}
producers.clear();
try {
if (getTransactionContext().isInLocalTransaction()) {
rollback();
}
} catch (JMSException e) {
}
} finally {
connection.removeSession(this);
this.transactionContext = null;
// ** 警察叔叔速警,就是這個(gè)人叹誉! **
closed = true;
}
}
}
因此想到可能是由于鏈接被釋放了才導(dǎo)致了這個(gè)問(wèn)題,開(kāi)始在MQ Server端找對(duì)應(yīng)日志闷旧,于是發(fā)現(xiàn)了如下錯(cuò)誤信息长豁,由于心跳(或其它)的超時(shí),導(dǎo)致服務(wù)器端主動(dòng)釋放了相關(guān)的鏈接:
天亮了 : 根因分析
由此我們得到大致的結(jié)論忙灼,由于我們本身的代碼里面采用的是單例模式匠襟,造成了Session值只會(huì)在一開(kāi)始創(chuàng)建Producer之后就保持住。因此雖然服務(wù)器端釋放后缀棍,我們Client端的Session值還是使用了之前的值宅此,導(dǎo)致了這個(gè)異常被拋出。
也就是說(shuō)MQ本身的程序在發(fā)現(xiàn)異常之后開(kāi)始清理相關(guān)的數(shù)據(jù)爬范,并設(shè)置相關(guān)的狀態(tài)(把Closed變量置為TRUE)父腕。而我們的程序由于還是引用到了之前的對(duì)象上,因此在訪問(wèn)Closed變量時(shí)發(fā)現(xiàn)出錯(cuò)了青瀑,因此拋出異常璧亮,導(dǎo)致我們之后的發(fā)送失敗
Session在Producer中的聲明如下:
// 這里的Producer是單例模式的
// Session一旦創(chuàng)建之后是不會(huì)更新的
protected class MQProducer {
private ActiveMQConnectionFactory factory = null;
private Connection connection = null;
private Session session = null;
// 以下省略若干行
...
請(qǐng)投票 : 問(wèn)題解決
那么問(wèn)題清楚了之后,還是不明確為什么會(huì)導(dǎo)致這種異常釋放枝嘶,突然小伙伴們想起了之前收到的一封郵件帘饶,于是成功發(fā)現(xiàn)了兇手(核對(duì)日志時(shí)間和機(jī)器,完全一致)……
這個(gè)故事告訴我們一定要注意看郵件啊群扶,以及阿里的監(jiān)控系統(tǒng)好犀利凹翱獭!
當(dāng)然我們自身的代碼中竞阐,對(duì)這方面理解也不夠深入缴饭,主要在于:
- 捕獲這種異常之后,僅僅是簡(jiǎn)單的記錄了相關(guān)的log骆莹,其實(shí)更優(yōu)雅的方式是針對(duì)這種錯(cuò)誤作出健壯性的反饋(當(dāng)如具體的方式和策略還得再深入理解相關(guān)的機(jī)制和相關(guān)實(shí)現(xiàn))
- 單例模式颗搂,有自身的好處,但是Session固定之后是否合適幕垦,還需要結(jié)合第1點(diǎn)來(lái)具體給出策略
結(jié)局 : 寫在最后
僅以此記錄一下的問(wèn)題丢氢,防止之后忘記了。
也希望能給有類似問(wèn)題發(fā)生的小伙伴們一些線索先改。
have fun~ :)