Progetto Arcipelago

Versione del 19 novembre 2002
autore: Massimo Coletti

copyright Massimo Coletti 2002

Introduzione

Questo lavoro parte da una constatazione: i sistemi applicativi, integrati sopra un unico, omnicomprensivo, database, sono uno spreco di risorse e non sono manutenibili.

E' un'affermazione forte, volutamente provocatoria, che mi nasce da una ultraventennale esperienza nel settore IT, dalla conoscenza approfondita di diversi sistemi gestionali ed ERP (fra cui l'eccellente SAP), e dall'esperienza diretta nella progettazione di applicazioni gestionali.
Le considerazioni alla base di questo lavoro sono riassunte in alcune slide relative ad una presentazione fatta su questi temi.

Obbiettivi del progetto

Questo progetto si pone come obiettivo il disegno di un'architettura applicativa concepita per garantire flessibilità, modularità, sicurezza, resilienza, per applicazioni disegnate per processare transazioni, ed in particolare per applicazioni di front e backoffice Titoli in ambito bancario.

Il concetto di partenza è quello che, per questo tipo di sistemi, l'uso di un ampio database integrato costituisca (pur non dimenticando gli specifici vantaggi) un vincolo di peso crescente al crescere della complessità dell'applicazione. Il modello ideato prevede quindi che l'applicazione sia composta di piccoli mattoni, assolutamente autonomi, che dialogano l'uno con l'altro secondo pipelines predefinite. Ogni componente conosce solo quelli adiacenti, e dialoga con loro non mediante la condivisione di un database o un modello ad oggetti, ma mediante messaggi, il cui formato è privato nell'ambito dell'interfaccia fra due singoli componenti adiacenti.

Come funziona?

Il progetto prevede di realizzare un'applicazione con una collezione di moduli indipendenti.

I moduli (Componenti) comunicano fra loro mediante messaggi, secondo protocolli e modalità di interfaccia definite bilateralmente fra modulo e modulo.

L'architettura prevede un Controller, che attiva i moduli, ed un servizio di routing, che fornisce ad ogni modulo le informazioni sul destinatario successivo del messaggio ricevuto.
In pratica, il Controller è la "testa" dell'applicazione, ma fa ben poco, oltre ad avviare i Componenti. I Componenti si distinguono fra componenti interni (che dialogano solo con altri componenti dell'applicazione) ed esterni (che ricevono input o richieste da applicazioni esterne o dall'utente).
Perché un componente ha bisogno di un servizio di routing? In questo modo riesco a rendere più flessibile l'applicazione: infatti lo "step" successivo nel processing di un messaggio non è hard-coded nella logica del componente, ma è configurabile dall'esterno. Il servizio di routing è un po' un DNS applicativo. Se voglio personalizzare o estendere la mia applicazione, questo comporterà l'introduzione di nuovi componenti e l'estensione della pipeline di elaborazione di un messaggio. Intervenendo sulle tabelle di routing, posso realizzare questa estensione senza dover personalizzare i singoli moduli preesistenti interessati dal cambiamento.

Lo schema seguente mostra un esempio di sequenza di attivazione delle componenti fin qui illustrate:

l'attore, ovvero l'agente esterno che stimola l'applicazione, chiama direttamente un componente di interfaccia (esterno). Questo componente è stato attivato dal Controller, che gli ha anche fornito indicazioni sul router da usare (infatti possono esistere diversi Router, uno per ogni modulo dell'applicazione) .

Il componente crea il messaggio (ovvero istanzia una Request) e chiede al Router dove mandarla, dopodichè la sottopone al servizio di messaging, ed aspetta (richiesta sincrona) la risposta, oppure si pone in attesa del nuovo stimolo.

La Request transiterà attraverso una serie di componenti, fino ad arrivare a quello finale, che storicizza la transazione sul suo database, e ritorna l'esito positivo. L'esito transiterà indietro di modulo in modulo fino a ritornare all'originante.
La scelta di un path di ritorno che ripercorra esattamente quello dell'andata è coerente con la filosofia di realizzare soltanto interfacce bidirezionali: il componente che riceve un messaggio, ne deve rispondere direttamente al mittente. In questo modo si mantiene il forte disaccoppiamento fra moduli non contigui, ed ogni modulo può implementare le politiche di ripartenza e riprocessamento opportune per la sua funzione, in quanto può tenere una contabilità precisa dei messaggi ricevuti, inoltrati, delle risposte ricevute e di quelle fornite.

Aspetti di sicurezza

In un contesto in cui si parla sempre di più di sicurezza integrata alle applicazioni, l'infrastruttura proposta prevede uno strato applicativo che gestisce le autorizzazioni ed i privilegi. L'obiettivo è di svincolare i componenti, che sono pensati come module "leggeri" dai compiti legati alla sicurezza applicativa, permettendo però al tempo stesso di basare il complesso dell'applicazione su una foundation che implementi meccanismi anche sofisticati di sicurezza.

