设为首页 收藏本站
查看: 1183|回复: 0

[经验分享] Jetty 8长连接上的又一个坑

[复制链接]

尚未签到

发表于 2017-3-1 12:34:31 | 显示全部楼层 |阅读模式
  Jetty 8 长连接的超时断开连接的机制:超时连接机制针对IO传输过程中的数据阻塞时间超过一定阈值时,断开该连接。阻塞指当前处于数据传输阶段,但是连续指定时间内都没有发出或者接收到任何数据时,Jetty系统断开该连接。强调一下,只有在数据传输过程中才会有超时机制。在服务端处理已经收到的数据时是不会检测该超时时间的。
  下面看一下具体的代码实现。在jetty 8.1.17版本中,由以下代码控制一个连接的空闲、非空闲和断开检查方法,在SelectChannelEndpoint类中:



/* ------------------------------------------------------------ */
publicvoid setCheckForIdle(boolean check)
{
if (check)
    {
        _idleTimestamp=System.currentTimeMillis();
        _checkIdle=true;
    }
else
        _checkIdle=false;
}
/* ------------------------------------------------------------ */
publicboolean isCheckForIdle()
{
return _checkIdle;
}
/* ------------------------------------------------------------ */
protectedvoid notIdle()
{
    _idleTimestamp=System.currentTimeMillis();
}
/* ------------------------------------------------------------ */
publicvoid checkIdleTimestamp(long now)
{
if (isCheckForIdle() && _maxIdleTime>0)
    {
finallong idleForMs=now-_idleTimestamp;
if (idleForMs>_maxIdleTime)
        {
// Don't idle out again until onIdleExpired task completes.
            setCheckForIdle(false);
            _manager.dispatch(new Runnable()
            {
publicvoid run()
                {
try
                    {
                        onIdleExpired(idleForMs);
                    }
finally
                    {
                        setCheckForIdle(true);
                    }
                }
            });
        }
    }
}
  几个关键点地方:当数据传输的过程中,发现无法接收到和写出数据时,会调用setCheckForIdle(true)方法,从当前时间点开始计时,当后台select线程发现该连接的空闲时间达到阈值时,则调用onIdleExpired方法。还有一种场景是,在一个请求结束后,立即将该请求置为空闲状态。直到连接关闭或者该连接上面来了新的请求。另外,每个新的连接建立时,会在构造函数中默认调用一次该方法设置连接为空闲状态。
  在哪些情况下会调用相反的设置呢,即将该连接置为非空闲状态的setCheckForIdle(false)方法,和刷新当前的idle时间方法notIdle()呢?第一个方法每次收到一个请求的数据提交后端的servlet的时候调用,后一个方法在每次刷出或者读到数据时调用。这样确保后端的servlet在处理数据时,不至于因为处理时间过长而被自己的select线程给关闭了。
  这一次jetty的bug正是出在上述的每个请求的数据收集完成进入后端处理之前发生的。看如下代码:
  AsyncHttpConnection类中,handle方法:



