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:
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?
J'avais joint le code ci-dessous qui émule le flux d'événements.
Remarques:
ServletOutputStream
qui lance IOException
pour détecter les clients déconnectéskeep-alive
messages pour vous assurer que les clients sont toujours làDans un tel exemple, j'ai explicitement défini un pool de threads de taille 1 pour montrer le problème:
curl http://localhost:8080/path/to/app
(deux fois)curd -d m=message http://localhost:8080/path/to/app
curd -d m=message http://localhost:8080/path/to/app
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?
J'ai trouvé le Servlet 3.0
Asynchronous
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();
}
}
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
cela pourrait être utile
http://www.Oracle.com/webfolder/technetwork/tutorials/obe/Java/async-servlet/async-servlets.html
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);
}
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/