web-dev-qa-db-fra.com

Servlet-3 Async Context, comment faire des écritures asynchrones?

Description du problème

L'API Servlet-3.0 permet de détacher un contexte de demande/réponse et d'y répondre plus tard.

Cependant, si j'essaie d'écrire une grande quantité de données, quelque chose comme:

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

Il peut en fait bloquer - et il se bloque dans des cas de test triviaux - pour Tomcat 7 et Jetty 8. Les didacticiels recommandent de créer un pool de threads qui gérerait une telle configuration - ce qui est généralement le contre-positif d'une architecture 10K traditionnelle.

Cependant, si j'ai 10 000 connexions ouvertes et un pool de threads, disons 10 threads, il suffit que même 1% des clients qui ont des connexions à faible vitesse ou une connexion simplement bloquée bloquent le pool de threads et bloquent complètement la réponse de la comète ou la ralentissent. significativement.

La pratique attendue consiste à obtenir une notification "prêt à l'écriture" ou une notification d'achèvement d'E/S et à continuer à pousser les données.

Comment cela peut-il être fait en utilisant l'API Servlet-3.0, c'est-à-dire comment obtenir soit:

  • Notification d'achèvement asynchrone sur l'opération d'E/S.
  • Obtenez des E/S non bloquantes avec une notification d'écriture.

Si cela n'est pas pris en charge par l'API Servlet-3.0, existe-t-il des API spécifiques au serveur Web (comme Jetty Continuation ou Tomcat CometEvent) qui permettent de gérer ces événements de manière vraiment asynchrone sans simuler les E/S asynchrones à l'aide du pool de threads.

Est-ce que quelqu'un sait?

Et si cela n'est pas possible, pouvez-vous le confirmer avec une référence à la documentation?

Démonstration de problème dans un exemple de code

J'avais joint le code ci-dessous qui émule le flux d'événements.

Remarques:

  • il utilise ServletOutputStream qui lance IOException pour détecter les clients déconnectés
  • il envoie keep-alive messages pour vous assurer que les clients sont toujours là
  • J'ai créé un pool de threads pour "émuler" les opérations asynchrones.

Dans un tel exemple, j'ai explicitement défini un pool de threads de taille 1 pour montrer le problème:

  • Lancer une application
  • Exécuter à partir de deux terminaux curl http://localhost:8080/path/to/app (deux fois)
  • Envoyez maintenant les données avec curd -d m=message http://localhost:8080/path/to/app
  • Les deux clients ont reçu les données
  • Maintenant, suspendez l'un des clients (Ctrl + Z) et renvoyez le message curd -d m=message http://localhost:8080/path/to/app
  • Observez qu'un autre client non suspendu n'a rien reçu ou que le message a été transféré a cessé de recevoir des demandes de maintien en vie car un autre thread est bloqué.

Je veux résoudre un tel problème sans utiliser le pool de threads, car avec 1000-5000 connexions ouvertes, je peux épuiser le pool de threads très rapidement.

L'exemple de code ci-dessous.


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


}

L'exemple ci-dessus utilise des threads pour empêcher le blocage ... Cependant, si le nombre de clients bloquants est supérieur à la taille du pool de threads, il bloquerait.

Comment pourrait-il être mis en œuvre sans blocage?

50
Artyom

J'ai trouvé le Servlet 3.0Asynchronous API délicate à implémenter correctement et documentation utile à éparpiller. Après beaucoup d'essais et d'erreurs et après avoir essayé de nombreuses approches différentes, j'ai pu trouver une solution robuste dont je suis très satisfait. Quand je regarde mon code et le compare au vôtre, je remarque une différence majeure qui peut vous aider avec votre problème particulier. J'utilise un ServletResponse pour écrire les données et non un ServletOutputStream.

Voici ma classe de servlet asynchrone idéale pour votre some_big_data Cas:

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

Au cours de mes recherches sur ce sujet, ce fil a continué à apparaître, alors j'ai pensé le mentionner ici:

Le servlet 3.1 a introduit des opérations asynchrones sur ServletInputStream et ServletOutputStream. Voir ServletOutputStream.setWriteListener .

