2017-04-25 40 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訪問服務。

在實際應用中,您可能會使用更高級的調度工具。