qileilove

          blog已經(jīng)轉(zhuǎn)移至github,大家請?jiān)L問 http://qaseven.github.io/

          使用異步Servlet改進(jìn)應(yīng)用性能

          Nikita Salnikov Tarnovski是plumbr的高級開發(fā)者,也是一位應(yīng)用性能調(diào)優(yōu)的專家,他擁有多年的性能調(diào)優(yōu)經(jīng)驗(yàn)。近日,Tarnovski撰文談到了如何通過異步Servlet來改進(jìn)常見的Java Web應(yīng)用的性能問題。
            眾所周知,Servlet 3.0標(biāo)準(zhǔn)已經(jīng)發(fā)布了很長一段時(shí)間,相較于之前的2.5版的標(biāo)準(zhǔn),新標(biāo)準(zhǔn)增加了很多特性,比如說以注解形式配置Servlet、web.xml片段、異步處理支持、文件上傳支持等。雖然說現(xiàn)在的很多Java Web項(xiàng)目并不會直接使用Servlet進(jìn)行開發(fā),而是通過如Spring MVC、Struts2等框架來實(shí)現(xiàn),不過這些Java Web框架本質(zhì)上還是基于傳統(tǒng)的JSP與Servlet進(jìn)行設(shè)計(jì)的,因此Servlet依然是最基礎(chǔ)、最重要的標(biāo)準(zhǔn)和組件。在Servlet 3.0標(biāo)準(zhǔn)新增的諸多特性中,異步處理支持是令開發(fā)者最為關(guān)注的一個(gè)特性,本文就將詳細(xì)對比傳統(tǒng)的Servlet與異步Servlet在開發(fā)上、使用上、以及最終實(shí)現(xiàn)上的差別,分析異步Servlet為何會提升Java Web應(yīng)用的性能。
            本文主要介紹的是能夠解決現(xiàn)代Web應(yīng)用常見性能問題的一種性能優(yōu)化技術(shù)。當(dāng)今的應(yīng)用已經(jīng)不僅僅是被動地等待瀏覽器來發(fā)起請求,而是由應(yīng)用自身發(fā)起通信。典型的示例有聊天應(yīng)用、拍賣系統(tǒng)等等,實(shí)際情況是大多數(shù)時(shí)間與瀏覽器的連接都是空閑的,等待著某個(gè)事件來觸發(fā)。
            這種類型的應(yīng)用自身存在著一個(gè)問題,特別是在高負(fù)載的情況下問題會變得更為嚴(yán)重。典型的癥狀有線程饑餓、影響用戶交互等等。根據(jù)近一段時(shí)間的經(jīng)驗(yàn),我認(rèn)為可以通過一種相對比較簡單的方案來解決這個(gè)問題。在Servlet API 3.0實(shí)現(xiàn)成為主流后,解決方案就變得更加簡單、標(biāo)準(zhǔn)化且優(yōu)雅了。
            在開始介紹解決方案前,我們應(yīng)該更深入地理解問題的細(xì)節(jié)。還有什么比看源代碼更直接的呢,下面就來看看下面這段代碼:
          @WebServlet(urlPatterns = "/BlockingServlet")
          public class BlockingServlet extends HttpServlet {
          private static final long serialVersionUID = 1L;
          protected void doGet(HttpServletRequest request,
          HttpServletResponse response) throws ServletException, IOException {
          try {
          long start = System.currentTimeMillis();
          Thread.sleep(2000);
          String name = Thread.currentThread().getName();
          long duration = System.currentTimeMillis() - start;
          response.getWriter().printf("Thread %s completed the task in %d ms.", name, duration);
          } catch (Exception e) {
          throw new RuntimeException(e.getMessage(), e);
          }
          }
            上面這個(gè)Servlet主要完成以下事情:
            請求到達(dá),表示開始監(jiān)控某些事件。
            線程被阻塞,直到事件發(fā)生為止。
            在接收到事件后,編輯響應(yīng)然后將其發(fā)回給客戶端。
            為了簡化,代碼中將等待部分替換為一個(gè)Thread.sleep()調(diào)用。
            現(xiàn)在,你可能會覺得這就是一個(gè)挺不錯(cuò)的Servlet。在很多情況下,你的理解都是正確的,上述代碼并沒有什么問題,不過當(dāng)應(yīng)用的負(fù)載變大后就不是這么回事了。
            為了模擬負(fù)載,我通過JMeter創(chuàng)建了一個(gè)簡單的測試,我會啟動2,000個(gè)線程,每個(gè)線程運(yùn)行10次,每次都會向/BlockedServlet這個(gè)地址發(fā)出請求。將這個(gè)Servlet部署在Tomcat 7.0.42中然后運(yùn)行測試,得到如下結(jié)果:
            平均響應(yīng)時(shí)間:19,324ms
            最快響應(yīng)時(shí)間:2,000ms
            最慢響應(yīng)時(shí)間:21,869ms
            吞吐量:97個(gè)請求/秒
            默認(rèn)的Tomcat配置有200個(gè)工作線程,此外再加上模擬的工作由2,000ms的睡眠時(shí)間來表示,這就能比較好地解釋最快與最慢的響應(yīng)時(shí)間了,每個(gè)線程都會睡眠2秒鐘。再加上上下文切換的代價(jià),因此97個(gè)請求/秒的吞吐量基本上是符合我們的預(yù)期的。
            對于絕大多數(shù)的應(yīng)用來說,這個(gè)吞吐量還算是可以接受的。重點(diǎn)來看看最慢的響應(yīng)時(shí)間與平均響應(yīng)時(shí)間,問題就變得有些嚴(yán)重了。經(jīng)過20秒而不是期待的2秒才能得到響應(yīng)顯然會讓用戶感到非常不爽。 下面我們來看看另外一種實(shí)現(xiàn),利用Servlet API 3.0的異步支持:
          @WebServlet(asyncSupported = true, value = "/AsyncServlet")
          public class AsyncServlet extends HttpServlet {
          private static final long serialVersionUID = 1L;
          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          Work.add(request.startAsync());
          }
          }
          public class Work implements ServletContextListener {
          private static final BlockingQueue queue = new LinkedBlockingQueue();
          private volatile Thread thread;
          public static void add(AsyncContext c) {
          queue.add(c);
          }
          @Override
          public void contextInitialized(ServletContextEvent servletContextEvent) {
          thread = new Thread(new Runnable() {
          @Override
          public void run() {
          while (true) {
          try {
          Thread.sleep(2000);
          AsyncContext context;
          while ((context = queue.poll()) != null) {
          try {
          ServletResponse response = context.getResponse();
          response.setContentType("text/plain");
          PrintWriter out = response.getWriter();
          out.printf("Thread %s completed the task", Thread.currentThread().getName());
          out.flush();
          } catch (Exception e) {
          throw new RuntimeException(e.getMessage(), e);
          } finally {
          context.complete();
          }
          }
          } catch (InterruptedException e) {
          return;
          }
          }
          }
          });
          thread.start();
          }
          @Override
          public void contextDestroyed(ServletContextEvent servletContextEvent) {
          thread.interrupt();
          }
          }
            上面的代碼看起來有點(diǎn)復(fù)雜,因此在開始分析這個(gè)解決方案的細(xì)節(jié)信息之前,我先來概述一下這個(gè)方案:速度上提升了75倍,吞吐量提升了20倍。看到這個(gè)結(jié)果,你肯定迫不及待地想知道這個(gè)示例是如何做到的吧。
            這個(gè)Servlet本身是非常簡單的。需要注意兩點(diǎn),首先是聲明Servlet支持異步方法調(diào)用:
            @WebServlet(asyncSupported = true, value = "/AsyncServlet")
            其次,重要的部分實(shí)際上是隱藏在下面這行代碼調(diào)用中的。
            Work.add(request.startAsync());
            整個(gè)請求處理都被委托給了Work類。請求上下文是通過AsyncContext實(shí)例來保存的,它持有容器提供的請求與響應(yīng)對象。
            現(xiàn)在來看看第2個(gè),也是更加復(fù)雜的類,Work類實(shí)現(xiàn)了ServletContextListener接口。進(jìn)來的請求會在該實(shí)現(xiàn)中排隊(duì)等待通知,通知可能是上面提到的拍賣中的競標(biāo)價(jià),或是所有請求都在等待的群組聊天中的下一條消息。
            當(dāng)通知到達(dá)時(shí),我們這里依然是通過Thread.sleep()讓線程睡眠2,000ms,隊(duì)列中所有被阻塞的任務(wù)都是由一個(gè)工作線程來處理的,該線程負(fù)責(zé)編輯與發(fā)送響應(yīng)。相對于阻塞成百上千個(gè)線程以等待外部通知,我們通過一種更加簡單且干凈的方式達(dá)成所愿,通過批處理在單獨(dú)的線程中處理請求。
            還是讓結(jié)果來說話吧,測試配置與方才的示例一樣,依然使用Tomcat 7.0.24的默認(rèn)配置,測試結(jié)果如下所示:
            平均響應(yīng)時(shí)間:265ms
            最快響應(yīng)時(shí)間:6ms
            最慢響應(yīng)時(shí)間:2,058ms
            吞吐量:1,965個(gè)請求/秒
            雖然說這個(gè)示例很簡單,不過對于實(shí)際項(xiàng)目來說通過這種方式依然能獲得類似的結(jié)果。
            在將所有的Servlet改寫為異步Servlet前,請容許我多說幾句。該解決方案非常適合于某些應(yīng)用場景,比如說群組通知與拍賣價(jià)格通知等。不過,對于等待數(shù)據(jù)庫查詢完成的請求來說,這種方式就沒有什么必要了。像往常一樣,我必須得重申一下——請通過實(shí)驗(yàn)進(jìn)行度量,而不是瞎猜。
            對于那些不適合于這種解決方案的場景來說,我還是要說一下這種方式的好處。除了在吞吐量與延遲方面帶來的顯而易見的改進(jìn)外,這種方式還可以在大負(fù)載的情況下優(yōu)雅地避免可能出現(xiàn)的線程饑餓問題。
            另一個(gè)重要的方面,這種異步處理請求的方式已經(jīng)是標(biāo)準(zhǔn)化的了。它不依賴于你所使用的Servlet API 3.0,兼容于各種應(yīng)用服務(wù)器,如Tomcat 7、JBoss 6或是Jetty 8等,在這些服務(wù)器上這種方式都可以正常使用。你不必再面對各種不同的Comet實(shí)現(xiàn)或是依賴于平臺的解決方案了,比如說Weblogic FutureResponseServlet。
            就如本文一開始所提的那樣,現(xiàn)在的Java Web項(xiàng)目很少會直接使用Servlet API進(jìn)行開發(fā)了,不過諸多的Web MVC框架都是基于Servlet與JSP標(biāo)準(zhǔn)實(shí)現(xiàn)的,那么在你的日常開發(fā)中,是否使用過出現(xiàn)多年的Servlet API 3.0,使用了它的哪些特性與API呢?

          posted on 2014-06-09 09:59 順其自然EVO 閱讀(235) 評論(0)  編輯  收藏 所屬分類: 測試學(xué)習(xí)專欄

          <2014年6月>
          25262728293031
          1234567
          891011121314
          15161718192021
          22232425262728
          293012345

          導(dǎo)航

          統(tǒng)計(jì)

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 梁平县| 侯马市| 福建省| 上思县| 白山市| 云和县| 郓城县| 龙井市| 定远县| 南华县| 广德县| 巩留县| 关岭| 四子王旗| 灌南县| 太仓市| 吉林省| 石家庄市| 普兰店市| 斗六市| 聂拉木县| 夏邑县| 武义县| 车致| 都兰县| 社旗县| 白河县| 文登市| 普兰县| 孙吴县| 射洪县| 宜川县| 安阳县| 白玉县| 开远市| 镇江市| 凌源市| 遂溪县| 赤壁市| 县级市| 洱源县|