前段時間我遇到了Servlet 3.0中AsyncContext.start(…)的目的是什么? 題。 引用上述方法的Javadoc :
使容器調度線程(可能從托管線程池中)運行指定的
Runnable
。 提醒大家,
AsyncContext
是Servlet 3.0規范中定義的一種標準方式,用于異步處理HTTP請求。 基本上,HTTP請求不再綁定到HTTP線程,這使我們以后可以使用更少的線程來處理它。 事實證明,該規范提供了一個API,用于處理其他不同線程池中的異步線程。 首先,我們將了解該功能在Tomcat和Jetty中是如何被完全破壞和無用的,然后我們將討論為什么該功能的用途普遍存在疑問。 我們的測試servlet只會在給定的時間內睡眠。 在正常情況下,這是可伸縮性的殺手,因為即使休眠的Servlet不會消耗CPU,但是與該特定請求綁定的休眠的HTTP線程也會消耗內存,并且其他傳入請求都無法使用該線程。 在我們的測試設置中,我將HTTP工作線程的數量限制為10個,這意味著即使應用程序本身幾乎完全處于空閑狀態,也只有10個并發請求完全阻塞了該應用程序(外部沒有響應)。 顯然,睡眠是可擴展性的敵人。
@WebServlet(urlPatterns = Array("/*"))
class SlowServlet extends HttpServlet with Logging {protected override def doGet(req: HttpServletRequest, resp: HttpServletResponse) {logger.info("Request received")val sleepParam = Option(req.getParameter("sleep")) map {_.toLong}TimeUnit.MILLISECONDS.sleep(sleepParam getOrElse 10)logger.info("Request done")}
}
對這段代碼進行基準測試可以發現,只要并發連接數低于HTTP線程數,平均響應時間就會接近
sleep
參數。 毫不奇怪,一旦我們超過HTTP線程數,響應時間就會開始增加。 第十一連接必須等待任何其他請求完成并釋放工作線程。 當并發級別超過100時,Tomcat開始斷開連接-太多的客戶端已排隊。 那么花哨的
AsyncContext.start()
方法又如何呢(不要與ServletRequest.startAsync()
混淆)? 根據JavaDoc,我可以提交任何Runnable
,并且容器將使用某些托管線程池來處理它。 這將在一定程度上有所幫助,因為我不再阻止HTTP工作線程(但仍使用servlet容器中某個位置的另一個線程)。 快速切換到異步servlet: @WebServlet(urlPatterns = Array("/*"), asyncSupported = true)
class SlowServlet extends HttpServlet with Logging {protected override def doGet(req: HttpServletRequest, resp: HttpServletResponse) {logger.info("Request received")val asyncContext = req.startAsync()asyncContext.setTimeout(TimeUnit.MINUTES.toMillis(10))asyncContext.start(new Runnable() {def run() {logger.info("Handling request")val sleepParam = Option(req.getParameter("sleep")) map {_.toLong}TimeUnit.MILLISECONDS.sleep(sleepParam getOrElse 10)logger.info("Request done")asyncContext.complete()}})}
}
我們首先啟用異步處理,然后簡單地將
sleep()
移至Runnable
并希望移至其他線程池中,從而釋放HTTP線程池。 快速壓力測試揭示了一些出乎意料的結果(此處:響應時間與并發連接數): 
猜猜是什么,響應時間與根本沒有異步支持的響應時間完全相同 (!)。仔細檢查后,我發現當調用
AsyncContext.start()
,Tomcat將給定的任務提交回……HTTP工作線程池,即用于所有HTTP請求! 基本上,這意味著我們發布了一個HTTP線程,只是為了在稍后的一毫秒內使用(甚至可能是同一線程)。 在Tomcat中調用AsyncContext.start()
絕對沒有好處。 我不知道這是錯誤還是功能。 一方面,這顯然不是API設計人員想要的。 假定servlet容器管理單獨的獨立線程池,因此HTTP工作線程池仍然可用。 我的意思是,異步處理的全部目的是逃避HTTP池。 Tomcat假裝將我們的工作委托給另一個線程,而它仍然使用原始的工作線程池。 那么,為什么我認為這是一個功能? 因為Jetty以完全相同的方式“破壞”了……無論它是按設計運行還是僅是不良的API實現,因此在Tomcat和Jetty中使用
AsyncContext.start()
都是沒有意義的,并且只會不必要地使代碼復雜化。 它不會給您任何好處,該應用程序在高負載下的工作原理完全相同,就好像根本沒有異步邏輯一樣。 但是,如何在正確的實現(例如IBM WAS)上使用此API功能呢? 更好,但API仍然沒有像在擴展性方面給我們帶來太多好處。 再次說明:異步處理的全部要點是能夠將HTTP請求與基礎線程分離,最好通過使用同一線程處理多個連接來實現。
AsyncContext.start()
將在單獨的線程池中運行提供的Runnable
。 您的應用程序仍然可以響應,可以處理普通的請求,而您決定異步處理的長期運行的請求則在單獨的線程池中處理。 更好的是,不幸的是線程池和每個連接線程的成語仍然是瓶頸。 對于JVM,啟動什么類型的線程都沒有關系–它們仍然占用內存。 因此,我們不再阻塞HTTP工作線程,但就我們可以支持的并發長期運行任務而言,我們的應用程序具有更大的可伸縮性。 在這個帶有休眠servlet的簡單,不現實的示例中,實際上,我們可以使用Servlet 3.0異步支持(只有一個額外的線程)并且不使用
AsyncContext.start()
來支持數千個并發(等待)連接。 你知道如何? 提示: ScheduledExecutorService
。 后記:斯卡拉善良
我差點忘了。 盡管示例是用Scala編寫的,但我還沒有使用任何出色的語言功能。 這是一個:隱式轉換。 使它在您的范圍內可用:
implicit def blockToRunnable[T](block: => T) = new Runnable {def run() {block}
}
突然之間,您可以使用代碼塊來代替手動和顯式實例化
Runnable
: asyncContext start {logger.info("Handling request")val sleepParam = Option(req.getParameter("sleep")) map { _.toLong}TimeUnit.MILLISECONDS.sleep(sleepParam getOrElse 10)logger.info("Request done")asyncContext.complete()
}
甜!
參考: Javax 和 servlet 社區的 JCG合作伙伴 Tomasz Nurkiewicz提供的javax.servlet.ServletRequest.startAsync()用途有限 。
翻譯自: https://www.javacodegeeks.com/2012/05/servletrequest-startasync-limited.html