Periodic, în industria software apare un nou set de caracteristici sau calităţi pe care ar trebui să le aibă o aplicaţie modernă şi noi reguli despre cum ar trebui abordată dezvoltarea acestor aplicaţii. Exemple recente fiind: Reactive manifesto (figura 1), aplicaţii 12factors sau multitudinea de articole din sfera micro-serviciilor.
Încercând să listăm câteva aspecte importante, care în opinia noastră caracterizează momentan o aplicaţie modernă, am identificat următoarele:
o arhitectură care le permite să evolueze rapid, în pas cu schimbările cerinţelor de business;
pot să folosească în avantajul lor modele de computaţie moderne cum ar fi platforme cloud;
se pretează unui proces de dezvoltare modern, cum ar fi unul bazat pe livrare continuă;
folosesc resursele eficient - acestea pot fi scalate dinamic în sus sau în jos, în funcţie de încărcare;
În acest articol vom analiza sistemele bazate pe actori şi Akka - un set de instrumente şi un mediu de execuţie pentru JVM, o tehnologie care bifează toate menţiunile noastre de mai sus referitoare la aplicaţii moderne. Ca suport vom folosi o aplicaţie, proof-of-concept, bazată pe o nevoie practică reală. Cu această ocazie testăm şi abordarea lui Martin Fowler de a intra într-un subiect nou - prin a scrie un articol sau o carte despre subiect - şi a vedea dacă se aplică oricui, sau e rezervată exclusiv maeştrilor.
Akka se prezintă în linii mari ca fiind: un set de instrumente şi un mediu de execuţie proiectat pentru a construi aplicaţii distribuite, cu un grad ridicat de concurenţă şi bazate pe pasarea de mesaje folosind JVM-ul. Din concepţie, o tehnologie pentru aplicaţii asincrone şi distribuite. Abstracţiile principale fiind: actorii, fluxurile şi promisiunile (futures). Potrivită pentru a dezvolta aplicaţii care au nevoie de capacitatea de a se auto-repara. Orice sistem, care are nevoie să proceseze un volum mare de informaţii cu o latenţă redusă, este un candidat potrivit pentru a fi implementat folosind Akka.
Această ultimă frază a influenţat decizia noastră de a folosi tehnologia Akka, pentru aplicaţia demonstrativă, dar vom expune mai pe larg la secţiunea de implementare.
Aplicaţiile construite cu Akka aderă la Reactive Manifesto, ceea ce se traduce prin următoarele caracteristici:
Message-driven- Sistemele reactive au la bază un model de comunicare bazat pe trimitere de mesaje non-blocante, asincrone. Aceasta permite izolarea eşecului unei componente şi prin strategii de supervizare, repararea acelei componente.
Resilience— Abilitatea sistemelor de a se auto-vindeca într-un mod previzibil şi automat. Este parte din ciclul de viaţă a aplicaţiei şi este posibilă datorită acestei abordări de comunicare prin mesaje.
Elasticitatea—Scalarea verticală/orizontală peste orice infrastructură fizică sau cloud. Din nou, posibilă datorită decuplării pe care fundaţia de comunicare o impune.
Am ales să încercăm Akka şi datorită caracteristicilor interesante listate mai sus, care se ataşează oricărei aplicaţii create cu Akka, dar şi datorită modelului "sistem de actori". Acest sistem de actori reprezintă un sistem de a modela domeniile şi procesele într-un mod mai apropiat de modul nostru, al oamenilor, de a aborda şi modela probleme cu scopul de a le rezolva. În cele ce urmează, vom descrie mai în detaliu aceşti actori.
Actorii sunt abstracţia de bază în Akka, încapsulează starea şi comportamentul şi sunt similari obiectelor din OOP. Comunică asincron prin mesaje, existând interpretări ale declaraţiei lui Alan Kay (pionier OOP): "The big idea is messaging", cum că sistemele bazate pe actori ar fi de fapt o implementare corectă a OOP.
Actorii sunt unitatea de organizare de cod din Akka, similar cu beanurile de sesiune din JavaEE. Sunt concepuţi în aşa fel încât atenţia programatorului să fie înspre implementarea cerinţelor funcţionale ale sistemului, şi nu înspre detalii de genul: locaţia altor actori, primitive de concurenţă, utilizarea optimă a resurselor hardware, etc. .
Figura 2 prezintă structura unui actor:
căsuţa de mesaje (mailbox) - folosită pentru a conecta emiţătorii de mesaje de actorul curent - aici sunt adăugate în coadă mesajele pentru a fi procesate. Fiecare actor are exact o căsuţă, care poate avea mai multe implementări - cea standard fiind FIFO, unde actorul trebuie să proceseze totdeauna următorul mesaj din coadă.
starea internă (internal state) - conţine în mod tipic variabile sau explicit o maşină de stări finită (FSM). Corectitudinea programului depinde de faptul că actorul este tot timpul într-o stare consistentă, de aceea în momentul în care actorul suferă un eşec, starea este recreată din cea iniţială pentru a da sistemului şansa să se repare.
Actorii formează ierarhii, iar un actor responsabil de o anumită funcţionalitate are posibilitatea să descompună procesul în taskuri mai mici, mai uşor de gestionat, pe care apoi le poate delega unor actori copil, şi doar superviza dacă aceştia colaborează corect şi eficient pentru a îndeplini cerinţa de business. În felul acesta se aseamănă unei echipe de specialişti dintr-un anumit domeniu, care lucrează împreună, folosindu-şi abilităţile diferite, pentru a rezolva o problemă mai complexă. Aceasta conduce la o încapsulare mai bună a comportamentelor fiecărui actor şi la separarea acestor comportamente de procesul de comunicare.
Un sistem proiectat în felul acesta are o arhitectură care reflectă domeniul problemei modelate - uncle Bob ar fi încântat, are o structură logică care urmăreşte procesele ce trebuie implementate şi codul în sine devine un exemplu al principiului singurei responsabilităţi.
Gestiunea eşecului este realizată printr-o caracteristică implementată în platformă, şi anume: supervizarea, care presupune o relaţie între actori de tipul supervizor-supervizat, unde primul urmăreşte defecţiunile în toţi actorii pe care îi supervizează. Exemplificat folosind Figura 3, actorul B3 este responsabil să repornească sau să folosească orice altă strategie de vindecare, în caz că oricare dintre actorii B5,B6,B7 ar deveni indisponibili.
Pentru o problemă reală a unui client, care avea nevoie de o soluţie software scalabilă şi capabilă să proceseze volume mari de date, am reuşit să construim o soluţie concept (proof-of-concept), folosind Akka, şi în secţiunile care urmează vom descrie cum a fost experienţa.
Figura 4: Contextul problemei
Figura 4 încearcă să rezume contextul problemei: în esenţă există un număr de aplicații (App1, App2, ...) care publică evenimente pentru fiecare tranzacție pe care o realizează, iar aceste evenimente trebuie să ajungă în fişiere jurnal, grupate pentru fiecare aplicaţie. Cerinţe specifice domeniului în care activează clientul:
Intrările în jurnal trebuie să rămână în ordinea în care au fost introduse și să fie înlănţuite. Nu se pot introduce intrări noi între cele existente și nu se pot falsifica cele introduse deja.
Periodic, sistemul trebuie să folosească un sistem extern de validare a timpului şi să marcheze anumite intrări cu rezultatul obţinut. Scopul fiind ca nici măcar clientul nostru - operatorul soluţiei - să nu poată manipula aceste fişiere. De aceea e nevoie de a valida faptul că un anumit eveniment s-a produs la momentul la care susţine clientul.
E nevoie ca aceste jurnale să reprezinte o "sursă de adevăr" asupra ce s-a întâmplat, în momentul în care din orice motiv, s-ar face un audit asupra tranzacţiilor realizate de către sistemele clientului.
În Figura 4 sunt reprezentate o serie de operații care trebuie executate pentru fiecare eveniment: corelarea, marcajul de timp, hashingul. Dintre aceste operaţii unele au nevoie de operații I/O, altele pot rula în paralel, iar arhitectura folosind sisteme de actori ne permite să modelăm aceste nevoi diferite şi să le încapsulăm - în actori, să le decuplăm, şi să folosim semantica de comunicare potrivită pentru fiecare din acestea: sincronă sau asincronă.
Dintre motivele pentru care am ales Akka au fost: nevoia ca aplicația să fie întotdeauna disponibilă; dorinţa de a suporta un flux mare de evenimente - pentru că toate componentele din sistemul clientului o să o folosească, și pentru că oferă un model de gestionare al concurenței necesar pentru a optimiza timpii de procesare al mesajelor până în momentul în care acestea sunt colectate în jurnal. Toate aceste cerinţe, după cum am văzut în prima parte a articolului, fiind unele din punctele forte ale tehnologiei Akka.
Figura 5: Arhitectura aplicaţiei
Figura 5 prezintă organizarea actorilor, o arhitectură logică a sistemului, de unde se poate observa faptul că organizarea structurilor aplicaţiei e foarte similară cu contextul de business al problemei, făcând astfel sistemul mai uşor de înţeles și de modificat. Fiecare fază din proces este realizată de un actor sau de un grup de actori (părinte și actor-muncitor), care implementează funcţionalitatea lor specifică și mecanismele de supraveghere și corectare a erorilor.
Nivelul superior de actori reprezintă nucleul sistemului, iar "coborând" în ierarhie găsim actori tot mai specializați pe anumite funcţionalităţi sau actori-muncitori care sunt creați pentru a suporta un volum mai mare de evenimente.
De exemplu, în figură este reprezentat:
Actorul System - rădăcina ierarhiei, iniţializează sistemul Akka și este responsabil pentru supravegherea actorilor din nivelul superior.
Actorul HashMaster - este un actor superior care știe cum să facă hash pe mesajele primite de la aplicații, și care creează actori noi dacă o nouă aplicație se înregistrează (reprezentaţi în imagine cu HashWorker1, HashWorker2 pentru App1, App2). Valoarea de hash a intrării curente depinde de cea a intrării imediat anterioare, făcând în acest fel imposibilă modificarea ordinii acestora.
Actorul HashWorker1 - este de acelaşi tip cu HashMaster, doar că este specializat în a procesa doar mesajele de la prima aplicație. Este creat când Aplicaţia 1 s-a înregistrat printr-un mesaj trimis către procesare fie către HashMasterActor, sau System Actor sau Admin Actor.
Actorul Config - folosit pentru a configura sistemul şi pe ceilalţi actori.
Actorul Monitor - folosit pentru a monitoriza sistemul, şi în cazul aplicaţiei concept şi pentru a colecta statistici de folosire cu scopul de a evalua performanţa sistemului.
Vom analiza omponentele de bază al unui actor Akka folosindu-ne de codul de implementare al clasei actorului HashHandlerMaster, de mai jos. Toţi ceilalţi actori sunt implementaţi similar, diferenţele fiind prezente la implementarea comportamentelor specifice fiecărei funcţionalităţi, şi la faptul că reacţionează la mesaje de tip diferit.
public class HashHandlerMaster extends
AbstractLoggingActor {
(1)
private Map
childActors = new HashMap<>();
public HashHandlerMasterActor() {
(2)
getContext().getSystem().eventStream().
subscribe(getSelf(), NewJournalEvent.class);
}
(3)
@Override
public Receive createReceive() {
return receiveBuilder()
.match(NewJournalEvent.class,
this::onNewJournalEvent)
.match(Object.class, this::unhandled)
.build();
}
(4)
private SupervisorStrategy strategy =
new OneForOneStrategy(false,
DeciderBuilder.matchAny(
e -> SupervisorStrategy.restart()).build());
(5)
private void onNewJournalEvent(NewJournalEvent event) {
String topic = event.getTopic();
ActorRef actorRef = childActors.
computeIfAbsent(topic, t -> getContext().
actorOf(Props.create(HashHandlerWorkerActor.class),
"hashHandlerWorker" + t));
actorRef.tell(event, getSender());
}
}
(1) Actorul master conține un dicționar în care sunt salvate referințele la actorii muncitori pentru fiecare topic. Astfel, fiecare actor procesează numai mesaje de un anumit tip și distribuirea sarcinilor se efectuează de actorul master. Motivul pentru care am creat un actor per topic și nu un grup de actori, sunt cerințele de hashing: fiecare hash nou se calculează în funcție de hash-ul precedent.
(2) Un mecanism util oferit de Akka este modelul subscriere/publicare. Fiecare actor poate subscrie pentru a consuma sau publica evenimente. În cadrul soluţiei- concept am folosit mecanismul EventBus prezent în Akka, însă soluția poate fi extinsă pentru a integra diferiți brokeri de mesaje cum ar fi: RabbitMQ, Kafka sau ActiveMQ.
Actorul de hashing subscrie unui eveniment de tipul NewJournalEvent, care este un simplu POJO, cu un cod concis dacă folosim adnotările din lombok:
@Data
public class NewJournalEvent implements Serializable {
private static final long serialVersionUID =
8999071264659069395L;
// topic of the event to be processed
private final String topic;
// log data to be processed
private final String data;
// the timestamp fetched from external source
private final String timestamp;
}
(3) Fiecare actor descrie modul în care reacționează la diferite mesaje. Aceasta se efectuează suprascriind metoda createReceive.
(4) Relația de copil - părinte oferă o serie de avantaje, unul dintre acestea fiind supervizarea implicită, astfel în momentul în care din varii motive, actorul copil nu reuşeste să termine sarcina, părintele poate opta pentru una dintre strategii de supervizare de mai jos:
Comandă actorului copilul să reia sarcina, păstrând starea acumulată;
Reiniţializează actorul copil, pierzând starea acumulată;
Oprește actorul copil permanent;
(5) Execuția metodei este declanşată în momentul recepţionării unui mesaj de tipul NewJournalEvent. Metoda se ocupă doar de distribuirea sarcinii către actorul copil specializat care ar trebui să o proceseze, delegarea efectivă realizându-se cu ajutorul metodei tell. Codul din actorul munictor este la fel de scurt și ușor de urmărit:
private void onNewJournalEvent(NewJournalEvent event) {
String topic = event.getTopic();
String encodedData = getEncodedData(
getValueToHash(event));
previousHash = encodedData;
// publish the event after hashing
getContext().getSystem().eventStream().publish(
new HashedJournalEvent(topic, encodedData,
event.getTimestamp()));
}
Probabil până acum ați reușit să vă faceți o idee despre elementele pe care le puteţi găsi într-o clasă a unui actor. Acestea sunt clase simple Java, care trebuie să definească modul în care procesează mesajele ce ajung în mail-boxul acestora.
Același mecanism de master-worker a fost folosit şi pentru actorul de logare. Actorul master ascultă la un mesaj de tip HashedJournalEvent și deleagă executarea taskului către actorul muncitor de logare, care scrie informațiile în fișier după ce se umple bufferul intern al acestuia (cu scopul evitării scrierii volumelor mici de date). La fel, urmând acest mod de organizare și repartizare a muncii, păstrăm ordinea informației în fișierele de log, decuplând procesul de recepționare a mesajului de scrierea acestora în fişier.
Același mecanism de master-worker a fost folosit şi pentru actorul de logare. Actorii Monitor, Config, TTS implementează ei funcţionalitatea direct. Singura diferenţă majoră între aceşti actori şi cei prezentaţi fiind faptul că ascultă la alte tipuri de mesaje (HashedJournalEvent, RawJournalEvent, LoggedToFileEvent, …) şi în metoda care defineşte comportamenul, implementarea e specifică funcţionalităţii pe care o servesc.
Am testat sistemul creat de noi pe diferite configuraţii de sistem şi folosind actorul de monitorizare am sumarizat mai jos rezultatele obţinute pentru throughputul sistemului:
Specificații sistem:
Mesaje cu lungime medie de 50 caractere:
Mesaje cu lungime medie de 500 caractere:
Mai jos, un grafic în care am încercat să reprezentăm procesul nostru de învățare şi acomodare cu Akka. Începutul a fost foarte ușor, adaugi dependința la librăria de Akka și eşti gata să creezi primii actori. Sistemul de logging a făcut procesul de urmărire a execuţiei destul de ușor, însă lucrurile au început să devină din ce în ce mai complicate în momentul când am încercat să combinăm concepte mai avansate precum: distribuirea actorilor pe mai multe noduri și crearea unor actori cu adevărat scalabili. A fost necesară studierea exhaustivă a documentației și a unor proiecte care aveau codul public pe github.
Merită să fie menționat faptul că documentația este foarte bună, atât pentru Scala cât și Java, dar totuși majoritatea exemplelor cu grad de dificultate mai mare, găsite de noi, sunt prezentate în Scala, ceea ce ar putea fi un factor prohibitiv începerii unei
aplicaţii mai complexe folosind Akka - Java.
Figura 6: Curba de învăţare Akka
Un alt punct demn de menţionat este lipsa compatibilității între versiuni. Akka 2.0 diferă destul de mult de versiunea precedentă și nu suportă multe funcționalități care existau înainte, şi a căror urmă încă există prin documentaţie.
Din punct de vedere al monitorizării, Akka se poate lăuda cu suport foarte bun deja integrat. Monitorizarea nodurilor distribuite și a actorilor e destul de simplă, la fel de ușor se poate face integrarea cu Grafana sau alte unelte din aceeași nișă.
Documentația oficială afirmă că se poate atinge o performantă de până la 50 millioane de mesaje pe secundă, per nod. Ca notă, pentru soluţia noastră concept şi utilizând mecanismul de subscriere și publicare a mesajelor, noi am reușit să obținem un rezultat de 1 milion de mesaje pe secundă.
În încheiere, dezvoltarea aplicaţiei concept a fost destul de facilă, aceasta a ajuns să includă multe funcţionalităţi pe care le va avea în varianta finală, destule cât să ne dăm seama de indicii de performanţă pe care i-am putea obţine şi a valida conceptul. Codul obţinut a fost lizibil, curat, decuplat, uşor de scris iar modelul de comunicare asincronă şi supervizarea integrată au făcut procesul cu atât mai uşor şi ne-au lăsat să ne concentrăm pe problema de rezolvat.
Fiind implicaţi în principal în aplicaţii cu arhitecturi tradiţionale ( stratificate), a necesitat un efort iniţial de adaptare la sistemul de modelare folosind actori, efort care merită luat în calcul înainte începerii oricărei aplicaţii mai serioase, folosind Akka.
Din punctul nostru de vedere, construirea unei aplicații întregi, folosind numai Akka, nu este de loc comună și poate provoca multe dificultăți pe parcurs. În schimb, considerăm oportună posibilitatea izolării şi rescrierii unor componente din cadrul unor sisteme mai mari - care au nevoie de caracteristicile prezentate în articol, utilizând Akka.
În acest articol, am atins o mică parte din ce oferă Akka, nu am povestit despre concepte interesante cum ar fi streams, persistenţa actorilor, distribuţia actorilor într-un cluster de noduri, etc…, dar sperăm că arhitecturile care pot fi obţinute modelând lumea reală prin sisteme de actori, abstracţiile de concurenţă şi comunicare oferite pentru a construi sisteme reactive moderne, sunt de ajuns să vă facă măcar să încercaţi măcar un HelloWorldActor.
"I wrote it, like most of my essays, as part of trying to understand the topic" - unul dintre cele mai citite şi citate articole ale lui Martin Fowler despre: "the new methodology": Agile
de Ovidiu Mățan
de Ovidiu Mățan
de Dan Sabadis