Java 在等待时重用tomcat线程;“长”;时间

Java 在等待时重用tomcat线程;“长”;时间,java,multithreading,tomcat,nginx,Java,Multithreading,Tomcat,Nginx,配置 Web服务器:Nginx 应用服务器:Tomcat,默认配置为200个请求服务线程 我的服务器的预期响应时间:~30秒(存在大量第三方依赖项) 场景 每10秒,应用程序将需要生成令牌以供使用。令牌生成的预期时间约为5秒,但由于它是通过网络联系的第三方系统,这显然不一致,可能长达10秒。 在令牌生成过程中,每秒将近80%的传入请求需要等待 我认为应该发生的事 由于等待令牌生成的请求必须等待“很长”的时间,因此在等待令牌生成过程完成时,没有理由将这些服务的请求重新用于服务其他传入请求。 基本上

配置
Web服务器:Nginx
应用服务器:Tomcat,默认配置为200个请求服务线程
我的服务器的预期响应时间:~30秒(存在大量第三方依赖项)

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

我认为应该发生的事
由于等待令牌生成的请求必须等待“很长”的时间,因此在等待令牌生成过程完成时,没有理由将这些服务的请求重新用于服务其他传入请求。
基本上,如果我的20%能够继续得到服务,这是有道理的。如果等待的线程没有被用于其他请求,tomcat请求服务将达到极限,服务器将基本上阻塞,这并不是任何开发人员都会喜欢的

我尝试的
最初我希望切换到tomcat NIO连接器可以完成这项工作。但经过比较,我真的不抱希望。尽管如此,我还是试图强制请求等待10秒,但没有成功。
现在我在想,我需要在请求等待时暂时搁置请求,并且需要向tomcat发出信号,表示该线程可以自由重用。类似地,当请求准备向前移动时,我需要tomcat从其线程池中给我一个线程。但我对如何做到这一点,甚至是这一点是否可能,都是盲目的


有什么指导或帮助吗?

这个问题本质上就是存在如此多“反应式”库和工具包的原因

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

否则,唯一的替代方法就是增加线程池的大小,操作系统已经负责调度CPU,以便非活动线程不会造成太多性能损失,但与被动方法相比,总有更多的开销


如果不了解应用程序的更多信息,就很难就特定方法提供建议。

您需要一个异步servlet,但也需要对外部令牌生成器进行异步HTTP调用。如果您仍然在每个令牌请求的某处创建一个线程,那么通过将请求从servlet传递到带有线程池的ExecutorService,您将一无所获。您必须将线程与HTTP请求分离,以便一个线程可以处理多个HTTP请求。这可以通过异步HTTP客户端(如Apache或HTTP)实现

首先,您必须创建一个像这样的异步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);
    }

}
然后,您需要HTTP客户机的使用者。当HttpClient在内部执行
buildResult()
时,此使用者通过调用
complete()
通知AsyncContext,作为将
Future
返回给调用方
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();
    }

}

您可以使用异步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
即可访问服务


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

我想问题可以归结为:
tomcat在什么情况下开始重用线程,以及如何生成这种情况?
。还没有尝试过RxJava,但在运行示例代码后将进行更新。Tomcat在线程控制权返回给它时重用线程,在您的情况下,线程在对第三方API的网络调用中被阻塞。当然,您需要等待该调用的结果来生成对客户端的响应。反应式方法不是等待响应,而是设置回调函数来生成响应。通过这种方式,您可以立即将线程的控制权返回给tomcat。您说过“在令牌生成过程中,每秒将近80%的传入请求需要等待。”,对于每个人来说,这80%的传入请求是对您的应用程序的传入请求还是您发送给第三方系统以生成令牌的请求并不明显。我认为你需要在你的完整答案中澄清这一点,就像你在谈论什么一样,因为正如我所说的,这可能对每个人来说都不明显,请澄清,你可能有更大的机会得到解决。@hagrawal 80%的传入请求将等待第三方。
<?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>
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);
    }

}
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);
    }
}