Servlet’in Non-Blocking Serüveni

Beklentimiz her zaman daha az kaynakla daha fazla ve daha hızlı iş yapabilmektir. Bunu sağlayabilmek için sistem kaynaklarını neyin tükettiğini, nerelerde darboğazların oluştuğunu inceleriz.

En temelde süreç client ve server arasında gerçekleşir. İşlemlerin hızlı gerçekleşmesi için hem server’in hemde client’in sorumlulukları vardır. Serverde işler yolunda gitmiyorsa, işlemin tamamlanması için başka bir kaynağı bekliyorsa hızı kaybederiz, kaynak tüketimi artar ya da client yavaş bir networkte çalışıyorsa serverden gelen datayı çabucak alıp işlemi tamamlayamaz.

Servlet tabanlı HTTP protokolü için client-server iletişimi esnasında iletişimi 2 aşamada değerlendirmek gerelidir.

  1. Client ve server arasındaki iletişim
  2. Server ve servlet arasındaki iletişim

Tomcat 6 (servlet 2.5) ya kadar default HTTP connector syncronous olarak çalışmaktadır. Yani request başına bir thread dedike edilmektedir. 100 concurrent user 100 aktif thread gerektirmektedir. Bu durumda 100 thread kapasiteli bir HTTP connector 101. request geldiğinde talebi karşılayamaz durumda olur, bekletir. Bu model thread per connection olarak adlandırılmaktadır.

 

Tomcat 6 ile geliştirilen Http11NioProtocol adlı yeni HTTP connetoru connectionları karşılama şeklini NIO(non-blocking io) prensibi ile farklı ve daha efektif hale getirmiştir. Yeni connector 2 seviyeli thread pool’a sahiptir. 1. seviyede connectionları kabul eden ve canlı tutan thredlar bulunmaktadır. Bu threadlerin amacı connection taleplerini alıp 2. seviyedeki worker threadlere aktarmaktır. Connection ile request arasındaki thread idle duruma düştüğünde, yani request processing işlemi başladı ise tekrardan yeni talep kabul etmek için 1. seviye poola bırakılır. “Thread per request” olarak adlandırılan bu model sabit thread pool ile yüksek kullanıcı bağlantılarını yönetmeyi sağlar. Model threadleri paylaşarak concurrent olarak daha fazla kullanıcı talebini karşılamayı sağlar.

1. seviyedeki threadlar “Acceptor Thread(s)” olarak adlandırılırlar ve CPU core bazında default olarak size’ı belirlenir.

2. seviyedeki threadlar ise “Request Processing Threads” olarak adlandırılırlar ve HTTP requestlerini handle edip işlemektir.

Bu modelin çalışma şeklini daha detaylı anlamak için Java NIO API incelenebilir.

Buradaki NIO yapı sadece client-server arasındaki iletişimi none-blocking hale getirmiştir. Fakat server ve servlet arasındaki iletişim hala blocking çalışır.

2.seviyedeki worker threadlere request aktarıldıktan sonra işlem “HttpInputStream” ve “HttpOutputStream” üzerinde read/write işlemi yapıldığı için doğal olarak blocking moda dönüşmektedir. Ya da database query’lerini bekleme gibi işlemler için.

Bu süreçteki aşamalara tekrardan baktığımızda;

  1. Client servere bir bağlantı açar
  2. Server 1. seviyedeki acceptor threads ile bağlantıyı alır
  3. Request’i process etmek için işlemi servlet container tarafından yönetilen requset processor thread’e aktarır, bu thread doğal olarak bloklanır.

Bu durumda request processor thread pool size 50 ise uygulama concurrent olarak yalnızca 50 request’i işleyip karşılayabilir. Bu durum görüldüğü gibi yine bir dar boğaz oluşturmaktadır.

Bu problemin çözümü Servlet 3.0 da gelen Async özelliği ile sağlanmıştır.

Asyn özelliği temel de şöyle çalışır; darboğaz, request processor threads’lerin blocking olması sebebiyle request processor thread pool concurrent olarak belirli bir sayıya kadar işlem yapabilirdi. Async özelliği ise processor threadlerinde işlerini arkaplanda çalışan servlet container dışında başka threadlere aktarması ile thread bloklanmadan işlemi aktarıp processor threadi serbest bırakır hale geldi.

Fakat bu çözüm de şöyle bir problemi ortaya çıkarmaktadır; sistemde eşzamanlı olarak çalışan thread sayısı düşmedi. Uygulama her ne kadar yüksek cevap verebilir yeteneğini kazanmış olsada sistemin kapasitesini ciddi oranda tüketmiş oldu.

Bunun dışında şu problem de threadin bloklanmasına sebep olmakta; request processor thread arka planda işlemi tamaladıktan sonra response’u cliente aktarmak için output streame yazar. Client bu yazılan datayı alarak thread’i serbest bırakmak zorundadır. Eğer client yavaş bir networkte çalışıyorsa veriyi alma süresi uzayacaktır. Bu da thread’in bloklanmasına sebep olur.

@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();
      }
     }

Bu noktaya kadar uygulamalarımızda concurrent request karşılama kapasitesini iki farklı özellik ile arttırabiliriz;

Tomcat NIO Connector’ün thread pool kapasitesini arttırarak; uygulamamız Async olmasa bile bu ciddi bir fayda sağlayacaktır.
Servlet 3.0 ile async özelliğini kullanarak
InputStream ve OutputStream’den kaynaklı bloklanma problemi Servlet 3.1ile gelen ReadListener ve WriteListener ile çözülmüştür. Listenerler ServletInput/OutputStream nesnelerine set edilir. Stream yazmaya ya da okumaya müsait olduğunda listenerler aracılığı ile yazma/okuma işlemi gerçekleşir. Bu sayede thread yazma/okuma işleminin müsaitlik durumuna göre idle durumunda beklemeden diğer işlere verilebilir. Ve böylece bu aşamadaki bloklanma problemi ortadan kalkar.

@WebServlet(urlPatterns={"/asyncioservlet"}, asyncSupported=true)
public class AsyncIOServlet extends HttpServlet {
   @Override
   public void doPost(HttpServletRequest request, 
                      HttpServletResponse response)
                      throws IOException {
      final AsyncContext acontext = request.startAsync();
      final ServletInputStream input = request.getInputStream();
      
      input.setReadListener(new ReadListener() {
         byte buffer[] = new byte[4*1024];
         StringBuilder sbuilder = new StringBuilder();
         @Override
         public void onDataAvailable() {
            try {
               do {
                  int length = input.read(buffer);
                  sbuilder.append(new String(buffer, 0, length));
               } while(input.isReady());
            } catch (IOException ex) { ... }
         }
         @Override
         public void onAllDataRead() {
            try {
               acontext.getResponse().getWriter()
                                     .write("...the response...");
            } catch (IOException ex) { ... }
            acontext.complete();
         }
         @Override
         public void onError(Throwable t) { ... }
      });
   }
}

Bu noktaya kadar Servlet’in gelişimini görmüş olduk. NIO desteği ile daha az thread ile daha fazla iş yapabileceğimiz uygulamalar geliştirebiliriz.

Bu problemlere daha farklı bakış açısı ile çözüm sunan Reactive yapılar ortaya çıkmıştır. Reactive yapılar Java tarafında servlet containerlerin dışında kendi request accepting ve processing mekanizmalarına sahiptir.