2011-01-16 14 views
21

Questa domanda mi infastidisce da tempo (spero di non essere l'unico). Voglio prendere una tipica applicazione Java EE a 3 livelli e vedere come può essere implementata con gli attori. Mi piacerebbe scoprire se ha davvero senso fare una tale transizione e come posso trarne profitto se ha senso (magari prestazioni, migliore architettura, estensibilità, manutenibilità, ecc ...).Trasferimento di un'architettura tipica a 3 livelli agli attori

Ecco tipico Controller (presentazione), servizi (logica di business), DAO (dati):

trait UserDao { 
    def getUsers(): List[User] 
    def getUser(id: Int): User 
    def addUser(user: User) 
} 

trait UserService { 
    def getUsers(): List[User] 
    def getUser(id: Int): User 
    def addUser(user: User): Unit 

    @Transactional 
    def makeSomethingWithUsers(): Unit 
} 


@Controller 
class UserController { 
    @Get 
    def getUsers(): NodeSeq = ... 

    @Get 
    def getUser(id: Int): NodeSeq = ... 

    @Post 
    def addUser(user: User): Unit = { ... } 
} 

si può trovare qualcosa di simile in molte applicazioni di primavera. Possiamo prendere un'implementazione semplice che non ha nessuno stato condiviso e questo perché non ha blocchi sincronizzati ... quindi tutto lo stato è nel database e l'applicazione si basa sulle transazioni. Servizio, controller e dao hanno solo un'istanza. Quindi per ogni richiesta il server applicativo utilizzerà thread separati, ma i thread non si bloccheranno a vicenda (ma saranno bloccati da DB IO).

Supponiamo di provare a implementare funzionalità simili con gli attori. Può apparire così:

sealed trait UserActions 
case class GetUsers extends UserActions 
case class GetUser(id: Int) extends UserActions 
case class AddUser(user: User) extends UserActions 
case class MakeSomethingWithUsers extends UserActions 

val dao = actor { 
    case GetUsers() => ... 
    case GetUser(userId) => ... 
    case AddUser(user) => ... 
} 

val service = actor { 
    case GetUsers() => ... 
    case GetUser(userId) => ... 
    case AddUser(user) => ... 
    case MakeSomethingWithUsers() => ... 
} 

val controller = actor { 
    case Get("/users") => ... 
    case Get("/user", userId) => ... 
    case Post("/add-user", user) => ... 
} 

penso che non è molto importante qui come get() e post() estrattori sono implementati. Supponiamo che io scriva un quadro per implementarlo. Posso inviare un messaggio al controller in questo modo:

controller !! Get("/users") 

La stessa cosa dovrebbe essere fatta dal controller e dal servizio. In questo caso l'intero flusso di lavoro sarebbe sincrono. Ancor peggio: posso elaborare una sola richiesta alla volta (nel frattempo tutte le altre richieste arrivano nella casella di posta del controllore). Quindi ho bisogno di renderlo tutto asincrono.

C'è un modo elegante per eseguire ogni passo di elaborazione in modo asincrono in questa configurazione?

Per quanto ne so, ogni livello dovrebbe in qualche modo salvare il contesto del messaggio che riceve e quindi inviare un messaggio al livello sottostante. Quando il livello sotto risponde con un messaggio di risultato, dovrei essere in grado di ripristinare il contesto iniziale e rispondere con questo risultato al mittente originale. È corretto?

Inoltre, al momento ho solo un'istanza di attore per ogni livello. Anche se funzionassero in modo asincrono, posso ancora elaborare in parallelo un solo controller, servizio e messaggio dao. Ciò significa che ho bisogno di più attori dello stesso tipo. Il che mi porta a LoadBalancer per ogni livello. Questo significa anche che se avessi UserService e ItemService dovrei caricarli entrambi separatamente.

Ho sentito, che ho capito qualcosa di sbagliato. Tutta la configurazione necessaria sembra essere complicata. Cosa ne pensi di questo?

(PS: Sarebbe inoltre molto interessante sapere come le transazioni DB rientrano in questa immagine, ma penso che sia eccessivo per questa discussione)

+0

+1 - Roba ambiziosa da parte tua, Easy Angel. – duffymo

risposta

4

grandi transazioni atomiche di calcolo intensivo sono difficili da tirare fuori, che è una ragione per cui i database sono così popolari. Quindi, se ti stai chiedendo se puoi usare in modo trasparente e facile gli attori per sostituire tutte le funzionalità transazionali e altamente scalabili di un database (il cui potere ti sta fortemente appoggiando nel modello Java EE), la risposta è no.

Ma ci sono alcuni trucchi che puoi giocare.Ad esempio, se un attore sembra causare un collo di bottiglia, ma non vuoi dedicarti alla creazione di una struttura di dispatcher/worker farm, potresti essere in grado di trasformare il lavoro intenso in future:

val service = actor { 
    ... 
    case m: MakeSomethingWithUsers() => 
    Futures.future { sender ! myExpensiveOperation(m) } 
} 

In questo modo, i compiti molto costosi vengono generati in nuovi thread (supponendo che non sia necessario preoccuparsi di atomicità e deadlock e così via, il che si può - ma, ancora una volta, risolvere questi problemi non è facile in generale) e i messaggi vengono spediti ovunque, indipendentemente da dove dovrebbero andare.

+0

A meno che, naturalmente, non si avvii spawn, tali thread possono essere memorizzati su un singolo server. Quindi la tua soluzione si ridurrebbe in modo insufficiente. – wheaties

+1

@wheaties: Infatti. Le prestazioni del tuo database sarebbero molto insignificanti anche su quella macchina. –

5

Proprio riffing, ma ...

Penso che se si desidera utilizzare gli attori, si dovrebbe buttare via tutti i modelli precedenti e sognare qualcosa di nuovo, allora forse ri-incorporare i vecchi schemi (controllore, dao, ecc.) come necessario per colmare le lacune.

Ad esempio, cosa succede se ciascun utente è un singolo attore seduto nella JVM, o tramite attori remoti, in molte altre JVM. Ogni utente è responsabile della ricezione di messaggi di aggiornamento, della pubblicazione di dati su se stesso e del salvataggio su disco (o un DB o Mongo o qualcosa del genere).

Immagino che quello che sto ottenendo è che tutti gli oggetti stateful possono essere attori che aspettano solo che i messaggi si aggiornino.

(Per HTTP (se si desidera implementare ciò stesso), ciascuna richiesta genera un attore che blocca fino a quando non ottiene una risposta (utilizzando!? O un futuro), che viene quindi formattata in una risposta. Molti attori in quel modo, penso.)

Quando arriva una richiesta per cambiare la password per l'utente "[email protected]", si invia un messaggio a "[email protected]"! ChangePassword ("new-segreto").

Oppure si dispone di un processo di directory che tiene traccia delle posizioni di tutti gli attori degli utenti. L'attore UserDirectory può essere un attore stesso (uno per JVM) che riceve messaggi su quali attori utente sono attualmente in esecuzione e quali sono i loro nomi, quindi inoltra loro messaggi dagli attori della richiesta, delegati ad altri attori della directory federata. Dovresti chiedere a UserDirectory dove si trova un utente, quindi inviarlo direttamente. L'attore UserDirectory è responsabile dell'avvio di un attore utente se non ne è già in esecuzione uno. L'attore utente ripristina il suo stato, quindi esclude gli aggiornamenti.

Ecc, e così via.

È divertente pensarci. Ogni attore Utente, ad esempio, può persistere su disco, timeout dopo un determinato periodo di tempo e persino inviare messaggi agli attori Aggregazione. Ad esempio, un attore utente potrebbe inviare un messaggio a un attore LastAccess. Oppure un PasswordTimeoutActor potrebbe inviare messaggi a tutti gli attori degli utenti, dicendo loro di richiedere una modifica della password se la loro password è precedente a una certa data. Gli attori degli utenti possono persino clonare se stessi su altri server o salvarsi in più database.

Divertimento!

+1

Scoprire qualcosa di nuovo è sicuramente una buona idea, ma i dettagli sono pericolosi. Gli attori bloccati bloccano un thread e la tua VM può gestire solo molti di questi. Cioè, implementare tutto come attore potrebbe non scalare il minimo. – Raphael

+0

+1 - È decisamente divertente. Sono d'accordo: dovrei scappare da questa scatola e provare a pensarci fuori. Penso che come primo passo riesca a concentrarmi sull'obiettivo reale: cosa cerco davvero di raggiungere? quali caratteristiche dovrebbe avere questa nuova architettura? Sarebbe anche utile analizzare l'architettura tipica e cercare di identificare le cose che mi piacciono e qualcosa che voglio migliorare. Non credo, che posso raggiungere i miei obiettivi con il modello di attore da solo ... Cercherò di riassumere tutte queste cose. – tenshi

3

Per le operazioni con gli attori, si dovrebbe dare un'occhiata a "Transcators" di Akka, che combinano gli attori con STM (memoria transazionale software): http://doc.akka.io/transactors-scala

è abbastanza grandi cose.

+0

Sono d'accordo con te - STM sarebbe una buona soluzione per l'elaborazione delle transazioni a meno che non avessi diverse JVM in esecuzione. Per favore, correggimi se sbaglio, ma penso che nella transazione di Akka non possa essere distribuita tra diverse JVM (ma per quanto ne so stanno lavorando su STM distribuito). Se ridimensionerò la mia app, installerò diverse JVM identiche e li bilanciamo in base al bilanciamento o semplicemente spargerò i miei attori su diverse JVM. In entrambi i casi non posso avere la stessa transazione su tutte le mie JVM. Ma con le transazioni DB posso ottenere questo. – tenshi

10

Evitare l'elaborazione asincrona a meno che e fino a quando non si abbia una chiara ragione per farlo. Gli attori sono belle astrazioni, ma anche loro non eliminano la complessità intrinseca dell'elaborazione asincrona.

Ho scoperto quella verità nel modo più difficile.Volevo isolare la maggior parte della mia applicazione dall'unico punto reale di potenziale instabilità: il database. Attori in soccorso! Attori di Akka in particolare. Ed è stato fantastico

Martello in mano, mi sono messo a picchiare ogni chiodo in vista. Sessioni utente? Sì, potrebbero essere anche attori. Ehm ... che ne dici di quel controllo degli accessi? Certo, perché no! Con un crescente senso di disagio, ho trasformato la mia architettura fino ad ora in un mostro: più livelli di attori, passaggio di messaggi asincroni, meccanismi elaborati per affrontare condizioni di errore e un caso serio di brutti.

Ho fatto marcia indietro, principalmente.

Ho trattenuto gli attori che mi davano quello di cui avevo bisogno - tolleranza d'errore per il mio codice di persistenza - e ho trasformato tutti gli altri in classi ordinarie.

Posso suggerire di leggere attentamente la domanda/risposta Good use case for Akka? Questo potrebbe darti una migliore comprensione di quando e come gli attori varrà la pena. Se decidi di usare Akka, potresti vedere la mia risposta a una domanda precedente su writing load-balanced actors.

+0

Grazie per aver condiviso la tua esperienza! In realtà ho letto la tua risposta sul bilanciamento del carico prima e mi piace - semplice e pratico (questa volta ho potuto votarlo :) – tenshi

3

Come hai detto, !! = blocking = bad per scalabilità e prestazioni, vedere questo: Performance between ! and !!

La necessità di transazioni si verifica in genere quando si mantiene lo stato anziché gli eventi. Si prega di dare un'occhiata a CQRS e DDDD (Distributed Domain Driven Design) e Event Sourcing, perché, come dici tu, non abbiamo ancora un STM distribuito.

+0

Grazie per i riferimenti! Sembra molto interessante, sicuramente scaverò in questi. – tenshi