web-dev-qa-db-ja.com

サーブレット3非同期コンテキスト、非同期書き込みの方法

問題の説明

Servlet-3.0 APIを使用すると、リクエスト/レスポンスコンテキストをデタッチし、後で応答できます。

ただし、次のような大量のデータを書き込もうとすると、

AsyncContext ac = getWaitingContext() ;
ServletOutputStream out = ac.getResponse().getOutputStream();
out.print(some_big_data);
out.flush()

Tomcat 7とJetty 8の両方で、実際にブロックすることがあります-些細なテストケースではブロックします-チュートリアルでは、このようなセットアップを処理するスレッドプールを作成することをお勧めします。

ただし、10,000個のオープン接続と、たとえば10個のスレッドのスレッドプールがある場合、低速接続または単にブロックされたクライアントの1%でさえ、スレッドプールをブロックし、彗星の応答を完全にブロックするか、遅くするのに十分ですかなり。

予想されるプラクティスは、「書き込み準備完了」通知またはI/O完了通知を取得し、データのプッシュを続行することです。

Servlet-3.0 APIを使用してこれをどのように行うことができますか、つまり、どのように取得しますか?

  • I/O操作に関する非同期完了通知。
  • 書き込み準備完了通知でノンブロッキングI/Oを取得します。

これがServlet-3.0 APIでサポートされていない場合、スレッドプールを使用して非同期I/Oを偽装することなく、このようなイベントを本当に非同期に処理できるWebサーバー固有のAPI(Jetty ContinuationやTomcat CometEventなど)があります。

誰か知っていますか?

これが不可能な場合は、ドキュメントを参照して確認できますか?

サンプルコードでの問題のデモ

イベントストリームをエミュレートする以下のコードを添付しました。

ノート:

  • ServletOutputStreamをスローするIOExceptionを使用して、切断されたクライアントを検出します
  • keep-aliveメッセージを送信して、クライアントがまだそこにいることを確認します
  • 非同期操作を「エミュレート」するスレッドプールを作成しました。

このような例では、問題を示すためにサイズ1のスレッドプールを明示的に定義しました。

  • アプリケーションを開始する
  • 2つの端末から実行curl http://localhost:8080/path/to/app(2回)
  • curd -d m=message http://localhost:8080/path/to/appでデータを送信します
  • 両方のクライアントがデータを受信しました
  • クライアントの1つを停止し(Ctrl + Z)、メッセージをもう一度送信しますcurd -d m=message http://localhost:8080/path/to/app
  • 他のスレッドがブロックされているため、別の非中断クライアントが何も受信しなかったか、メッセージの転送後にキープアライブ要求の受信が停止したことを確認します。

スレッドプールを使用せずにこのような問題を解決したいのは、1000〜5000の接続を開くと、スレッドプールを非常に高速に使い果たすことができるからです。

以下のサンプルコード。


import Java.io.IOException;
import Java.util.HashSet;
import Java.util.Iterator;
import Java.util.concurrent.ThreadPoolExecutor;
import Java.util.concurrent.TimeUnit;
import Java.util.concurrent.LinkedBlockingQueue;

import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;


@WebServlet(urlPatterns = "", asyncSupported = true)
public class HugeStreamWithThreads extends HttpServlet {

    private long id = 0;
    private String message = "";
    private final ThreadPoolExecutor pool = 
        new ThreadPoolExecutor(1, 1, 50000L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        // it is explicitly small for demonstration purpose

    private final Thread timer = new Thread(new Runnable() {
        public void run()
        {
            try {
                while(true) {
                    Thread.sleep(1000);
                    sendKeepAlive();
                }
            }
            catch(InterruptedException e) {
                // exit
            }
        }
    });


    class RunJob implements Runnable {
        volatile long lastUpdate = System.nanoTime();
        long id = 0;
        AsyncContext ac;
        RunJob(AsyncContext ac) 
        {
            this.ac = ac;
        }
        public void keepAlive()
        {
            if(System.nanoTime() - lastUpdate > 1000000000L)
                pool.submit(this);
        }
        String formatMessage(String msg)
        {
            StringBuilder sb = new StringBuilder();
            sb.append("id");
            sb.append(id);
            for(int i=0;i<100000;i++) {
                sb.append("data:");
                sb.append(msg);
                sb.append("\n");
            }
            sb.append("\n");
            return sb.toString();
        }
        public void run()
        {
            String message = null;
            synchronized(HugeStreamWithThreads.this) {
                if(this.id != HugeStreamWithThreads.this.id) {
                    this.id = HugeStreamWithThreads.this.id;
                    message = HugeStreamWithThreads.this.message;
                }
            }
            if(message == null)
                message = ":keep-alive\n\n";
            else
                message = formatMessage(message);

            if(!sendMessage(message))
                return;

            boolean once_again = false;
            synchronized(HugeStreamWithThreads.this) {
                if(this.id != HugeStreamWithThreads.this.id)
                    once_again = true;
            }
            if(once_again)
                pool.submit(this);

        }
        boolean sendMessage(String message) 
        {
            try {
                ServletOutputStream out = ac.getResponse().getOutputStream();
                out.print(message);
                out.flush();
                lastUpdate = System.nanoTime();
                return true;
            }
            catch(IOException e) {
                ac.complete();
                removeContext(this);
                return false;
            }
        }
    };

    private HashSet<RunJob> asyncContexts = new HashSet<RunJob>();

    @Override
    public void init(ServletConfig config) throws ServletException
    {
        super.init(config);
        timer.start();
    }
    @Override
    public void destroy()
    {
        for(;;){
            try {
                timer.interrupt();
                timer.join();
                break;
            }
            catch(InterruptedException e) {
                continue;
            }
        }
        pool.shutdown();
        super.destroy();
    }


    protected synchronized void removeContext(RunJob ac)
    {
        asyncContexts.remove(ac);
    }

    // GET method is used to establish a stream connection
    @Override
    protected synchronized void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // Content-Type header
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");

        // Access-Control-Allow-Origin header
        response.setHeader("Access-Control-Allow-Origin", "*");

        final AsyncContext ac = request.startAsync();

        ac.setTimeout(0);
        RunJob job = new RunJob(ac);
        asyncContexts.add(job);
        if(id!=0) {
            pool.submit(job);
        }
    }

