2011-01-16 15 views
21

Esta pregunta me molesta desde hace un tiempo (espero no ser el único). Quiero tomar una aplicación típica Java EE de 3 niveles y ver cómo se ve cómo se implementa con los actores. Me gustaría saber si realmente tiene sentido hacer tal transición y cómo puedo sacar provecho si tiene sentido (tal vez el rendimiento, una mejor arquitectura, extensibilidad, capacidad de mantenimiento, etc.).Transfiriendo la arquitectura típica de 3 niveles a los actores

Éstos son de Typical (presentación), Servicio (lógica de negocio), DAO (datos):

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 = { ... } 
} 

Usted puede encontrar algo como esto en muchas aplicaciones de primavera. Podemos tomar una implementación sencilla que no tenga ningún estado compartido y eso se debe a que no tiene bloques sincronizados ... por lo que todo el estado está en la base de datos y la aplicación depende de las transacciones. Service, controller y dao tienen solo una instancia. Por lo tanto, para cada solicitud, el servidor de aplicaciones usará un hilo separado, pero los hilos no se bloquearán entre sí (pero serán bloqueados por DB IO).

Supongamos que estamos intentando implementar una funcionalidad similar con los actores. Puede tener este aspecto:

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) => ... 
} 

Creo que no es muy importante aquí cómo get() y se implementan Post() extractores. Supongamos que escribo un marco para implementar esto. Puedo enviar un mensaje al controlador de esta manera:

controller !! Get("/users") 

Lo mismo se haría con el controlador y el servicio. En este caso, todo el flujo de trabajo sería sincrónico. Lo que es peor, puedo procesar solo una solicitud a la vez (mientras tanto, todas las demás solicitudes caen en el buzón del controlador). Entonces necesito hacer que todo sea asincrónico.

¿Hay alguna forma elegante de realizar cada paso de procesamiento de forma asíncrona en esta configuración?

Por lo que yo entiendo, cada nivel debe de alguna manera guardar el contexto del mensaje que recibe y luego enviar el mensaje al nivel que se encuentra debajo. Cuando el nivel debajo de las respuestas con algún mensaje de resultado debería ser capaz de restaurar el contexto inicial y responder con este resultado al remitente original. ¿Es esto correcto?

Además, en este momento solo tengo una instancia de actor para cada nivel. Incluso si funcionan de forma asíncrona, aún puedo procesar en paralelo solo un controlador, servicio y mensaje de dao. Esto significa que necesito más actores del mismo tipo. Lo que me lleva a LoadBalancer para cada nivel. Esto también significa que si tengo UserService y ItemService, debería LoadBalace ambos por separado.

Tengo la sensación de que entiendo algo mal. Toda la configuración necesaria parece ser demasiado complicada. ¿Qué piensas sobre esto?

(PS: También sería muy interesante saber cómo las transacciones de base de datos encajan en este cuadro, pero creo que es excesivo para este thread)

+0

+1 - Ambiciosas cosas tuyas, Easy Angel. – duffymo

Respuesta

4

transacciones atómicas grandes de cálculo intensivo son difícil de lograr, que es una razón por la cual las bases de datos son tan populares. Entonces, si está preguntando si puede usar actores de forma transparente y fácil para reemplazar todas las características transaccionales y altamente escalables de una base de datos (cuya potencia está muy inclinada en el modelo Java EE), la respuesta es no.

Pero hay algunos trucos que puedes jugar.Por ejemplo, si un actor parece estar causando un cuello de botella, pero no desea realizar el esfuerzo de crear una estructura de granja de operarios/despachadores, puede mover el trabajo intensivo a futuros:

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

De esta manera, las tareas realmente costosas se generan en nuevos hilos (suponiendo que no necesita preocuparse por la atomicidad y los interbloqueos, etc., lo que puede hacer, pero una vez más, resolver estos problemas no es fácil en general) y los mensajes se envían a donde sea que vayan independientemente.

+0

A menos que, por supuesto, comience a engendrar esos hilos en un solo servidor. Entonces su solución escalaría pobremente. – wheaties

+1

@wheaties: De hecho. El rendimiento de su base de datos también sería muy poco impresionante en dicha máquina. –

5

Sólo riffs, pero ...

Creo que si se desea utilizar actores, se debe tirar a la basura todos los patrones anteriores y soñar con algo nuevo, entonces tal vez volver a incorporar los viejos patrones (controlador, DAO, etc.) según sea necesario para llenar los vacíos.

Por ejemplo, ¿qué pasa si cada Usuario es un actor individual sentado en la JVM, o mediante actores remotos, en muchas otras JVM? Cada usuario es responsable de recibir mensajes de actualización, publicar datos sobre sí mismo y guardarse en el disco (o un DB o Mongo o algo así).

Creo que a lo que me refiero es a que todos sus objetos con estado pueden ser actores que esperan mensajes para actualizarse.

(Para HTTP (si desea implementarlo usted mismo), cada solicitud genera un actor que bloquea hasta que obtiene una respuesta (usando!? O un futuro), que luego se formatea en una respuesta. Puede generar un MUCHOS actores de esa manera, creo).

