2011-01-16 11 views
21

To pytanie dręczy mnie już od pewnego czasu (mam nadzieję, że nie jestem jedyny). Chcę wziąć typową trójwarstwową aplikację Java EE i zobaczyć, jak to możliwe, że może wyglądać jak zaimplementowana z aktorami. Chciałbym dowiedzieć się, czy rzeczywiście ma sens takie przejście i jak mogę z niego skorzystać, jeśli ma to sens (może wydajność, lepsza architektura, rozszerzalność, łatwość konserwacji itd.).Przenoszenie typowej architektury trójwarstwowej na aktorów.

Oto typowy kontroler (prezentacja), usługi (logika biznesowa), DAO (dane):

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

Można znaleźć coś takiego w wielu zastosowaniach sprężynowych. Możemy przyjąć prostą implementację, która nie ma żadnego współdzielonego stanu, a to dlatego, że nie ma zsynchronizowanych bloków ... więc cały stan jest w bazie danych, a aplikacja opiera się na transakcjach. Usługa, kontroler i dao mają tylko jedno wystąpienie. Tak więc dla każdego żądania serwer aplikacji użyje osobnego wątku, ale wątki nie będą się blokować (ale będą blokowane przez DB IO).

Załóżmy, że próbujemy wprowadzić podobną funkcjonalność z aktorami. To może wyglądać następująco:

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

myślę, że nie jest to bardzo ważne, tutaj jak Get() i Poczty (odciągi) są realizowane. Załóżmy, że piszę framework do wdrożenia tego. Mogę wysłać wiadomość do kontrolera w ten sposób:

controller !! Get("/users") 

To samo zrobi kontroler i obsługa. W takim przypadku cały przepływ pracy byłby synchroniczny. Co gorsza - mogę przetworzyć tylko jedną prośbę o czasie (w międzyczasie wszystkie inne żądania trafią do skrzynki pocztowej kontrolera). Muszę więc zrobić to wszystko asynchronicznie.

Czy istnieje jakiś elegancki sposób wykonania asynchronicznego wykonywania każdego kroku przetwarzania w tej konfiguracji?

O ile rozumiem, każdy poziom powinien w jakiś sposób zapisać kontekst wiadomości, którą otrzymuje, a następnie wysłać wiadomość do warstwy poniżej. Kiedy poziom poniżej odpowiedzi za pomocą jakiegoś komunikatu wynikowego powinienem być w stanie przywrócić początkowy kontekst i odpowiedzieć tym wynikiem pierwotnemu nadawcy. Czy to jest poprawne?

Co więcej, w tej chwili mam tylko jedną instancję aktora dla każdej warstwy. Nawet jeśli będą działać asynchronicznie, nadal mogę przetwarzać równolegle tylko jeden kontroler, usługę i wiadomość dao. Oznacza to, że potrzebuję więcej aktorów tego samego typu. Co prowadzi mnie do LoadBalancer dla każdego poziomu. Oznacza to również, że jeśli mam usługę UserService i ItemService, powinienem załadować oba te ustawienia osobno.

Mam wrażenie, że rozumiem coś złego. Wszystkie potrzebne konfiguracje wydają się zbyt skomplikowane. Co o tym myślisz?

(PS: Byłoby bardzo interesujące wiedzieć, jak transakcje DB pasuje do tego obrazu, ale myślę, że to przesada dla tego wątku)

+0

+1 - Ambitne rzeczy od ciebie, Easy Angel. – duffymo

Odpowiedz

4

Duże transakcje atomowe intensywnych obliczeniowo są trudne do zdjąć, co jest jeden z powodów, dlaczego bazy danych są tak popularne. Jeśli więc pytasz, czy możesz w przejrzysty i łatwy sposób wykorzystać aktorów do zastąpienia wszystkich transakcyjnych i wysoce skalowalnych funkcji bazy danych (której moc bardzo mocno opierasz się w modelu Java EE), odpowiedź brzmi: nie.

