开发者

Transferring typical 3-tier architecture to actors

开发者 https://www.devze.com 2023-02-04 05:39 出处:网络
This question bothers me for some time now (I hope I\'m not the only one). I want to take a typical 3-tier Java EE app and see how it possibly can look like implemented with actors. I would like to fi

This question bothers me for some time now (I hope I'm not the only one). I want to take a typical 3-tier Java EE app and see how it possibly can look like implemented with actors. I would like to find o开发者_StackOverflowut whether it actually makes any sense to make such transition and how I can profit from it if it does makes sense (maybe performance, better architecture, extensibility, maintainability, etc...).

Here are typical Controller (presentation), Service (business logic), DAO (data):

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

You can find something like this in many spring applications. We can take simple implementation that does not have any shared state and that's because does not have synchronized blocks... so all state is in the database and application relies on transactions. Service, controller and dao have only one instance. So for each request application server will use separate thread, but threads will not block each other (but will be blocked by DB IO).

Suppose we are trying to implement similar functionality with actors. It can look like this:

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

I think it's not very important here how Get() and Post() extractors are implemented. Suppose I write a framework to implement this. I can send message to controller like this:

controller !! Get("/users")

The same thing would be made by controller and service. In this case the whole workflow would be synchronous. Even worse - I can process only one request at time (in meantime all other requests would land in controller's mailbox). So I need to make it all asynchronous.

Is there any elegant way to perform each processing step asynchronously in this setup?

As far as I understand each tier should somehow save the context of the message it receives and then send message to the tier beneath. When tier beneath replies with some result message I should be able to restore initial context and reply with this result to the original sender. Is this correct?

Moreover, at the moment I have only one instance of actor for each tier. Even if they will work asynchronously, I still can process in parallel only one controller, service and dao message. This means that I need more actors of the same type. Which leads me to LoadBalancer for each tier. This also means, that if I have UserService and ItemService I should LoadBalace both of them separately.

I have feeling, that I understand something wrong. All needed configuration seems to be overcomplicated. What do you think about this?

(PS: It would be also very interesting to know how DB transactions fit into this picture, but I think it's overkill for this thread)


Avoid asynchronous processing unless and until you have a clear reason for doing it. Actors are lovely abstractions, but even they don't eliminate the inherent complexity of asynchronous processing.

I discovered that truth the hard way. I wanted to insulate the bulk of my application from the one real point of potential instability: the database. Actors to the rescue! Akka actors in particular. And it was awesome.

Hammer in hand, I then set about bashing every nail in view. User sessions? Yes, they could be actors too. Um... how about that access control? Sure, why not! With a growing sense of un-ease, I turned my hitherto simple architecture into a monster: multiple layers of actors, asynchronous message passing, elaborate mechanisms to deal with error conditions, and a serious case of the uglies.

I backed out, mostly.

I retained the actors that were giving me what I needed - fault-tolerance for my persistence code - and turned all of the others into ordinary classes.

May I suggest that you carefully read the Good use case for Akka question/answers? That may give you a better understanding of when and how actors will be worthwhile. Should you decide to use Akka, you might like to view my answer to an earlier question about writing load-balanced actors.


Just riffing, but...

I think if you want to use actors, you should throw away all previous patterns and dream up something new, then maybe re-incorporate the old patterns (controller, dao, etc) as necessary to fill in the gaps.

For instance, what if each User is an individual actor sitting in the JVM, or via remote actors, in many other JVMs. Each User is responsible for receiving update messages, publishing data about itself, and saving itself to disk (or a DB or Mongo or something).

I guess what I'm getting at is that all your stateful objects can be actors just waiting for messages to update themselves.

(For HTTP (if you wanted to implement that yourself), each request spawns an actor that blocks until it gets a reply (using !? or a future), which is then formatted into a response. You can spawn a LOT of actors that way, I think.)

When a request comes in to change the password for user "foo@example.com", you send a message to 'Foo@Example.Com' ! ChangePassword("new-secret").

Or you have a directory process which keeps track of the locations of all User actors. The UserDirectory actor can be an actor itself (one per JVM) which receives messages about which User actors are currently running and what their names are, then relays messages to them from the Request actors, delegates to other federated Directory actors. You'd ask the UserDirectory where a User is, and then send that message directly. The UserDirectory actor is responsible for starting a User actor if one isn't already running. The User actor recovers its state, then excepts updates.

Etc, and so on.

It's fun to think about. Each User actor, for instance, can persist itself to disk, time out after a certain time, and even send messages to Aggregation actors. For instance, a User actor might send a message to a LastAccess actor. Or a PasswordTimeoutActor might send messages to all User actors, telling them to require a password change if their password is older than a certain date. User actors can even clone themselves onto other servers, or save themselves into multiple databases.

Fun!


Large compute-intensive atomic transactions are tricky to pull off, which is one reason why databases are so popular. So if you are asking whether you can transparently and easily use actors to replace all the transactional and highly-scalable features of a database (whose power you are very heavily leaning on in the Java EE model), the answer is no.

But there are some tricks you can play. For example, if one actor seems to be causing a bottleneck, but you don't want to go to the effort of creating a dispatcher/worker farm structure, you may be able to move the intensive work into futures:

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

This way, the really expensive tasks get spawned off in new threads (assuming that you don't need to worry about atomicity and deadlocks and so on, which you may--but again, solving these problems is not easy in general) and messages get sent along to wherever they should be going regardless.


For transactions with actors, you should take a look at Akka's "Transcators", which combine actors with STM (software transactional memory): http://doc.akka.io/transactors-scala

It's pretty great stuff.


As you said, !! = blocking = bad for scalability and performance, see this: Performance between ! and !!

The need for transactions usually occur when you are persisting state instead of events. Please have a look at CQRS and DDDD (Distributed Domain Driven Design) and Event Sourcing, because, as you say, we still haven't got a distributed STM.

0

精彩评论

暂无评论...
验证码 换一张
取 消