@Override
public Connection handle() throws IOException
{
    Connection connection = this;
boolean some_progress=false;
boolean progress=true;
try
    {
        setCurrentConnection(this);
// don't check for idle while dispatched (unless blocking IO is done).
        _asyncEndp.setCheckForIdle(false);
// While progress and the connection has not changed
while (progress && connection==this)
        {
            progress=false;
try
            {
// Handle resumed request
if (_request._async.isAsync())
                {
if (_request._async.isDispatchable())
                       handleRequest();
                }
// else Parse more input
elseif (!_parser.isComplete() && _parser.parseAvailable())
                    progress=true;
// Generate more output
if (_generator.isCommitted() && !_generator.isComplete() && !_endp.isOutputShutdown() && !_request.getAsyncContinuation().isAsyncStarted())
if (_generator.flushBuffer()>0)
                        progress=true;
// Flush output
                _endp.flush();
// Has any IO been done by the endpoint itself since last loop
if (_asyncEndp.hasProgressed())
                    progress=true;
            }
catch (HttpException e)
            {
if (LOG.isDebugEnabled())
                {
                    LOG.debug("uri="+_uri);
                    LOG.debug("fields="+_requestFields);
                    LOG.debug(e);
                }
                progress=true;
                _generator.sendError(e.getStatus(), e.getReason(), null, true);
            }
finally
            {
                some_progress|=progress;
//  Is this request/response round complete and are fully flushed?
boolean parserComplete = _parser.isComplete();
boolean generatorComplete = _generator.isComplete();
boolean complete = parserComplete && generatorComplete;
if (parserComplete)
                {
if (generatorComplete)
                    {
// Reset the parser/generator
                        progress=true;
// look for a switched connection instance?
if (_response.getStatus()==HttpStatus.SWITCHING_PROTOCOLS_101)
                        {
                            Connection switched=(Connection)_request.getAttribute("org.eclipse.jetty.io.Connection");
if (switched!=null)
                                connection=switched;
                        }
                        reset();
// TODO Is this still required?
if (!_generator.isPersistent() && !_endp.isOutputShutdown())
                        {
                            LOG.warn("Safety net oshut!!!  IF YOU SEE THIS, PLEASE RAISE BUGZILLA");
                            _endp.shutdownOutput();
                        }
                    }
else
                    {
// We have finished parsing, but not generating so
// we must not be interested in reading until we
// have finished generating and we reset the generator
                        _readInterested = false;
                        LOG.debug("Disabled read interest while writing response {}", _endp);
                    }
                }
if (!complete && _request.getAsyncContinuation().isAsyncStarted())
                {
// The request is suspended, so even though progress has been made,
// exit the while loop by setting progress to false
                    LOG.debug("suspended {}",this);
                    progress=false;
                }
            }
        }
    }
finally
    {
        setCurrentConnection(null);
// If we are not suspended
if (!_request.getAsyncContinuation().isAsyncStarted())
        {
// return buffers
            _parser.returnBuffers();
            _generator.returnBuffers();
// reenable idle checking unless request is suspended
            _asyncEndp.setCheckForIdle(true);
        }
// Safety net to catch spinning
if (some_progress)
            _total_no_progress=0;
else
        {
            _total_no_progress++;
if (NO_PROGRESS_INFO>0 && _total_no_progress%NO_PROGRESS_INFO==0 && (NO_PROGRESS_CLOSE<=0 || _total_no_progress< NO_PROGRESS_CLOSE))
                LOG.info("EndPoint making no progress: "+_total_no_progress+" "+_endp+" "+this);
if (NO_PROGRESS_CLOSE>0 && _total_no_progress==NO_PROGRESS_CLOSE)
            {
                LOG.warn("Closing EndPoint making no progress: "+_total_no_progress+" "+_endp+" "+this);
if (_endp instanceof SelectChannelEndPoint)
                    ((SelectChannelEndPoint)_endp).getChannel().close();
            }
        }
    }
return connection;
}
  可以看到,在handle方法进入时,调用了一次:



// don't check for idle while dispatched (unless blocking IO is done).
_asyncEndp.setCheckForIdle(false);
  如果当前连接是一个短连接,那么这里调用完全没问题。请求处理完成后本来就可能立即断开连接。但是如果是一个长连接,该连接在处理完请求后,可能“休息”一段时间继续处理新的请求,那么就问题就来了,从该代码看,jetty在handle方法的while循环中处理多个请求,这样可以避免同一个连接上面的多个请求被分到不同的线程中处理,而是绑定在一个线程上面处理,当长连接上面的请求比较“密集”(请求之间间隔极短)时,该while会循环多次,有两种情况会进入该请求:1、一个请求上面的数据没有处理完,即



// else Parse more input
elseif (!_parser.isComplete() && _parser.parseAvailable())
progress=true;
  这个代码控制的。
  另外当一个请求处理完了,也会在finally里面走到progess=true上面。



//  Is this request/response round complete and are fully flushed?
boolean parserComplete = _parser.isComplete();
boolean generatorComplete = _generator.isComplete();
boolean complete = parserComplete && generatorComplete;
if (parserComplete)
{
if (generatorComplete)
    {
// Reset the parser/generator
        progress=true;


  由这个控制。
  问题出在第二个上面,当一个请求处理完成后,连接会被置为空闲状态。但是这里将progess设置为true,那么while循环立即准备读取下一个请求的数据,但是并没有将连接置为非空闲状态,此时如果服务端进入耗时较长的处理流程,那么可能不等到客户端超时,连接就被后台检查空闲连接的线程断开了。
  因此这里很明显,jetty有bug,应该在最后的这段代码出补充



// don't check for idle while dispatched (unless blocking IO is done).
_asyncEndp.setCheckForIdle(false);
  这个调用。或者是在每次进入while循环时调用,而不是只在进入handle时调用。
  该问题发生有几个关键点:长连接上面持续不断有新请求过来,并且新请求发起的时间距离上一个请求完成的时间间隔非常短。经过实测,python的http客户端在处理长连接上面,请求间隔非常短。而其他语言和库编写的客户端测试程序都有比较长的间隔,导致问题不易重现。附一个jetty的简易http长连接测试程序:



import httplib  
count=0
conn = httplib.HTTPConnection("127.0.0.1", timeout=600)  
while (count < 1000000):
    conn.request("PUT","/")
    res = conn.getresponse()  
    print res.status, res.reason  
    print res.read()   
    count += 1
  在jetty上面讲超时时间配置尽可能短,在servlet里面处理请求时休眠一个大于等于超时时间的值,配合上述客户端,很容易重现问题。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-348835-1-1.html 上篇帖子: 嵌入式服务器jetty,让你更快开发web 下篇帖子: eclipse内嵌jetty(run-jetty-run插件) 配置jndi数据源
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表