Ale są pewne sztuczki, które można grać.Na przykład, jeśli jeden aktor wydaje się powodować wąskie gardło, ale nie chcesz podejmować wysiłku tworzenia struktury farmy dyspozytora/pracownika, możesz być w stanie przenieść intensywną pracę na kontrakty futures:

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

W ten sposób bardzo kosztowne zadania zostają odrodzone w nowych wątkach (zakładając, że nie musisz martwić się o atomowość i zakleszczenia itd., Które możesz - ale znowu, rozwiązanie tych problemów nie jest łatwe w ogóle) a wiadomości są wysyłane dalej, niezależnie od tego, gdzie powinny iść.

+0

O ile oczywiście nie rozpoczniesz odradzania się, mogą takie wątki na jednym serwerze. Wtedy twoje rozwiązanie będzie słabo skalowane. – wheaties

+1

@ gratki: Rzeczywiście. Twoja wydajność bazy danych byłaby bardzo imponująca również na wspomnianej maszynie. –

5

Wystarczy riffy, ale ...

Myślę, że jeśli chcesz korzystać z aktorów, należy wyrzucić wszystkie poprzednie wzory i wymyślać coś nowego, to może ponownie włączyć stare wzory (regulator dao, itp.) w razie potrzeby, aby wypełnić luki.

Na przykład, jeśli każdy użytkownik jest indywidualnym aktorem siedzącym w JVM lub zdalnych aktorów w wielu innych maszynach JVM. Każdy użytkownik jest odpowiedzialny za otrzymywanie wiadomości aktualizujących, publikowanie danych o sobie i zapisywanie się na dysku (lub DB lub Mongo lub coś podobnego).

Domyślam się, że wszystkie twoje obiekty stanowe mogą być aktorami, którzy tylko czekają, aż wiadomości się zaktualizują.

(W przypadku HTTP (jeśli chcesz to zaimplementować samodzielnie), każde żądanie uruchamia aktora, który blokuje, dopóki nie otrzyma odpowiedzi (używając!? Lub przyszłości), który jest następnie sformatowany w odpowiedzi. WIELU aktorów w ten sposób, jak sądzę.)

Gdy pojawi się żądanie zmiany hasła dla użytkownika "[email protected]", wyślesz wiadomość na adres "[email protected]"! ChangePassword ("new-secret").

Albo masz proces katalogowy, który śledzi lokalizacje wszystkich użytkowników User. Aktor UserDirectory może być samym aktorem (po jednym na JVM), który otrzymuje komunikaty o tym, którzy aktorzy Użytkownicy są aktualnie uruchomieni i jakie są ich nazwy, a następnie przekazuje im wiadomości od aktorów żądania, deleguje do innych stowarzyszonych podmiotów katalogu. Użytkownik powinien zapytać UserDirectory, gdzie jest użytkownik, a następnie bezpośrednio wysłać tę wiadomość. Aktor UserDirectory jest odpowiedzialny za uruchomienie aktora użytkownika, jeśli jeszcze nie jest uruchomiony. Aktor użytkownika odzyskuje swój stan, a następnie oprócz aktualizacji.

Itd, i tak dalej.

Fajnie jest o tym myśleć. Każdy aktor, na przykład, może przetrwać na dysku, przestać działać po określonym czasie, a nawet wysyłać wiadomości do aktorów Agregacji. Na przykład aktor użytkownika może wysłać wiadomość do aktora LastAccess. Lub PasswordTimeoutActor może wysyłać wiadomości do wszystkich użytkowników Użytkowników, nakazując im zmianę hasła, jeśli ich hasło jest starsze niż określona data. Użytkownicy mogą nawet klonować się na innych serwerach lub zapisywać się w wielu bazach danych.

Zabawa!

+1

Tworzenie czegoś nowego jest zdecydowanie dobrym pomysłem, ale szczegóły są niebezpieczne. Zablokowani aktorzy blokują wątek, a twoja maszyna wirtualna może obsłużyć tylko tyle z nich. Oznacza to, że wdrażanie wszystkiego, ponieważ aktor może nie skalować się w najmniejszym stopniu. – Raphael

