2017-04-25 32 views
12

配置
Web服务器:Nginx的
应用服务器:Tomcat的200个请求服务线程
预期的响应时间,我的服务器的默认配置:< 30秒(有有很多第三方的依赖)重用tomcat的线程在等待“长”时间

场景
每隔10秒,应用程序将需要生成供其使用的令牌。令牌生成的预期时间约为5秒,但由于其第三方系统通过网络进行联系,这显然不一致,可能会降低到10秒。
在令牌生成过程中,每秒接近80%的传入请求需要等待。

什么,我相信应该发生
由于等待令牌生成的请求将不得不等待一个“长”的时候,没有理由为这些请求服务被重用服务,同时等待其他传入的请求令牌生成过程完成。
基本上,如果我的20%继续服务,这将是有道理的。如果等待的线程没有被用于其他请求,将会达到tomcat请求服务限制,并且服务器将基本上窒息,这是我并不真正兴奋发生的事情。

是我的尝试
最初我预期切换到Tomcat NIO连接器会做这项工作。但看看this比较后,我真的没有希望。尽管如此,我试图迫使这些请求等待10秒,但它不起作用。
现在我正在思考的问题是,我需要在请求的时候搁置请求,并且需要通知tomcat这个线程可以重用。同样,当需要向前移动时,我需要tomcat给它一个线程池中的线程。但我怎么做,或者即使这是可能的,我也不知情。

任何指导或帮助?

+0

你说:“在令牌生成过程中,每秒接近80%的传入请求需要等待*”,对于每个人来说,这80%的传入请求是传入请求还是应用程序或请求您已发送至第三方系统以生成令牌。我认为你需要在完整答案中澄清这一点,因为当你谈论哪件事情时,因为就像我说过的那样,对每个人都不明显,请澄清一下,你可能有更大的机会获得解决方案。 – hagrawal

+0

@hagrawal 80%的传入请求将等待第三方。 –

回答

6

您需要异步servlet,但您还需要对外部令牌生成器进行异步HTTP调用。如果您仍然在每个令牌请求的某个位置创建一个线程,您将无法通过将Servlet的请求传递给带有线程池的ExecutorService来获得任何收益。您必须从HTTP请求中分离线程,以便一个线程可以处理多个HTTP请求。这可以通过异步HTTP客户端(如Apache Asynch HttpClientAsync Http Client)来实现。

首先,你必须创建一个异步的servlet这样一个

public class ProxyService extends HttpServlet { 

    private CloseableHttpAsyncClient httpClient; 

    @Override 
    public void init() throws ServletException { 
     httpClient = HttpAsyncClients.custom(). 
       setMaxConnTotal(Integer.parseInt(getInitParameter("maxtotalconnections"))).    
       setMaxConnPerRoute(Integer.parseInt(getInitParameter("maxconnectionsperroute"))). 
       build(); 
     httpClient.start(); 
    } 

    @Override 
    public void destroy() { 
     try { 
      httpClient.close(); 
     } catch (IOException e) { } 
    } 

    @Override 
    public void doGet(HttpServletRequest request, HttpServletResponse response) { 
     AsyncContext asyncCtx = request.startAsync(request, response); 
     asyncCtx.setTimeout(ExternalServiceMock.TIMEOUT_SECONDS * ExternalServiceMock.K);  
     ResponseListener listener = new ResponseListener(); 
     asyncCtx.addListener(listener); 
     Future<String> result = httpClient.execute(HttpAsyncMethods.createGet(getInitParameter("serviceurl")), new ResponseConsumer(asyncCtx), null); 
    } 

} 

这个servlet进行使用Apache HttpClient的非同步异步HTTP调用。请注意,您可能需要配置每个路由的最大连接数,因为根据RFC 2616规范,默认情况下,HttpAsyncClient最多只允许同一主机的两个并发连接。还有很多其他选项可以配置,如HttpAsyncClient configuration中所示。 HttpAsyncClient创建起来非常昂贵,因此您不希望在每个GET操作中创建它的实例。

一个侦听器挂钩到AsyncContext,这个侦听器仅用于上面的例子来处理超时。

public class ResponseListener implements AsyncListener { 

    @Override 
    public void onStartAsync(AsyncEvent event) throws IOException { 
    } 

    @Override 
    public void onComplete(AsyncEvent event) throws IOException { 
    } 

    @Override 
    public void onError(AsyncEvent event) throws IOException { 
     event.getAsyncContext().getResponse().getWriter().print("error:"); 
    } 

    @Override 
    public void onTimeout(AsyncEvent event) throws IOException { 
     event.getAsyncContext().getResponse().getWriter().print("timeout:"); 
    } 

} 

然后你需要一个消费者的HTTP客户端。此消费者通过在buildResult()由HttpClient在内部执行时调用complete()来通知AsyncContext,作为将Future<String>返回给调用者ProxyService servlet的步骤。

public class ResponseConsumer extends AsyncCharConsumer<String> { 

    private int responseCode; 
    private StringBuilder responseBuffer; 
    private AsyncContext asyncCtx; 

    public ResponseConsumer(AsyncContext asyncCtx) { 
     this.responseBuffer = new StringBuilder(); 
     this.asyncCtx = asyncCtx; 
    } 

    @Override 
    protected void releaseResources() { } 