    private synchronized void sendKeepAlive()
    {
        for(RunJob job : asyncContexts) {
            job.keepAlive();
        }
    }

    // POST method is used to communicate with the server
    @Override
    protected synchronized void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException 
    {
        request.setCharacterEncoding("utf-8");
        id++;
        message = request.getParameter("m");        
        for(RunJob job : asyncContexts) {
            pool.submit(job);
        }
    }


}

上記のサンプルでは、​​ブロッキングを防ぐためにスレッドを使用しています...ただし、ブロッキングクライアントの数がブロックするスレッドプールのサイズよりも大きい場合は。

ブロックせずにどのように実装できますか?

50
Artyom

Servlet 3.0Asynchronous AP​​Iは、適切で有用なドキュメントをスパースにするために正しく実装するのが難しいことがわかりました。多くの試行錯誤と多くの異なるアプローチを試した後、私は非常に満足している堅牢なソリューションを見つけることができました。私のコードを見て、それをあなたのコードと比較すると、特定の問題に役立つ大きな違いに気付きます。 ServletResponseではなく、ServletOutputStreamを使用してデータを書き込みます。

ここで、あなたのsome_big_dataの場合に少し適応した私の非同期サーブレットクラス:

import Java.io.IOException;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.Apache.log4j.Logger;

@javax.servlet.annotation.WebServlet(urlPatterns = { "/async" }, asyncSupported = true, initParams = { @WebInitParam(name = "threadpoolsize", value = "100") })
public class AsyncServlet extends HttpServlet {

  private static final Logger logger = Logger.getLogger(AsyncServlet.class);

  public static final int CALLBACK_TIMEOUT = 10000; // ms

  /** executor service */
  private ExecutorService exec;

  @Override
  public void init(ServletConfig config) throws ServletException {

    super.init(config);
    int size = Integer.parseInt(getInitParameter("threadpoolsize"));
    exec = Executors.newFixedThreadPool(size);
  }

  @Override
  public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

    final AsyncContext ctx = req.startAsync();
    final HttpSession session = req.getSession();

    // set the timeout
    ctx.setTimeout(CALLBACK_TIMEOUT);

    // attach listener to respond to lifecycle events of this AsyncContext
    ctx.addListener(new AsyncListener() {

      @Override
      public void onComplete(AsyncEvent event) throws IOException {

        logger.info("onComplete called");
      }

      @Override
      public void onTimeout(AsyncEvent event) throws IOException {

        logger.info("onTimeout called");
      }

      @Override
      public void onError(AsyncEvent event) throws IOException {

        logger.info("onError called: " + event.toString());
      }

      @Override
      public void onStartAsync(AsyncEvent event) throws IOException {

        logger.info("onStartAsync called");
      }
    });

    enqueLongRunningTask(ctx, session);
  }

  /**
   * if something goes wrong in the task, it simply causes timeout condition that causes the async context listener to be invoked (after the fact)
   * <p/>
   * if the {@link AsyncContext#getResponse()} is null, that means this context has already timed out (and context listener has been invoked).
   */
  private void enqueLongRunningTask(final AsyncContext ctx, final HttpSession session) {

    exec.execute(new Runnable() {

      @Override
      public void run() {

        String some_big_data = getSomeBigData();

        try {

          ServletResponse response = ctx.getResponse();
          if (response != null) {
            response.getWriter().write(some_big_data);
            ctx.complete();
          } else {
            throw new IllegalStateException(); // this is caught below
          }
        } catch (IllegalStateException ex) {
          logger.error("Request object from context is null! (nothing to worry about.)"); // just means the context was already timeout, timeout listener already called.
        } catch (Exception e) {
          logger.error("ERROR IN AsyncServlet", e);
        }
      }
    });
  }

  /** destroy the executor */
  @Override
  public void destroy() {

    exec.shutdown();
  }
}
29
herrtim

このトピックに関する私の研究中、このスレッドはポップアップし続けていたので、ここで言及したと思いました。

サーブレット3.1では、ServletInputStreamおよびServletOutputStreamで非同期操作が導入されました。見る - ServletOutputStream.setWriteListener

例は http://docs.Oracle.com/javaee/7/tutorial/servlets013.htm にあります。

10
Erich Eichinger
3
ATilara

書き込みを非同期にすることはできません。現実的には、クライアントに何かを書き出すときに、すぐに書き出すことができ、書き出さない場合はエラーとして扱うことができるという制限に耐えなければなりません。つまり、可能な限り高速にクライアントにデータをストリーミングし、フローを制御する方法としてチャネルのブロッキング/非ブロッキングステータスを使用することが目標である場合、運が悪いのです。ただし、クライアントが処理できるはずの低いレートでデータを送信している場合、少なくとも十分に速く読み取れないクライアントを即座に切断することができます。

たとえば、アプリケーションでは、キープアライブを低速のレート(数秒ごと)で送信し、クライアントが送信されているすべてのイベントに追いつくことができることを期待しています。クライアントにデータを分散し、データが維持できない場合は、迅速かつクリーンにデータを切断できます。それは真の非同期I/Oよりも少し制限されていますが、それはあなたのニーズ(そして偶然にも私のもの)を満たすでしょう。

秘Theは、IOExceptionをスローするだけの出力を書き込むためのすべてのメソッドは、実際にはそれよりも少し多くのことを行うということです。実装では、interrupt()edできるものへのすべての呼び出しは、このようなものでラップされます(桟橋9):

catch (InterruptedException x)
    throw (IOException)new InterruptedIOException().initCause(x);

(また、Jetty 8でこのdoes n'tが発生し、InterruptedExceptionがログに記録され、ブロックループがすぐに再試行されることにも注意してください。トリック。)

つまり、遅いクライアントが書き込みスレッドをブロックする場合、スレッドでinterrupt()を呼び出すことにより、書き込みを強制的にIOExceptionとしてスローします。考えてみてください:ノンブロッキングコードは処理スレッドの1つで単位時間を消費して実行されるため、(たとえば1ミリ秒後に)中止されたブロッキング書き込みを使用することは原則としてまったく同じです。私たちはまだスレッド上で少し時間をかじっているだけで、効率はわずかに低下しています。

書き込みを開始する直前にメインタイマースレッドが各書き込みの時間を制限するジョブを実行し、書き込みがすぐに完了するとジョブがキャンセルされるようにコードを変更しました。

最後の注意:十分に実装されたサーブレットコンテナで、I/Oがoughtをスローするようにスローします。 InterruptedIOExceptionをキャッチして、後でもう一度書き込みを試みることができれば素晴らしいでしょう。おそらく、完全なストリームに追いつかない場合は、遅いクライアントにイベントのサブセットを提供したいでしょう。私が知る限り、Jettyではこれは完全に安全ではありません。書き込みがスローされた場合、HttpResponseオブジェクトの内部状態は、書き込みを後で安全に再入力するのに十分な一貫性がない場合があります。この保証を提供し損ねた特定のドキュメントがない限り、この方法でサーブレットコンテナをプッシュしようとするのは賢明ではないでしょう。 IOExceptionが発生した場合に接続がシャットダウンされるように設計されているという考え方だと思います。

コードは次のとおりです。RunJob:: run()の修正版では、単純な図解を使用しています(実際には、書き込みごとに1つの愚かな書き込みをスピンアップするのではなく、ここでメインタイマースレッドを使用したいと思います)。

public void run()
{
    String message = null;
    synchronized(HugeStreamWithThreads.this) {
        if(this.id != HugeStreamWithThreads.this.id) {
            this.id = HugeStreamWithThreads.this.id;
            message = HugeStreamWithThreads.this.message;
        }
    }
    if(message == null)
        message = ":keep-alive\n\n";
    else
        message = formatMessage(message);

    final Thread curr = Thread.currentThread();
    Thread canceller = new Thread(new Runnable() {
        public void run()
        {
            try {
                Thread.sleep(2000);
                curr.interrupt();
            }
            catch(InterruptedException e) {
                // exit
            }
        }
    });
    canceller.start();

    try {
        if(!sendMessage(message))
            return;
    } finally {
        canceller.interrupt();
        while (true) {
            try { canceller.join(); break; }
            catch (InterruptedException e) { }
        }
    }

    boolean once_again = false;
    synchronized(HugeStreamWithThreads.this) {
        if(this.id != HugeStreamWithThreads.this.id)
            once_again = true;
    }
    if(once_again)
        pool.submit(this);

}
3
Nicholas Wilson

Springはあなたにとって選択肢ですか? Spring-MVC 3.2にはDeferredResultというクラスがあり、「10,000オープン接続/ 10サーバープールスレッド」シナリオを適切に処理します。

例: http://blog.springsource.org/2012/05/06/spring-mvc-3-2-preview-introducing-servlet-3-async-support/

2
JJ Zabkar