Servlet 3.0 異步處理詳解

Github地址

相關係列文章:

Servlet 3.0 開始提供了AsyncContext用來支持異步處理請求,那麼異步處理請求到底能夠帶來哪些好處?

Web容器一般來說處理請求的方式是:爲每個request分配一個thread。我們都知道thread的創建不是沒有代價的,Web容器的thread pool都是有上限的。 那麼一個很容易預見的問題就是,在高負載情況下,thread pool都被佔着了,那麼後續的request就只能等待,如果運氣不好客戶端會報等待超時的錯誤。 在AsyncContext出現之前,解決這個問題的唯一辦法就是擴充Web容器的thread pool。

但是這樣依然有一個問題,考慮以下場景:

有一個web容器,線程池大小200。有一個web app,它有兩個servlet,Servlet-A處理單個請求的時間是10s,Servlet-B處理單個請求的時間是1s。 現在遇到了高負載,有超過200個request到Servlet-A,如果這個時候請求Servlet-B就會等待,因爲所有HTTP thread都已經被Servlet-A佔用了。 這個時候工程師發現了問題,擴展了線程池大小到400,但是負載依然持續走高,現在有400個request到Servlet-A,Servlet-B依然無法響應。

看到問題了沒有,因爲HTTP thread和Worker thread耦合在了一起(就是同一個thread),所以導致了當大量request到一個耗時操作時,就會將HTTP thread佔滿,導致整個Web容器就會無法響應。

但是如果使用AsyncContext,我們就可以將耗時的操作交給另一個thread去做,這樣HTTP thread就被釋放出來了,可以去處理其他請求了。

注意,只有使用AsyncContext才能夠達到上面所講的效果,如果直接new Thread()或者類似的方式的,HTTP thread並不會歸還到容器。

下面是一個官方的例子:

@WebServlet(urlPatterns={"/asyncservlet"}, asyncSupported=true)
public class AsyncServlet extends HttpServlet {
   /* ... Same variables and init method as in SyncServlet ... */

   @Override
   public void doGet(HttpServletRequest request, 
                     HttpServletResponse response) {
      response.setContentType("text/html;charset=UTF-8");
      final AsyncContext acontext = request.startAsync();
      acontext.start(new Runnable() {
         public void run() {
            String param = acontext.getRequest().getParameter("param");
            String result = resource.process(param);
            HttpServletResponse response = acontext.getResponse();
            /* ... print to the response ... */
            acontext.complete();
            }
      });
   }
}

陷阱

在這個官方例子裏,每個HTTP thread都會開啓另一個Worker thread來處理請求,然後把HTTP thread就歸還給Web容器。但是看AsyncContext.start()方法的javadoc:

Causes the container to dispatch a thread, possibly from a managed thread pool, to run the specified Runnable.

實際上這裏並沒有規定Worker thread到底從哪裏來,也許是HTTP thread pool之外的另一個thread pool?還是說就是HTTP thread pool?

The Limited Usefulness of AsyncContext.start()文章裏寫道:不同的Web容器對此有不同的實現,不過Tomcat實際上是利用HTTP thread pool來處理AsyncContext.start()的。

這也就是說,我們原本是想釋放HTTP thread的,但實際上並沒有,因爲有HTTP thread依然被用作Worker thread,只不過這個thread和接收請求的HTTP thread不是同一個而已。

這個結論我們也可以通過AsyncServlet1SyncServlet的Jmeter benchmark看出來,兩者的throughput結果差不多。啓動方法:啓動Main,然後利用Jmeter啓動benchmark.jmx(Tomcat默認配置下HTTP thread pool=200)。

使用ExecutorService

前面看到了Tomcat並沒有單獨維護Worker thread pool,那麼我們就得自己想辦法搞一個,見AsyncServlet2,它使用了一個帶Thread pool的ExecutorService來處理AsyncContext。

其他方式

所以對於AsyncContext的使用並沒有固定的方式,你可以根據實際需要去採用不同的方式來處理,爲此你需要一點Java concurrent programming的知識。

對於性能的誤解

AsyncContext的目的並不是爲了提高性能,也並不直接提供性能提升,它提供了把HTTP thread和Worker thread解藕的機制,從而提高Web容器的響應能力

不過AsyncContext在某些時候的確能夠提高性能,但這個取決於你的代碼是怎麼寫的。 比如:Web容器的HTTP thread pool數量200,某個Servlet使用一個300的Worker thread pool來處理AsyncContext。 相比Sync方式Worker thread pool=HTTP thread pool=200,在這種情況下我們有了300的Worker thread pool,所以肯定能夠帶來一些性能上的提升(畢竟幹活的人多了)。

相反,如果當Worker thread的數量<=HTTP thread數量的時候,那麼就不會得到性能提升,因爲此時處理請求的瓶頸在Worker thread。 你可以修改AsyncServlet2的線程池大小,把它和SyncServlet比較benchmark結果來驗證這一結論。

一定不要認爲Worker thread pool必須比HTTP thread pool大,理由如下:

  1. 兩者職責不同,一個是Web容器用來接收外來請求,一個是處理業務邏輯
  2. thread的創建是有代價的,如果HTTP thread pool已經很大了再搞一個更大的Worker thread pool反而會造成過多的Context switch和內存開銷
  3. AsyncContext的目的是將HTTP thread釋放出來,避免被操作長期佔用進而導致Web容器無法響應

所以在更多時候,Worker thread pool不會很大,而且會根據不同業務構建不同的Worker thread pool。 比如:Web容器thread pool大小200,一個慢速Servlet的Worker thread pool大小10,這樣一來,無論有多少請求到慢速操作,它都不會將HTTP thread佔滿導致其他請求無法處理。

相關資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章