Un exemple peut être trouvé sur http://docs.Oracle.com/javaee/7/tutorial/servlets013.htm

10
Erich Eichinger
3
ATilara

Nous ne pouvons pas vraiment faire en sorte que les écritures soient asynchrones. Nous devons en réalité vivre avec la limitation que lorsque nous écrivons quelque chose à un client, nous nous attendons à pouvoir le faire rapidement et à le traiter comme une erreur si nous ne le faisons pas. Autrement dit, si notre objectif est de diffuser des données au client le plus rapidement possible et d'utiliser le statut de blocage/non-blocage du canal comme moyen de contrôler le flux, nous n'avons pas de chance. Mais, si nous envoyons des données à un faible taux qu'un client devrait être capable de gérer, nous pouvons au moins déconnecter rapidement les clients qui ne lisent pas assez rapidement.

Par exemple, dans votre application, nous envoyons les Keepalives à un rythme lent (toutes les quelques secondes) et attendons des clients qu'ils soient en mesure de suivre tous les événements qui leur sont envoyés. Nous divulguons les données au client, et si elles ne peuvent pas suivre, nous pouvons les déconnecter rapidement et proprement. C'est un peu plus limité que les vraies E/S asynchrones, mais cela devrait répondre à vos besoins (et accessoirement au mien).

L'astuce est que toutes les méthodes d'écriture de sortie qui lèvent juste des IOExceptions font en fait un peu plus que cela: dans l'implémentation, tous les appels à des choses qui peuvent être interrompues () ed seront enveloppés avec quelque chose comme ça (tiré de Jetée 9):

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

(Je note également que cela ne se produit pas se produit dans Jetty 8, où une InterruptedException est enregistrée et la boucle de blocage est immédiatement réessayée. Vraisemblablement, vous vous assurez que votre conteneur de servlet se comporte bien pour utiliser ceci tour.)

Autrement dit, lorsqu'un client lent provoque le blocage d'un thread d'écriture, nous forçons simplement l'écriture à être levée en tant qu'exception IOException en appelant interrupt () sur le thread. Pensez-y: le code non bloquant consommerait une unité de temps sur l'un de nos threads de traitement pour s'exécuter de toute façon, donc utiliser des écritures de blocage qui sont juste abandonnées (après disons une milliseconde) est vraiment identique en principe. Nous mordons encore juste un court laps de temps sur le fil, seulement légèrement moins efficacement.

J'ai modifié votre code afin que le thread principal du minuteur exécute un travail pour limiter le temps dans chaque écriture juste avant de commencer l'écriture, et le travail est annulé si l'écriture se termine rapidement, ce qu'il devrait faire.

Une dernière note: dans un conteneur de servlet bien implémenté, provoquant la sortie d'E/S devrait pour être sûr. Ce serait bien si nous pouvions intercepter l'interruptionIOException et réessayer l'écriture plus tard. Peut-être que nous aimerions donner aux clients lents un sous-ensemble des événements s'ils ne peuvent pas suivre le flux complet. Pour autant que je sache, dans Jetty, ce n'est pas entièrement sûr. Si une écriture est lancée, l'état interne de l'objet HttpResponse peut ne pas être suffisamment cohérent pour gérer la réintroduction de l'écriture en toute sécurité ultérieurement. Je m'attends à ce qu'il ne soit pas sage d'essayer de pousser un conteneur de servlet de cette manière, sauf s'il existe des documents spécifiques qui m'ont manqué d'offrir cette garantie. Je pense que l'idée est qu'une connexion est conçue pour être fermée si une exception IOException se produit.

Voici le code, avec une version modifiée de RunJob :: run () utilisant une illustration simple et grincheuse (en réalité, nous voudrions utiliser le thread principal du minuteur ici plutôt que d'en tourner un par écriture, ce qui est idiot).

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

Le printemps est-il une option pour vous? Spring-MVC 3.2 a une classe appelée DeferredResult, qui gérera gracieusement votre scénario "10 000 connexions ouvertes/10 threads de pool de serveurs".

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

2
JJ Zabkar