異步servlet的原理探究


異步servlet是servlet3.0開始支持的,對於單次訪問來講,同步的servlet相比異步的servlet在響應時長上並不會帶來變化(這也是常見的誤區之一),但對於高並發的服務而言異步servlet能增加服務端的吞吐量。本篇來從源碼角度上來探究為何說異步servlet能增加服務端的吞吐量的?

首先來個簡單的異步servlet的demo



@WebServlet(
    name = "asynchelloServlet",
    urlPatterns = {"/asynchello"},
    asyncSupported = true
)
public class AsyncHelloServlet extends HttpServlet {
    private static final ThreadPoolExecutor executor;

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        AsyncContext ctx = req.startAsync();
        executor.execute(() -> {
            System.out.println("AsyncHello Start->" + LocalDateTime.now());

            try {
                PrintWriter writer = ctx.getResponse().getWriter();
                writer.write("asyncHelloWorld");
            } catch (IOException var2) {
                var2.printStackTrace();
            }

            ctx.complete();
            System.out.println("AsyncHello End->" + LocalDateTime.now());
        });
    }

    static {
        executor = new ThreadPoolExecutor(10, 20, 5000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100));
    }
}


上面的代碼寫異步servlet的寫法最關鍵的就是

  • AsyncContext ctx = req.startAsync()
  • ctx.complete()

我們先講講下當邏輯進入servlet之前,tomcat經歷了哪些步驟:

  • tcp三次握手后
  • Acceptor線程處理 socket accept
  • Acceptor線程處理 注冊registered OP_READ到多路復用器
  • ClientPoller線程 監聽多路復用器的事件(OP_READ)觸發
  • 從tomcat的work線程池取一個工作線程來處理socket[http-nio-8080-exec-xx],下面幾個步驟也都是在work線程中進行處理的
  • 因為是http協議所以用Http11Processor來解析協議
  • CoyoteAdapter來適配包裝成Request和Response對象
  • 開始走pipeline管道(Valve),最后一個invoke的是把我們的servlet對象包裝的StandardWrapperValve管道

接下來就走到我們的servlet,由於是我們是異步的servlet,

1. req.startAsync()

   @Override
    public AsyncContext startAsync(ServletRequest request,
            ServletResponse response) {
        if (!isAsyncSupported()) {
            IllegalStateException ise =
                    new IllegalStateException(sm.getString("request.asyncNotSupported"));
            log.warn(sm.getString("coyoteRequest.noAsync",
                    StringUtils.join(getNonAsyncClassNames())), ise);
            throw ise;
        }

        if (asyncContext == null) {
            asyncContext = new AsyncContextImpl(this);
        }
        //修改狀態機
        asyncContext.setStarted(getContext(), request, response,
                request==getRequest() && response==getResponse().getResponse());
        asyncContext.setTimeout(getConnector().getAsyncTimeout());

        return asyncContext;
    }

從這里開始有2個線程我們要特別關注它們分別做了哪些事情:

  • tomcat的work線程
  • 我們自定義的業務線程

當在tomcat的work線程中調用startAsync(),會創建了一個異步的上下文(AsyncContext),並且異步的上下文(AsyncContext)會設置這個狀態機狀態為 STARTING, 然后把這個異步上下文放到了我們的自定義線程池中去執行,

對於異步的servlet,有一個專門的狀態機來控制:AsyncMachine,如下圖

image
image

那狀態機的扭轉控制肯定也做針對異步做了什么特殊處理

image
image

這里是一個Socket狀態的切換的處理邏輯,在異步servlet的時候是通過AsyncMachined的狀態來連動Socket狀態

如上圖異步狀態機的切換過程為:

DISPATCHED(初始)->STARTING->STARTED->COMPLETING

Socket的狀態的切換為:LONG

對於tomcat的work線程而言,servlet調用就結束了! 正常來說,如果是同步servlet的話,request和response會在servlet執行完成后由tomcat釋放掉!

image
image

異步的話 在這個時機request和response肯定不能釋放掉,釋放那不就沒得完了!

雖然Request和Response沒有釋放,但是這根work線程回到tomcat的線程池中去了(非核心線程的話那就釋放)。

2. ctx.complete()

回到我們的業務線程,處理完業務邏輯后,調用ctx.complete()


@Override
public void complete() {
    if (log.isDebugEnabled()) {
        logDebug("complete   ");
    }
    check();
    //更改異步狀態機
    request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null);
}

注意:COMPLETING是在我們的自定義的業務線程改變的!

修改狀態會觸發 新開一個tomcat工作線程 image

異步狀態狀態切換:

COMPLETING->DISPATCHED

Socket狀態切換為ASYNC_END

如下圖,一次異步的完整過程如下圖:

image
image

總結

研究了整個如何異步的過程,雖然這個狀態機的切換挺繞的,會發現在異步servlet中,最大的改變是為了盡快的釋放tomcat的work線程,讓它有機會請求新accept過來的請求,接受更多的請求,當在自定義線程池中處理好業務邏輯后,在去啟動新的tomcat的work線程來處理response,這樣不就很好理解了為什么說異步servlet能增加服務端的吞吐量了對吧!

思考:

  1.  SpringBoot的@EnableAsync背后是異步servlet嗎?​

  2.  servlet 3.1的non-blocking I/O 解決了3.0的什么問題?

關注公眾號一起學習


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM