Servlet-3异步上下文,如何进行异步写操作?

时间:2012-08-23 05:27:46

标签: java comet servlet-3.0

问题描述

Servlet-3.0 API允许分离请求/响应上下文并在以后回复它。

但是,如果我尝试编写大量数据,例如:

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

对于Tomcat 7和Jetty 8,它实际上可能会阻塞 - 并且它确实阻止了琐碎的测试用例。教程建议创建一个线程池, 处理这样的设置 - 女巫通常对传统的10K架构起反作用。

但是如果我有10,000个打开的连接和一个让我们说10个线程的线程池, 对于低速连接或仅阻塞的客户,即使是1%也足够了 连接阻塞线程池并完全阻止彗星响应或 慢下来。

预期的做法是获得“可写”通知或I / O完成通知 而不是继续推送数据。

如何使用Servlet-3.0 API完成此操作,即如何获得:

  • I / O操作的异步完成通知。
  • 通过写入就绪通知获取非阻塞I / O.

如果Servlet-3.0 API不支持,那么是否有任何特定于Web Server的API(如Jetty Continuation或Tomcat CometEvent)允许真正异步处理此类事件而不使用线程池伪造异步I / O. p>

有人知道吗?

如果这不可能,您可以参考文档确认吗?

示例代码中的问题演示

我附上了模拟事件流的代码。

注意:

  • 它使用ServletOutputStream引发IOException来检测已断开连接的客户端
  • 它会发送keep-alive条消息以确保客户端仍在那里
  • 我创建了一个线程池来“模拟”异步操作。

在这样的例子中,我明确定义了大小为1的线程池来显示问题:

  • 启动申请
  • 从两个终端curl http://localhost:8080/path/to/app(两次)
  • 运行
  • 现在使用curd -d m=message http://localhost:8080/path/to/app
  • 发送数据
  • 两位客户都收到了数据
  • 现在暂停其中一个客户端(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);
        }
    }


}

上面的示例使用线程来阻止阻塞...但是如果阻塞客户端的数量大于它将阻塞的线程池的大小。

如何在没有阻止的情况下实施?

6 个答案:

答案 0 :(得分:29)

我发现Servlet 3.0 Asynchronous API难以正确实施且有用的文档很稀疏。经过大量的试验和错误并尝试了许多不同的方法后,我找到了一个我非常满意的强大解决方案。当我查看我的代码并将其与您的代码进行比较时,我注意到一个可能对您的特定问题有帮助的主要区别。我使用ServletResponse来编写数据,而不是ServletOutputStream

这里我的首选异步Servlet类略适用于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();
  }
}

答案 1 :(得分:10)

在我对这个主题的研究中,这个主题不断涌现,所以我在这里提到它:

Servlet 3.1在ServletInputStreamServletOutputStream上引入了异步操作。请参阅ServletOutputStream.setWriteListener

可以在http://docs.oracle.com/javaee/7/tutorial/servlets013.htm

找到一个示例

答案 2 :(得分:3)

答案 3 :(得分:3)

我们不能完全导致写入异步。我们实际上必须忍受这样的限制:当我们向客户写一些东西时,我们希望能够迅速地这样做,并且如果我们不这样做就能够将其视为错误。也就是说,如果我们的目标是尽可能快地将数据流式传输到客户端并使用通道的阻塞/非阻塞状态作为控制流量的方式,那么我们就不走运了。但是,如果我们以低速率发送客户应该能够处理的数据,我们至少能够迅速断开那些读取速度不够快的客户端。

例如,在您的应用程序中,我们以低速率(每隔几秒)发送Keepalive,并期望客户能够跟上他们发送的所有事件。我们将数据挥霍到客户端,如果无法跟上,我们可以及时,干净地断开连接。这比真正的异步I / O更有限,但它应该满足您的需求(顺便说一下,我的)。

诀窍是所有用于写出输出的方法只是抛出IOExceptions实际上比这更多:在实现中,所有对可能是interrupt()ed的东西的调用都将包含这样的东西(取自Jetty 9):

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

(我还注意到这个不会发生在Jetty 8中,其中记录了InterruptedException并立即重试阻塞循环。大概是为了确保你的servlet容器表现良好使用这个技巧。)

也就是说,当一个慢客户端导致写入线程被阻塞时,我们只需通过调用线程上的interrupt()强制将该写入作为IOException抛出。想一想:非阻塞代码在我们的一个处理线程上花费一个单位的时间来执行,所以使用刚刚中止的阻塞写入(比如说一毫秒)原则上是完全相同的。我们仍然只是在线程上花费了很少的时间,但效率稍差。

我修改了你的代码,以便主计时器线程在我们开始写入之前运行一个作业来限制每次写入的时间,如果写入快速完成,则取消作业,这应该是。

最后一点:在一个良好实现的servlet容器中,导致I / O抛出应该是安全的。如果我们能够捕获InterruptedIOException并稍后再次尝试写入会很好。如果他们无法跟上整个流,我们可能会给慢速客户端提供一部分事件。据我所知,在Jetty中,这并不完全安全。如果写入抛出,则HttpResponse对象的内部状态可能不够一致,以便稍后安全地重新进入写入。我希望尝试以这种方式推送servlet容器是不明智的,除非有特定的文档我错过了提供此保证。我认为这个想法是一个连接被设计为在发生IOException时关闭。

这是代码,使用一个简单的简单插图修改版本的RunJob :: run()(实际上,我们想在这里使用主计时器线程,而不是每次写入一个愚蠢的旋转)

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);

}

答案 4 :(得分:2)

春天是你的选择吗? 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/

答案 5 :(得分:-1)

我已快速查看了您的商家信息,因此我可能错过了一些观点。 池线程的优点是随着时间的推移在多个任务之间共享线程资源。也许你可以通过间隔不同http连接的keepAlive响应来解决你的问题,而不是同时对所有这些连接进行分组。