问题一:http status 500
描述:
在项目切换tomcat7的时候,原来通过使用amq实现的消息推送出现了问题,
amq.js发送的大部分请求都是500,只有少部分才是200。
分析:
在Servlet2.5容器下, servlet组件默认支持异步通信, 但是到Servlet3.0的时候需要手动进行开启支持。经过排查发现, 配置参数[async-supported]来开启这个异步支持。
解决:
1.刚开始只在amq的后端Servlet组件[AjaxServlet]中配置,发现依然会出现500错误。原来只要请求经过的Filter、Servlet都需要增加配置才行。
2.简单粗暴点:
在web.xml中所有Filter、Servlet都增加[async-supported]支持。或者找到该请求的过滤器链,增加相应的也行【比较麻烦】
<servlet>
<servlet-name>AjaxServlet</servlet-name>
<servlet-class>org.apache.activemq.web.AjaxServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
问题二:Amq请求超时
描述:
在解决了第一个问题后,实际使用中又出现了amq【版本5.8.0】请求经常会超时,js报错【
Serverconnection dropped.】导致后台推送的消息不能实时更新,但偶尔又很正常。
分析:
看到问题后的第一反应会不会是Apache的请求超时时间太短造成的,但是查看Apache的timeout参数及amq的超时参数,明显前者大于后者。按理说请求不至于达到Apache的超时时间而被拒绝。带着这个问题,先看下amq的前端组件的实现,发现它是在请求之前先建立,如下代码所示:
var sendPoll = function(reCon) {
if (reCon) {
reConnect = reCon;
}
// Workaround IE6 bug where it caches the response
// Generate a unique query string with date and random
var now = new Date();
var timeoutArg = sessionInitialized ? timeout : 0.001;
var data = 'timeout=' + timeoutArg * 1000
+ '&d=' + now.getTime()
+ '&r=' + Math.random();
var successCallback = sessionInitialized ? pollHandler : initHandler;
var options = {
method: 'get',
data: addClientId( data ),
success: successCallback,
error: pollErrorHandler
};
adapter.ajax(uri, options);
};
通过调试工具拦截请求发现,参数timeout的值都是1,这就说明初次建立连接就是失败,后面的请求更不用说了。
这个时候我们需要看一下AjaxServlet的实现,这里先说一个Continuation机制,它是建立在NIO基础上,允许被"suspend"(挂起)和"rsueme"(继续、恢复)。在suspend之前一般需要设置timeout来设置阻塞时间, 同时“Continuation”对象会提供对应的监听对象来处理事件是否“超时”或者“完成”。而在Servlet3.0的环境下,MessageServletSupport中的Continuation使用的是Servlet3Continuation,而这个Continuation只有addContinuationListener方法会设置超时,我们可以看下代码:
代码2.1:MessageServletSupport中的doMessages方法
if (message == null && client.getListener().getUndeliveredMessages().size() == 0) {
Continuation continuation = ContinuationSupport.getContinuation(request);
if (continuation.isExpired()) {
response.setStatus(HttpServletResponse.SC_OK);
StringWriter swriter = new StringWriter();
PrintWriter writer = new PrintWriter(swriter);
writer.println("<ajax-response>");
writer.print("</ajax-response>");
writer.flush();
String m = swriter.toString();
response.getWriter().println(m);
return;
}
continuation.setTimeout(timeout);
continuation.suspend();
LOG.debug( "Suspending continuation " + continuation );
// Fetch the listeners
AjaxListener listener = client.getListener();
listener.access();
// register this continuation with our listener.
listener.setContinuation(continuation);
return;
}
这里当没有消息时,并不会调用Servlet3Continuation的设置超时方法,也就是无法从“suspend”状态恢复。导致请求一直闲置到Apache的请求超时时间, 同时造成amq客户端无法完成初始化标识。
从中可以看出amq客户端的初始化只不过是发出一个请求, 设置一个超级短的时间, 但后台的流程跟正常的实时信息获取一致。 这就可以解释一个现象: 如果刚初始化的时候,ActiveMq对应的队列刚好有对应信息, 则会正确返回初始化标志被正确设置。 但是随后如果无法获取到队列数据, 则一直处于等待直到apache拒绝。
通过上述的排查发现, 我们有理由怀疑是不是当前ActiveMq组件包的“Continuation”实现有问题。
解决:
通过查找资料,发现这个是amq的一个 AMQ-3447, 该bug在5.9.0中修复,所以升级MQ版本即可。在5.9.0的 MessageServletSupport中的 doMessages方法中我们可以看下代码: if (message == null && client.getListener().getUndeliveredMessages().size() == 0) {
Continuation continuation = ContinuationSupport.getContinuation(request);
// Add a listener to the continuation to make sure it actually
// will expire (seems like a bug in Jetty Servlet 3 continuations,
// see https://issues.apache.org/jira/browse/AMQ-3447
continuation.addContinuationListener(new ContinuationListener() {
@Override
public void onTimeout(Continuation cont) {
if (LOG.isDebugEnabled()) {
LOG.debug("Continuation " + cont.toString() + " expired.");
}
}
@Override
public void onComplete(Continuation cont) {
if (LOG.isDebugEnabled()) {
LOG.debug("Continuation " + cont.toString() + " completed.");
}
}
});
if (continuation.isExpired()) {
response.setStatus(HttpServletResponse.SC_OK);
StringWriter swriter = new StringWriter();
PrintWriter writer = new PrintWriter(swriter);
writer.println("<ajax-response>");
writer.print("</ajax-response>");
writer.flush();
String m = swriter.toString();
response.getWriter().println(m);
return;
}
continuation.setTimeout(timeout);
continuation.suspend();
LOG.debug( "Suspending continuation " + continuation );
// Fetch the listeners
AjaxListener listener = client.getListener();
listener.access();
// register this continuation with our listener.
listener.setContinuation(continuation);
return;
}
这里通过设置ContinuationListener来保证当超时或者完成时能够返回给前台一个空的Ajax报文,不至于出现超时情况。