    @Override 
    protected String buildResult(final HttpContext context) { 
     try { 
      PrintWriter responseWriter = asyncCtx.getResponse().getWriter(); 
      switch (responseCode) { 
       case javax.servlet.http.HttpServletResponse.SC_OK: 
        responseWriter.print("success:" + responseBuffer.toString()); 
        break; 
       default: 
        responseWriter.print("error:" + responseBuffer.toString()); 
       } 
     } catch (IOException e) { } 
     asyncCtx.complete();   
     return responseBuffer.toString(); 
    } 

    @Override 
    protected void onCharReceived(CharBuffer buffer, IOControl ioc) throws IOException { 
     while (buffer.hasRemaining()) 
      responseBuffer.append(buffer.get()); 
    } 

    @Override 
    protected void onResponseReceived(HttpResponse response) throws HttpException, IOException {   
     responseCode = response.getStatusLine().getStatusCode(); 
    } 

} 

为ProxyService servlet的web.xml配置可以像

<?xml version="1.0" encoding="UTF-8"?> 
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xmlns="http://java.sun.com/xml/ns/javaee" 
     xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
     id="WebApp_ID" version="3.0" metadata-complete="true"> 
    <display-name>asyncservlet-demo</display-name> 

    <servlet> 
    <servlet-name>External Service Mock</servlet-name> 
    <servlet-class>ExternalServiceMock</servlet-class> 
    <load-on-startup>1</load-on-startup> 
    </servlet> 

    <servlet> 
    <servlet-name>Proxy Service</servlet-name> 
    <servlet-class>ProxyService</servlet-class> 
    <load-on-startup>1</load-on-startup> 
    <async-supported>true</async-supported> 
    <init-param> 
     <param-name>maxtotalconnections</param-name> 
     <param-value>200</param-value> 
    </init-param> 
    <init-param> 
     <param-name>maxconnectionsperroute</param-name> 
     <param-value>4</param-value> 
    </init-param> 
    <init-param> 
     <param-name>serviceurl</param-name> 
     <param-value>http://127.0.0.1:8080/asyncservlet/externalservicemock</param-value> 
    </init-param> 
    </servlet> 

    <servlet-mapping> 
    <servlet-name>External Service Mock</servlet-name> 
    <url-pattern>/externalservicemock</url-pattern> 
    </servlet-mapping> 

    <servlet-mapping> 
    <servlet-name>Proxy Service</servlet-name> 
    <url-pattern>/proxyservice</url-pattern> 
    </servlet-mapping> 

</web-app> 

并与在几秒钟的延迟令牌生成一个模拟的servlet可能是:

public class ExternalServiceMock extends HttpServlet{ 

    public static final int TIMEOUT_SECONDS = 13; 
    public static final long K = 1000l; 

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 
     Random rnd = new Random(); 
     try { 
      Thread.sleep(rnd.nextInt(TIMEOUT_SECONDS) * K); 
     } catch (InterruptedException e) { } 
     final byte[] token = String.format("%10d", Math.abs(rnd.nextLong())).getBytes(ISO_8859_1); 
     response.setContentType("text/plain"); 
     response.setCharacterEncoding(ISO_8859_1.name()); 
     response.setContentLength(token.length); 
     response.getOutputStream().write(token); 
    } 

} 

你可以获得fully working example at GitHub

1

这个问题实质上就是存在这么多“反应性”库和工具包的原因。

这不是一个问题,可以通过调整或更换tomcat连接器来解决。
您基本上需要删除所有阻塞IO调用,将其替换为非阻塞IO可能需要重新编写大部分应用程序。
您的HTTP服务器需要是非阻塞的,您需要使用非阻塞API(如servlet 3.1),并且您对第三方API的调用需要是非阻塞的。
像Vert.x和RxJava这样的库提供工具来帮助完成所有这些工作。

否则唯一的另一种方法是只是增加线程池的大小,操作系统已经采取调度CPU,使非活动线程不会造成太大的性能损失的照顾,但总有将是与被动方法相比,开销更大。

不知道更多关于您的应用程序很难提供有关具体方法的建议。

+0

嗯,我想这个问题归结为:'在什么情况下,tomcat开始重用线程,以及如何产生这种情况?'。还没有尝试过RxJava,但是一旦我运行示例代码就会更新。 –

+0

当线程的控制权返回给它时,Tomcat会重用该线程,在您的情况下,线程在第三方API的网络调用中被阻止。当然,您需要等待该调用的结果才能生成对客户端的响应。被动方法不是等待响应,而是设置回调函数来生成响应。这样你可以立即将线程的控制权返回给tomcat。 – Magnus

0

使用异步servlet请求或反应式库(如其他答案中所述)可以提供帮助,但需要进行主要的体系结构更改。

另一种选择是将令牌更新与令牌使用分开。

这里是一个天真的实现:

public class TokenHolder { 
    public static volatile Token token = null; 
    private static Timer timer = new Timer(true); 
    static { 
     // set the token 1st time 
     TokenHolder.token = getNewToken(); 

     // schedule updates periodically 
     timer.schedule(new TimerTask(){ 
      public void run() { 
       TokenHolder.token = getNewToken(); 
      } 
     }, 10000, 10000); 
    } 
} 

现在你的请求可以只使用TokenHolder.token访问服务。

在实际应用中,您可能会使用更高级的调度工具。