Il concetto prevede che l'utente effettui il logon prima di iniziare ad operare sull'applicazione, ed il sistema di sicurezza gli assegni un certificato di sessione. Il componente esterno che genererà la Request otterrà questo certificato, che, da quel momento, accompagnerà la richiesta.
Ogni componente, quando riceve la richiesta, può inviare al sistema di controllo delle autorizzazioni una query per verificare se l'utente (identificato dal certificato di sessione) possiede i privilegi per effettuare l'operazione che il modulo deve compiere. Il progettista dell'applicazione può decidere anche che quel modulo non ha bisogno di meccanismi di controllo, e non effettuare questa chiamata. L'ok ritornato dal sistema di controllo è a sua volta rappresentato da un certificato, che verrà aggiunto alla richiesta. E' così tecnicamente possibile archiviare insieme alla transazione anche l'insieme dei certificati man mano ricevuti, oppure effettuare dei riscontri fra i log del sistema di autorizzazioni e quanto memorizzato con la transazione, per assicurare che non ci siano state intrusioni o falsificazioni nel processo di autorizzazione.
Il modulo può basare la sua verifica di autorizzazioni anche su un hash della Request stessa, offrendo così la possibilità al modulo finale (anche se l'architettura non prevede necessariamente un modulo finale) di verificare la congruenza della transazione ricevuta con le autorizzazioni richieste dai componenti che l'hanno elaborata.

Anche il livello di sicurezza fra moduli può essere sofisticato in funzione dei livelli di protezione che si vogliono ottenere: ad esempio è possibile che un componente firmi digitalmente la richiesta che invia, eventualmente con un timestamp. Il componente ricevente può quindi avere un buon livello di confidenza sull'originalità della richiesta ricevuta.

Basandosi sui servizi dell'infrastruttura di messaging usata (che ricordo può differire ad ogni passo di elaborazione), è possibile costruire applicazioni con una intrinseca resistenza a possibili backdoor, abusi o sostituzioni di codice.

Routing

Una delle idee base è quella di un servizio di routing esterno ai componenti. In pratica ogni componente, quando riceve una richiesta la elabora, effettua gli eventuali aggiornamenti necessari, la converte eventualmente nel suo formato di output, e poi chiede al router l'indirizzo del componente successivo a cui elaborarlo.

Il router avrà una tabella in cui alla coppia:

Sincronismo

Disegnando un'architettura basata su componenti che probabilmente operano su thread separati, o trasparentemente su server separati, ci si deve porre il problema di mantenere in sincronia le elaborazioni di questi messaggi, gestire problemi di ripartenza, o allertare il sistema quando un componente cessa di rispondere.

Anche in quest'area, le opportunità dipendono dal grado di affidabilità richiesto.

Ogni componente è responsabile della generazione dei messaggi che invia. Ricordiamoci che ogni Request ha un id univoco (il dominio di univocità è dato dal Controller).
Ogni componente può inviare messaggi diversi, in risposta a diverse Request che riceve. Il componente avrà quindi la responsabilità di numerare in modo univoco i messaggi che invia, in modo da essere in grado di ricollegare le risposte ricevute ai messaggi inviati. La numerazione è valida solo per lo scambio di informazioni fra se ed i suoi destinatari diretti.

La catena dei messaggi associati ad una Request entra nel sistema, e percorre tutta la sua route, finchè un modulo non "decide" che l'elaborazione è finita (tipicamente glielo dovrebbe dire il router). A quel punto, se non ha trovato errori, risponde all'ultimo componente che gli aveva inviato il messaggio con una conferma, ed il suo ruolo è finito. La catena delle conferme torna indietro, fino al punto di entrata, ed a questo punto la transazione è conclusa.

In questo processo, possono verificarsi delle anomalie:

Il mittente di un messaggio non ha teoricamente possibilità di riconoscere i due casi. Il secondo caso si può verificare se, in un punto qualunque della catena elaborativa a valle, si verifica un'anomalia. Per introdurre maggior controllo, il destinatario di un messaggio può rispondere al mittente con un acknowledge, anche se a sua volta non ha ricevuto la risposta al messagigo inoltrato al passo successivo. Questo acknowledge non è una conferma della transazione, ma solo del ricevimento del messaggio: con questa informazione, il mittente sa che il destinatario sta "funzionando".

Il componente che rileva un'anomalia (ack o conferma dopo il timeout) può informare il router (o il controller) dell'anomalia, e ritornare al suo mittente un messaggio di errore per transazione non completata; inoltrerà anche al destinatario che non ha risposto un messagigo di annullamento richiesta: si assume che il sistema di messaging sia garantito (o che altri meccanismo di ritrasmissione siano in atto) per cui, quando il destinatario riprenderà la sua funzione, potrà propagare l'annullamento a tutta la catena.

I controlli indicati non prendono in considerazione il fatto che il link di provenienza sia rotto, o altri tipi di incongruenza. Un ulteriore livello di controllo può essere introdotto con meccanismi di heartbeat, per cui quando un componente non riceve nessun input dopo un determinato timeout, può inoltrare un alert. Nell'ottica di controllo a catena sopra descritta questo non è strettamente necessario, ma potrebbe rendere il sistema di controllo consapevole di un'anomalia prima che vi impatti sopra una transazione.

Il router che riceve un segnale di anomalia può, se ne ha le informazioni, alterare dinamicamente le sue tabelle di routing per dirottare le successive richieste su un componente di failover.

Le richieste al router potrebbero anche contenere informazioni sui ritardi presenti in rete: ad esempio il numero dei messaggi del tipo corrente che aspettano ancora ack o risposta, il numero dei messaggi totali in attesa, il timestamp dell'ultimo messaggio inviato (o del primo in attesa). Con questi dati il router potrebbe costruire una mappa del carico di lavoro ed attivare anche meccanismi di load balancing (assumendo che i singoli componenti che lavorano in parallelo si occupino della sincronizzazione dei database locali).

un caso pratico...