Cuando llega una solicitud para cambiar la contraseña del usuario "[email protected]", envía un mensaje a '[email protected]'! ChangePassword ("nuevo secreto").

O tiene un proceso de directorio que realiza un seguimiento de las ubicaciones de todos los actores de usuario. El actor de User Directory puede ser un actor en sí mismo (uno por JVM) que recibe mensajes sobre qué actores de usuario se están ejecutando actualmente y cuáles son sus nombres, luego les transmite mensajes desde los actores de Request, delegados a otros actores de Directory federados. Le preguntaría a UserDirectory dónde está un Usuario, y luego enviará ese mensaje directamente. El actor de User Directory es responsable de iniciar un actor de usuario si no se está ejecutando uno. El actor de Usuario recupera su estado, luego exceptúa las actualizaciones.

Etc, y así sucesivamente.

Es divertido pensar en ello. Cada actor de usuario, por ejemplo, puede persistir en el disco, agotar el tiempo de espera después de un cierto tiempo e incluso enviar mensajes a los agentes de agregación. Por ejemplo, un actor de usuario podría enviar un mensaje a un actor de LastAccess. O un PasswordTimeoutActor podría enviar mensajes a todos los actores del usuario, diciéndoles que soliciten un cambio de contraseña si su contraseña es anterior a cierta fecha. Los usuarios pueden incluso clonarse en otros servidores o guardarse en múltiples bases de datos.

Diversión!

+1

Diseñar algo nuevo definitivamente es una buena idea, pero sus detalles son peligrosos. Los actores bloqueados bloquean un hilo y tu máquina virtual puede manejar solo muchos de esos. Es decir, implementar todo, ya que el actor podría no escalar en lo más mínimo. – Raphael

+0

+1 - Definitivamente es divertido. Estoy de acuerdo, debería escapar de esta caja e intentar pensar fuera de esta. Creo que como primer paso puedo concentrarme en la meta real: ¿qué estoy tratando de lograr? ¿Qué características debe tener esta nueva arquitectura? También sería útil analizar la arquitectura típica e intentar identificar las cosas que me gustan y lo que quiero mejorar. No creo, puedo lograr mis objetivos solo con el modelo de actor ... Intentaré resumir todas estas cosas. – tenshi

3

Para las transacciones con actores, debería echar un vistazo a "Transcators" de Akka, que se combinan con los actores STM (memoria transaccional de software): http://doc.akka.io/transactors-scala

Es bastante grandes cosas.

+0

Estoy de acuerdo con usted: STM sería una buena solución para el procesamiento de transacciones a menos que tenga varias JVM en ejecución. Por favor, corríjanme si me equivoco, pero creo que en la transacción de Akka no se puede distribuir a través de varias JVM (pero hasta donde sé que están trabajando en STM distribuido). Si voy a escalar mi aplicación, configuraré varias JVM idénticas y las equilibraré, o simplemente extenderé mis actores a través de varias JVM. En cualquier caso, no puedo tener la misma transacción en todas mis JVM. Pero con las transacciones DB puedo lograr esto. – tenshi

10

Evite el procesamiento asíncrono a menos y hasta que tenga un motivo claro para hacerlo. Los actores son abstracciones encantadoras, pero incluso ellos no eliminan la complejidad inherente del procesamiento asincrónico.

Descubrí esa verdad de la manera difícil.Quería aislar la mayor parte de mi aplicación del único punto de inestabilidad potencial: la base de datos. Actores al rescate! Actores Akka en particular. Y fue increíble.

Martillo en mano, me puse a golpear cada clavo a la vista. Sesiones de usuario? Sí, podrían ser actores también. Um ... ¿qué tal ese control de acceso? ¡Seguro Por qué no! Con una creciente sensación de incomodidad, convertí mi arquitectura hasta ahora simple en un monstruo: múltiples capas de actores, paso de mensajes asíncrono, mecanismos elaborados para tratar las condiciones de error y un caso grave de los feos.

Me retiré, principalmente.

Retení a los actores que me daban lo que necesitaba - tolerancia a fallas para mi código de persistencia - y convertí a todos los demás en clases ordinarias.

Le sugiero que lea cuidadosamente la pregunta Good use case for Akka preguntas/respuestas? Eso puede darle una mejor comprensión de cuándo y cómo valdrán los actores. En caso de que decida utilizar Akka, le agradecería ver mi respuesta a una pregunta anterior sobre writing load-balanced actors.

+0

¡Gracias por compartir tu experiencia! De hecho, leí tu respuesta sobre el equilibrio de carga antes y me gusta, simple y práctica (esta vez pude votarla :) – tenshi

3

Como dijiste, !! = bloqueo = malo para la escalabilidad y el rendimiento, vea esto: Performance between ! and !!

La necesidad de realizar transacciones generalmente ocurre cuando está persistiendo el estado en lugar de los eventos. Por favor, eche un vistazo a CQRS y DDDD (Distributed Domain Driven Design) y Event Sourcing, porque, como usted dice, todavía no tenemos un STM distribuido.

+0

¡Gracias por las referencias! Parece muy interesante, definitivamente voy a profundizar en estos. – tenshi

Cuestiones relacionadas