+0

+1 - To zdecydowanie zabawa. Zgadzam się - powinienem uciec z tego pudełka i spróbować myśleć poza nim. Myślę, że jako pierwszy krok mogę skoncentrować się na rzeczywistym celu - co tak naprawdę staram się osiągnąć? jakie cechy powinna mieć ta nowa architektura? Przydatna byłaby również analiza typowej architektury i próba zidentyfikowania rzeczy, które mi się podobają i czegoś, co chcę poprawić. Nie wierzę, że mogę osiągnąć moje cele samym aktorem ... Spróbuję podsumować wszystkie te rzeczy. – tenshi

3

Dla transakcji z podmiotami, należy przyjrzeć Akka za „Transcators”, które łączą aktorów z STM (pamięć transakcyjnego oprogramowania): http://doc.akka.io/transactors-scala

To bardzo wielkie rzeczy.

+0

Zgadzam się z Tobą - STM byłby dobrym rozwiązaniem do przetwarzania transakcji, chyba że mam kilka JVM działających. Proszę, popraw mnie, jeśli się mylę, ale myślę, że w transakcji Akki nie można rozłożyć na kilka JVM (ale o ile mi wiadomo, pracują na rozproszonym STM). Jeśli skalowuję swoją aplikację, ustawię kilka identycznych JVM i zrównuję obciążenie lub po prostu podzielę moich aktorów na kilka JVM. W obu przypadkach nie mogę mieć tej samej transakcji we wszystkich moich maszynach JVM. Ale dzięki transakcjom DB mogę to osiągnąć. – tenshi

10

Unikaj przetwarzania asynchronicznego, chyba że masz do tego wyraźny powód. Aktorzy są uroczymi abstrakcjami, ale nawet one nie eliminują nieodłącznej złożoności przetwarzania asynchronicznego.

Odkryłem tę prawdę na własnej skórze.Chciałem odizolować większość mojej aplikacji od jednego punktu potencjalnej niestabilności: bazy danych. Aktorzy na ratunek! Akka aktorzy w szczególności. I było niesamowite.

Młotek w ręce, a następnie przystąpiłem do bicia każdego gwoździa. Sesje użytkownika? Tak, mogą też być aktorami. Um ... a może kontrola dostępu? Jasne, czemu nie! Wraz z rosnącym poczuciem łatwości, zamieniłem moją dotychczas prostą architekturę w potwora: wiele warstw aktorów, przekazywanie wiadomości asynchronicznych, rozbudowane mechanizmy radzenia sobie z warunkami błędu i poważny przypadek brzydkich.

Wycofałem się, głównie.

Zachowałem aktorów, którzy dawali mi to, czego potrzebowałem - odporność na błędy dla mojego kodu uporczywości - i zmienili wszystkich pozostałych w zwykłe klasy.

Czy mogę polecić uważnie przeczytanie pytania/odpowiedzi Good use case for Akka? To może pomóc ci lepiej zrozumieć, kiedy i jak będą działać aktorzy. Jeśli zdecydujesz się użyć Akka, możesz zobaczyć moją odpowiedź na wcześniejsze pytanie dotyczące writing load-balanced actors.

+0

Dzięki za podzielenie się wrażeniami! Czytałem wcześniej odpowiedź na temat równoważenia obciążenia i lubię to - proste i praktyczne (tym razem mogłem zagłosować) :) – tenshi

3

Jak już powiedziałeś, !! = blokowanie = złe ze względu na skalowalność i wydajność, patrz: Performance between ! and !!

Potrzeba transakcji zwykle występuje, gdy utrzymujesz stan zamiast zdarzeń. Proszę spojrzeć na CQRS i DDDD (Distributed Domain Driven Design) i Event Sourcing, ponieważ, jak pan twierdzi, nadal nie mamy rozproszonego STM.

+0

Dzięki za referencje! Wygląda bardzo interesująco, zdecydowanie się w to zagłębię. – tenshi

Powiązane problemy