Perché un server non può continuare ad agire su una richiesta dopo aver inviato la risposta?

3

Ad esempio, ho un servizio web RESTful e devo supportare la creazione di un widget tramite una richiesta HTTP POSTed. Ho bisogno di

  1. Deserializza il widget POSTed.
  2. Convalida il widget deserializzato.
  3. Persiste il widget su un servizio di supporto.
  4. Invia una risposta positiva al client.

Ora, il problema è che il mio SLA non consente al 3 di bloccare 4 - la persistenza in questo caso richiede troppo tempo e il client deve sapere subito che la richiesta è riuscita (questo è il caso d'uso standard per la 202 codice di stato http).

Quasi ovunque io guardi, il presupposto è che il modo per risolverlo sia quello di "fare da sfondo" alla costosa parte di persistenza. Questo è in genere un processo scomodo con molte parti mobili e spesso la propria latenza (ad esempio, effettuare una chiamata di blocco a un servizio di accodamento separato).

Il semplice parallelismo tra 3 e 4 usando i costrutti nativi è generalmente fuori discussione, così come lo è l'ordine in modo tale che 4 blocchi 3. Per quanto posso dire, questo è principalmente dovuto al fatto che i server web e app sono costruiti con il fondamentale Supponendo che un processo (e tutti i bambini a cui è stato biforcato) sia libero di essere ucciso / riutilizzato non appena viene inviata la sua risposta. O, equivalentemente, che la risposta non può essere inviata fino a quando l'app non ha finito di fare tutto ciò che sta per fare.

Questo è estremamente frustrante per me! In qualsiasi altro contesto, posso fare ciò che mi piace con il flusso di controllo del programma. Ma quando gestisco un Phusion Passenger - > Installazione di Ruby on Rails, e voglio fare una cosa dopo aver inviato la risposta, mi rimane una grande varietà di opzioni barocche che sembrano considerare tutto perfettamente naturale e accettabile, per esempio, serializzare lo stato dell'applicazione, postarlo ad Amazon SQS, hanno fondamentalmente separato il servizio web che esegue il polling di SQS, deserializza il vecchio stato dell'applicazione, quindi fa la cosa. Avevo lo stato dell'applicazione impostato come volevo dopo aver inviato la risposta! Perché non c'è niente scritto quindi posso solo fare

def create
  widget = Widget.new(params[:widget])
  if widget.valid?
    respond_with(widget)
    widget.save
  else
    respond_with(widget.errors)
  end
end

Perché esiste un'ipotesi pervasiva che gli stack di servizi Web non supporteranno mai questo tipo di flusso? C'è un inconveniente nascosto, o un compromesso, per rendere possibile farlo?

    
posta histocrat 13.05.2014 - 19:53
fonte

5 risposte

3

Now, the problem is that my SLA does not allow 3 to block 4--persistence in this case takes too long and the client needs to know right away that the request has succeeded (this is the standard use case for the 202 http status code).

Ma finché il cambiamento di stato non viene mantenuto, non puoi sapere che hai successo. Potresti avere un improvviso problema con l'energia elettrica e un terremoto errato (questo succede!) E poi quando il servizio riprende, è completamente dimenticato della risorsa. Il client ritorna e il server non ha idea di cosa stia parlando. Non va bene.

No, devi accelerare i commit (ed evitare l'elaborazione non necessaria nel percorso critico).

Potresti dover pensare a come puoi impegnarti in modo logico più veloce, su cosa significhi commettere; puoi semplicemente accettare il fatto che c'è del lavoro da fare invece di dover richiedere il risultato del lavoro, cosa che può essere fatta molto più rapidamente. Quindi, quando l'utente torna indietro, l'elaborazione viene eseguita nel qual caso è possibile 301 per i risultati oppure è possibile fornire un risultato che indichi perché le cose continuano a essere elaborate o che hanno fallito.

L'acquisizione di commit più rapidi potrebbe significare riflettere più attentamente su come distribuire. Stai usando la giusta scelta del database? Quel database è ospitato sull'hardware giusto? (I carichi pesanti per commit sono molto più veloci quando hai un SSD per ospitare il log delle transazioni.) So che è bello ignorare queste cose e gestire il modello di dati a un livello astratto, ma la performance è una di quelle cose in cui i dettagli sottostanti hanno l'abitudine di filtrare.

In risposta ai commenti che chiariscono ...
Se hai un compito veramente costoso da eseguire (ad esempio, hai chiesto una raccolta di file di grandi dimensioni da trasferire da terze parti) devi smettere di pensare in termini di completare l'intera attività prima che la risposta ritorni per l'utente. Invece, rende l'attività stessa una risorsa : puoi quindi rispondere rapidamente all'utente per dire che l'attività è iniziata (anche se è una piccola bugia, potresti aver messo in coda il fatto che tu vuoi che l'attività abbia inizio) e l'utente può quindi interrogare la risorsa per scoprire se è stata completata. Questo è il modello di elaborazione asincrono (in opposizione al più comune modello di elaborazione sincrono).

Ci sono molti modi per gestire lasciando che l'utente sappia che l'attività è finita. Di gran lunga il più semplice è semplicemente aspettare fino a quando non sondano e dire loro allora. In alternativa, puoi inviare una notifica da qualche parte (magari un feed Atom o inviando un'e-mail?) Ma questi sono molto più complicati in generale; le notifiche push sono purtroppo relativamente facili da abusare. Consiglio davvero di attenersi al polling (o usare l'intelligenza con i websocket).

Il caso in cui un'attività potrebbe essere veloce o potrebbe essere lento e l'utente non ha modo di sapere in anticipo è davvero malvagio. Sfortunatamente, il modo corretto per risolverlo è rendere il modello di elaborazione asincrono; è possibile eseguire in entrambi i casi il ribaltamento di REST / HTTP (inviando un 200 o un 202, il contenuto del 202 includerebbe un collegamento alla risorsa del task) ma è piuttosto complicato. Non so se il tuo framework supporta queste cose bene.

Tieni presente che la maggior parte degli utenti non comprende l'elaborazione asincrona . Non capiscono di dover sondare i risultati. Non apprezzano che un server possa fare cose diverse dalla gestione di ciò che gli hanno chiesto di fare. Ciò significa che se stai offrendo una rappresentazione HTML, probabilmente dovresti includere alcuni Javascript per eseguire il polling (o la connessione a una websocket, o qualsiasi altra cosa tu scelga) in modo che non possano aver bisogno di sapere sull'aggiornamento della pagina o dettagli come quello. In questo modo puoi fare molto per fingere di fare solo ciò che gli hanno chiesto di fare, ma senza tutti i problemi associati alle attuali richieste di lunga durata.

    
risposta data 13.05.2014 - 20:37
fonte
1

Penso che tu abbia bisogno di qualcosa di simile a JMS (ma in Ruby o qualunque cosa tu stia usando, la domanda non era completamente chiaro).

L'idea qui è che il tuo servizio web front-end può trasferire rapidamente un messaggio a un altro servizio che dice "persist i dati". Quel servizio salverà il messaggio in una coda persistente e ritornerà immediatamente. Il servizio web può tornare immediatamente senza dover masticare più risorse del server.

Quindi, in background, un processore della coda preleva i messaggi nella coda e li invia . In questo caso, invierà a un servizio di persistenza che prenderà il messaggio non elaborato e prenderà le misure necessarie per salvarlo a destinazione.

La differenza tra il salvataggio in una coda e il salvataggio nel database dell'applicazione è questa. La coda potrebbe usare JSON o XML o un altro formato di interscambio e non gliene importa molto del carico utile. Si preoccupa di quali servizi o servizi hanno bisogno del messaggio e quando è stato messo in coda. Il tuo servizio di persistenza riceve il messaggio e apre il carico utile per eseguire qualsiasi elaborazione specifica del dominio necessaria.

    
risposta data 14.05.2014 - 18:08
fonte
1

Is there a hidden drawback, or tradeoff, to making it possible to do this?

Sì. Il vantaggio di limitare tutto a un ciclo di richiesta-risposta è che il framework e il server Web possono automaticamente liberare tutte le risorse contenute nella richiesta, ad es. memoria, connessioni di rete, transazioni di database e connessioni a database, processi, handle di file, processi di lavoro, ecc. Scrivere daemon / server di lunga durata di alta qualità è molto difficile, perché qualsiasi perdita di risorse in processi di lunga durata si amplificherà nel tempo e potrebbe portare un server che sta funzionando bene per settimane fino a un arresto senza una ragione apparente. Legare le risorse al ciclo di richiesta-risposta consentirebbe la pulizia automatica non solo quando il codice è nel percorso felice, ma anche quando ottiene le eccezioni rare e impreviste. Ciò riduce il carico per lo sviluppatore dell'applicazione, mentre le pulizie possono essere lasciate al framework, dove possono essere eseguite in modo sistematico e coerente.

Inoltre, se le richieste continuano ad essere elaborate dopo che la risposta è terminata, si apre la possibilità che la richiesta non riesca dopo che la risposta è terminata (ad esempio, il server di persistenza è scomparso o ha un disco completo o il server web esaurisce memoria e viene annullato in modo non vergognoso dal killer OOM del kernel) e ora poiché non rientra nel ciclo di richiesta-risposta, il messaggio di errore non può più essere consegnato all'utente. Puoi sicuramente escogitare la tua strada da queste situazioni, ad esempio creando una risorsa di stato per l'agente utente per verificare lo stato delle richieste accettate ma non completate, che richiede un sacco di pensieri e ingegneria, ma è esattamente come si intende utilizzare 202 . Si noti che 202 non specifica come deve essere controllato lo stato. È possibile che una risposta 202 chieda al client di eseguire il polling della risorsa di stato, o di aprire una connessione Websocket alla risorsa, ecc. Sei libero di fare ciò che è meglio per la tua situazione.

Molte risorse che potrebbero trapelare in demoni longevi a volte semplicemente non sono ovvi, e scrivere la tua applicazione in linguaggi di alto livello con i garbage collector automatici non risolve tutto.

Tornando alla tua domanda:

Is there a hidden drawback, or tradeoff, to making it possible to do this?

Quando interrompi il ciclo di richiesta-risposta, sei ora da solo. Mentre è certamente possibile scrivere un server che non segue il ciclo richiesta-risposta, questo è estremamente difficile da fare e richiede un sacco di ingegneria. C'è un numero limitato di pattern architettonici che può risolvere in modo soddisfacente i problemi dei daemon longevi, la request-response è una delle più semplici e facili da usare e simple == good .

Ci sono altre architetture che ti permettono di lavorare dopo che è terminato il ciclo di richiesta-risposta che funziona bene con il paradigma richiesta-risposta. Questo di solito ruota attorno al passaggio dei messaggi. Con il passaggio dei messaggi, dopo la persistenza dei dati e immediatamente prima di terminare la risposta, si invia un comando ai processi di lavoro per elaborare i dati appena inviati, in genere tramite un servizio di ricezione messaggi. Ciò offre il vantaggio di una separazione netta tra i processi del server Web che elabora brevi richieste interattive che devono rispondere al messaggio il più rapidamente possibile, dagli operatori in background che impiegano molto tempo per terminare e ritardi molto meno sensibili (interessante curiosità : molti algoritmi di pianificazione dei processi all'interno dei kernel hanno euristiche per rilevare se un processo si presenta come un processo interattivo o un processo orientato ai batch e li pianifica in modo diverso, il primo assume solitamente la priorità per le interruzioni a bassa latenza, il secondo riduce le fasce temporali).

TL; DR. Il vincolo del ciclo richiesta-risposta è una buona cosa, prova a lavorare con esso piuttosto che contro.

the response can't be sent until the app has finished doing everything it's going to do.

Questo non è corretto. La maggior parte dei framework web decenti dovrebbe supportare anche la codifica di trasferimento chunked HTTP che può restituire dati parziali al client mentre il resto dei dati viene ancora calcolato.

    
risposta data 29.09.2014 - 18:02
fonte
0

nulla impedisce a un servizio Web di continuare a elaborare i dati dopo l'invio di una risposta. Il tuo problema è probabilmente dovuto alla scelta del framework che ti costringe a un'interazione semplicistica richiesta-risposta-fatta.

Il tuo servizio potrebbe avviare un thread e inviarlo ai dati su cui lavorare, o passarlo (in modo asincrono) a un altro servizio su cui lavorare.

Potresti implementare un websocket e inviare più risposte al client man mano che i dati diventano disponibili.

I problemi che potresti avere sono in genere ridotti alla scalabilità. Che cosa succede se il tuo lavoratore in background impiega molto tempo per essere elaborato, o ogni richiesta gira un thread - potresti finire con un sacco di lavoro in background, quindi ci sono molte meno risorse disponibili per le richieste dei client.

    
risposta data 13.05.2014 - 20:37
fonte
-1

Penso che il problema qui derivi dal fatto che ora il client assume che il widget esiste e sarà disponibile anche se è del tutto possibile che il salvataggio abbia fallito per un numero qualsiasi di motivi. Ciò lascia la parte anteriore e il backend in uno stato incoerente, anche REST è il trasferimento dello stato Representational, se in realtà non stai trasferendo una rappresentazione accurata dello stato, non è realmente REST

    
risposta data 13.05.2014 - 20:02
fonte

Leggi altre domande sui tag