I pattern per la composizione dei servizi sono stati anch‘essi catalogati in sottofamiglie: pattern sulla composizione delle funzionalità, pattern sulla messaggistica del servizio, pattern sull‘implementazione della composizione, pattern sulla sicurezza delle interazioni dei servizi. In tal modo è possibile definirli più nei dettagli per scegliere al meglio quale pattern usare quando ci si trova in una delle fasi sopra indicate.
Pattern sulla composizione delle funzionalità
Questa famiglia di pattern fa da supporto alle problematiche di design nelle quali ci si può imbattere durante la composizione di servizi. Vediamo come si possono evitare errori tipici grazie al supporto dei pattern qui di seguito illustrati.
Capability Composition
In molti casi, nonostante la natura delle operazioni offerte da un servizio sia in linea col contesto funzionale del servizio stesso, la logica richiede funzionalità che stanno al di fuori del contesto. Dunque, il confine del servizio potrebbe crescere, espandersi, cambiando così il suo originale contesto e introducendo possibili sovrapposizioni funzionali e denormalizzazioni del servizio.
Per evitare che accadano queste situazioni, la soluzione richiede che la funzionalità del servizio non deve essere espansa, ne’ deve eseguire logica che risiede al di fuori del suo contesto funzionale. Il servizio sarà eletto a fare da compositore delle funzionalità che risiedono in servizi differenti in modo da portare a termine la richiesta business originale.
Pattern sulla messaggistica del servizio
Questa tipologia di pattern fornisce un supporto valido nel momento in cui occorre coordinare e processare lo scambio di dati tra i vari servizi di una composizione.
Service Messaging
Molte implementazioni di soluzioni nel distribuito si affidano a framework che supportano invocazioni remote, come quelli basati sulla tecnologia RPC.
Il problema però è che questi sistemi di comunicazione definiscono delle connessioni persistenti, per uno scambio di dati tra le unità di logica dei servizi facenti parte dei confini applicativi. Nonostante la soluzione sia efficiente, l’utilizzo di tali componenti come risorsa enterprise, acceduta da più consumatori, porta ad abbassare la soglia di utilizzo concorrente in quanto le connessioni che si stabiliscono sono strettamente accoppiate, limitando il potenziale riuso del componente come risorsa.
La soluzione proposta da questo pattern consiste nell’utilizzo della messaggistica come alternativa alle connessioni persistenti. In questo modo, i messaggi non definiscono un accoppiamento stretto tra fornitore e consumatore, essendo inviati come unità indipendenti di comunicazione.
Intermediate Routing
La composizione dei servizi può essere vista come un canale punto-punto per lo scambio dei dati tra i partecipanti della composizione. La logica di routing dei messaggi può essere insita nella logica di ciascun servizio e questo è sufficiente se si tratta di percorsi predefiniti di messaggi. Ci possono essere però dei fattori imprevisti che possono portare a dei fallimenti, come per esempio:
- l’invio di un messaggio viene effettuato quando il servizio destinatario è temporaneamente non disponibile;
- la logica di routing per la gestione delle eccezioni contiene delle condizioni di “catch all” non sufficienti per la giusta determinazione del path, per cui la destinazione del messaggio risultante non è corretta (figura 1);
- la logica del servizio di destinazione non è capace di determinare il path da cui proviene il messaggio e dunque non riesce a instradarlo verso il consumatore.
Figura 1 – Esempio di logica di routing embedded.
La soluzione proposta consiste nell’astrarre la logica di instradamento di modo che sia parte dell’architettura generale. La realizzazione di tale logica di instradamento molto spesso si basa su agenti event-driven che in maniera trasparente intercettano i messaggi e determinano dinamicamente il loro path.
Per esempio, un messaggio prima di arrivare a destinazione può essere intercettato da agenti di tipo router come il rules-based che identifica il servizio target sulla base di regole business che esso stesso recupera da un repository e interpreta. A cascata ci può essere un secondo agente come il load balancing router che controlla le statistiche di utilizzo per quel servizio (di destinazione del messaggio) e decide a quale istanza del servizio inviare il messaggio.
State Messaging
Spesso ai servizi è richiesto di mantenere le informazioni di stato in memoria durante lo scambio di messaggi e ciò può compromettere la loro scalabilità. Per esempio, nella conversazione tra il servizio A e B, il servizio A che agisce da consumatore invia la richiesta di Messaggio X al servizio B. Quest’ultimo processa la richiesta e crea su se stesso la struttura dati per mantenere lo stato associato alla richiesta del Messaggio X e ne effettua l’aggiornamento dopo che ne ha terminato l’elaborazione.
Se il servizio A effettua una richiesta successiva, il servizio B sarà pronto ancora una volta a processarla con un conseguente aumento dei dati di stato in seguito all’aggiornamento degli stessi. Dal momento che questa soluzione compromette la scalabilità e la riusabilità del servizio che gestisce lo stato, l’alternativa proposta consiste nel mantenere lo stato nel messaggio, di modo che durante un’interazione il servizio possa recuperare l’ultimo stato dal successivo messaggio di input.
Riprendendo l’esempio precedente tra il servizio A e B, dopo che il servizio B ha creato la struttura per i dati di stato, questa volta aggiungerà lo stato al Messaggio X di risposta restituito ad A piuttosto che mantenerlo su se stesso. Il servizio A processa la risposta e quando genererà la seconda richiesta di Messaggio Y, questa conterrà i dati di stato aggiornati dal servizio A che saranno ricevuti e processati dal servizio B.
Service Callback
Ci sono situazioni in cui alle richieste di un consumatore occorre rispondere con una serie di messaggi e la comunicazione a scambi di messaggi non risulta appropriata. Oppure ci sono casi in cui il consumatore, prima di ricevere la risposta, ha dei lunghi periodi di attesa con un conseguente consumo di memoria e di risorse allocate e allo stesso tempo locked.
La soluzione proposta consiste nel definire i servizi di modo che i consumatori forniscano delle informazioni di correlazione e un indirizzo di callback, al quale possono essere contattati dal servizio dopo che quest’ultimo ha portato a termine la richiesta. Le informazioni di correlazione sono utili al consumatore del servizio per associare la risposta al task originario. In questo modo, durante i tempi in cui il servizio è impegnato a fornire la risposta, il richiedente non è più bloccato nell’attesa e le risorse possono essere rilasciate.
Service Instance Routing
Ci sono casi in cui un consumatore invia più messaggi che devono essere processati all’interno dello stesso contesto di esecuzione. Per esempio, il servizio B quando riceve la richiesta da A, creerà un’istanza X del servizio B e restituirà anche un identificatore per l’istanza, come parte del messaggio di risposta.
Quando il servizio consumatore A estrae l’identificatore dell’istanza, userà quest’ultima per effettuare le successive richieste al servizio B, inglobandola all’interno del corpo del messaggio.Questo tipo di comunicazione crea però un forte accoppiamento tra il servizio e qualunque altro tipo di consumatore.
La soluzione proposta consiste nell’estendere l’infrastruttura in modo da consentire all’identificatore delle istanze del servizio di essere posizionato come un riferimento per la destinazione. In questo modo, l’ID dell’istanza non sarà più un accoppiamento tra consumatore e servizio poiche’ il consumatore non conterrà più l’istanza diretta del servizio, ma il riferimento del servizio viaggerà nell’header del messaggio.
Asynchronous Queuing
Una comunicazione sincrona richiede una risposta immediata a ciascuna richiesta e forza uno scambio dati two-way per ciascuna interazione. Per cui, sia il consumatore che il servizio devono essere entrambi disponibili al momento della comunicazione e pronti per completare lo scambio dati. Questo comporta problemi di affidabilità quando per esempio il servizio non riesce a garantire la sua disponibilità a ricevere la richiesta, oppure è il consumatore a essere assente per la ricezione della risposta alla sua richiesta. Ovviamente i lunghi periodi di attesa introducono problemi sull’allocazione delle risorse e della memoria.
La soluzione proposta sta nell’utilizzo di una coda come un buffer di intermediazione che riceve i messaggi di richiesta e ne effettua il forwarding al servizio destinatario. Se il servizio target infatti non è disponibile, la coda agirà come uno storage temporaneo dei messaggi e ne tenterà periodicamente l’invio:
Figura 2 – Asynchronous Queuing.
In maniera analoga, se vi è una risposta da recapitare al mittente della richiesta che al momento non è disponibile, questa verrà posta temporaneamente nella coda e inoltrata non appena il consumatore risulta disponibile.
Reliable Messaging
Quando i servizi vengono realizzati per comunicare mediante scambio di messaggi, c’è una naturale perdita di qualità del servizio dovuta alla natura stateless dei protocolli di messaggistica utilizzati come l’HTTP. Infatti, dal momento che lo scambio dei messaggi non è persistente, non si ha la certezza o meno della loro avvenuta consegna, o comunque, la piattaforma non e’ in grado di fornire dei feedback.
La soluzione proposta dal pattern consiste nell’utilizzo di un framework che tenga traccia dei messaggi e ne renda temporaneamente persistente la trasmissione, comunicando inoltre i casi di successo o di fallimento d’invio del messaggio ai rispettivi mittenti. Un framework di questo tipo è capace di:
- garantire la consegna dei messaggi nelle condizioni di failure, grazie al supporto di uno store in cui vengono depositati i messaggi in maniera persistente;
- stabilire l’avvenuta consegna o meno per un singolo o una sequenza di messaggi.
Il framework prevede oltre allo storing dei messaggi, un insieme di agenti che gestiscono la conferma o il fallimento della consegna mediante notifiche positive (ACK) o negative (NACK). Questo tipo di messaggi può essere spedito per ciascun messaggio o si può decidere di inviarne solo uno alla fine della sequenza di messaggi scambiati tra server e consumatore.
Event-driven Messaging
In un ambiente di messaggistica, i consumatori del servizio possono scegliere tra le modalità one-way o request-response per comunicare col servizio, ma questi pattern prevedono che ogni richiesta venga sempre originata dal consumatore. Ci sono casi in cui si ha la necessità di conoscere determinati eventi originati dal servizio, di interesse ai vari consumatori. In tali circostanze, i consumatori dovrebbero continuamente fare polling del servizio per verificare se l’evento d’interesse si è verificato.
È evidente che tali pattern di comunicazione non sono sufficienti in quanto condurrebbero a innumerevoli ed inutili invocazioni del servizio, con introduzione di possibili ritardi anche sull’avvenuta conoscenza delle informazioni relative all’evento da parte del consumatore, in quanto i polling al servizio vengono effettuati ad istanti predefiniti.
La soluzione proposta è quella di un programma di gestione degli eventi, che permette ai consumatori di sottoscriversi come subscriber agli eventi associati al servizio, che in questo caso assume il ruolo di publisher.
Dal momento che il servizio può mettere a disposizione vari tipi di eventi, i consumatori possono scegliere quelli ai quali vogliono sottoscriversi per ricevere le notifiche. Per cui, quando l’evento per il quale un consumatore si è sottoscritto si verifica, sarà il servizio stesso ad inviare in maniera del tutto automatica i dettagli delle’evento, con la modalità broadcast che notifica a tutti gli iscritti per quel determinato evento.
Pattern d’implementazione della composizione
Sulla base dei requisiti di una composizione, alcune scelte di design possono risultare congeniali per la stessa architettura. In questo paragrafo verranno introdotte alcune soluzioni a livello implementativo della composizione, che si riversano sull’architettura precedentemente definita.
Composition Autonomy
I servizi di una composizione sono idealmente autonomi e possono essere riusati e condivisi da altre composizioni. Il risultato di una composizione è comunque una perdita di autonomia dovuta al fatto che molto spesso i servizi costituenti devono invocare della logica che risiede al di fuori del contesto di esecuzione della composizione.
Quando tutti i singoli servizi forniscono un elevato grado di autonomia, anche tutta la composizione ha lo stesso livello di autonomia. Ma quando uno o più partecipanti della composizione risultano poco autonomi, tutta la composizione ne risente.
La soluzione proposta dal pattern, per far sì che la composizione sia autonoma, consiste nell’effettuare il deploy di tutti i servizi di una composizione in un unico ambiente, di modo che nessun servizio partecipante alla composizione sia condiviso in maniera specifica e non sia coinvolto in comunicazioni remote, di modo da aumentare l’autonomia della composizione come unità singola di servizio.
Atomic Service Transaction
Ci sono situazioni in cui si verificano condizioni di errore e la logica di gestione delle eccezioni fornita dai servizi non risulta sufficiente per portare a termine il processo business. Per esempio, ci possono essere tre servizi A, B e C coinvolti per realizzare un processo business: nel corso dell’elaborazione, i primi due riescono a completare gli aggiornamenti dei rispettivi database, mentre il terzo servizio fallisce proprio nel suo tentativo di aggiornamento.
In base alle regole del processo business, quest’ultimo andrà a buon fine se e solo se tutti e tre gli aggiornamenti sono portati a termine correttamente. Per cui, dal momento che fallisce l’aggiornamento del terzo database, ciò va a compromettere la qualità dei dati nei primi due.
La soluzione proposta consiste nel richiedere che tutti i servizi di una particolare composizione si registrino come parte di una transazione prima di completare i loro cambiamenti. In questo modo, i servizi comunicano al sistema di transazioni il loro stato, il quale interrogherà i servizi per verificare che le loro funzioni siano state portate a termine correttamente. Se almeno uno di essi risponde negativamente, viene effettuato il comando di rollback, il che riporta i servizi allo stato precedente ad ogni cambiamento fatto sino a quel momento. Se tutti i servizi invece rispondono positivamente, allora verrà richiesto ad essi di effettuare il commit dei loro cambiamenti.
Riprendendo l’esempio, se i servizi A e B completano positivamente il loro task, iniziano una transazione locale che permette loro di salvare temporaneamente lo stato corrente del database prima di rendere effettivi i loro cambiamenti. Se il servizio C adesso fallisce nel suo tentativo di aggiornamento del database, i servizi A e B ripristineranno lo stato iniziale che avevano prima degli aggiornamenti.
Pattern sulla sicurezza delle interazioni di servizi
I servizi di una composizione possono essere soggetti a vari scenari di utilizzo che possono introdurre rischi di sicurezza. Proprio per questo motivo verranno adesso indicati i pattern a supporto della sicurezza tra le interazioni dei servizi facenti parte di una composizione.
Data Confidentiality
I dati dei messaggi rischiano di viaggiare in reti non sicure e l’approccio comunemente usato è quello di proteggerli a livello di trasporto criptando la connessione tra il consumatore e il servizio, mediante tecnologie come l’SSL e la TLS.
La sicurezza a livello di trasporto è realizzata per scambi dati punto-punto dal momento che i servizi e i consumatori coinvolti sono predefiniti. Il problema si pone nel momento in cui i messaggi sono scambiati all’interno di una composizione o su path in cui sono previsti intermediari, per i quali la sicurezza a livello di trasporto non risulta sufficiente:
- servizi o agenti potrebbero riuscire a ottenere l’accesso ai dati dei messaggi poiche’ mentre sono in possesso di tali dati, questi non risultano criptati;
- i dati sensibili potrebbero risultare vulnerabili mentre sono resi temporaneamente persistenti su una coda, un database, un file, etc.
La soluzione proposta è quella di applicare delle tecnologie di criptazione che proteggano i dati a livello di messaggio e non solo a livello di trasporto. In questo modo, la sicurezza del dato è embedded col dato stesso, rimarrà col messaggio e solo i destinatari autorizzati saranno in grado di accedere al contenuto.
Questo pattern è tipicamente applicato ai web services mediante la tecnologia XML-Encription, referenziata dallo standard WS-Security. L’XML-Encription converte i messaggi in chiaro, plaintext, in dati criptati noti come ciphertext. I dati in chiaro vengono criptati con un algoritmo e una chiave crittografata e il ciphertext risultante sarà poi riconvertito in chiaro dal destinatario che possiede la chiave per decifrare il messaggio.
Ovviamente ci sono varie tecniche di crittografia dei messaggi che non verranno qui illustrate.
Data Origin Authentication
Un messaggio inviato da un consumatore al servizio può essere processato da diversi intermediari col rischio che un attaccante potrebbe modificare tali messaggi in transito in modo da manipolare il comportamento del servizio destinatario. Le manipolazioni possono essere di varia natura: vanno dalla modifica del contenuto dei dati del messaggio alla sostituzione delle credenziali che identificano l’origine del messaggio di modo da consentire ad un attaccante di impersonarsi come il consumatore originale.
La soluzione proposta è quella della firma digitale che consente al destinatario del messaggio di verificare che il messaggio non sia stato manomesso mentre era in transito e che come tale arriva da un mittente di fiducia.
Authentication
Ci sono servizi che trattano dati sensibili o privati e come tali non possono essere resi accessibili a tutti i potenziali consumatori. Per cui, tali servizi richiedono un meccanismo di autenticazione al richiedente del servizio stesso, di modo che questo fornisca le credenziali per l’autenticazione.
Il meccanismo di autenticazione può essere gestito in maniera diretta dal servizio oppure essere delegato; nel primo caso si parlerà di Direct Authentication mentre nel secondo si avrà una Brokered Authentication.
Nel caso di DirectAuthentication, il servizio ha la responsabilità diretta dell’autenticazione del consumatore che accede al servizio, e sarà il servizio stesso a verificarne la validità delle credenziali interrogando un identity store in cui sono memorizzate tutte le credenziali. In base alla validità o meno delle credenziali, il consumatore avrà accesso o meno alle funzionalità del servizio. Nal caso di BrokeredAuthentication, l’autenticazione del consumatore viene gestita da un Authentication Broker apposito.
La necessità di tale autenticazione si ha quando per esempio un consumatore deve accedere a più di un servizio e in tal caso dovrebbe richiedere l’autenticazione a ciascuno di essi, il che porterebbe ad una replicazione di meccanismi identici su tutti i servizi. Con l’Authentication Broker invece si ha la possibilità di autenticare il consumatore su più servizi, poiche’ il broker fornirà un apposito token per accedere a tutti i servizi cui il consumatore necessita di accedere. Per la realizzazione dell’authentication broker ci sono varie tecnologie a supporto come:
- L’infrastruttura X.509 che utilizza una Certificate Authority per attestare i certificati X.509;
- Il protocollo Kerberos che utilizza un servizio di autenticazione e un servizio di Ticket Granting per emettere un ticket Kerberos;
- La specifica WS-Trust che descrive un protocollo usato dal servizio Security Token per emettere i token come SAML e SecPal.
Conclusioni
Con questo articolo si chiude la serie dei pattern SOA, la cui illustrazione si è limitata semplicemente a mettere in luce solo alcuni dei problemi più ricorrenti in cui ci si può imbattere nella definizione di un’architettura SOA-oriented. La classificazione dei pattern nelle tre famiglie che abbiamo visto in questi tre ultimi articoli ha permesso di focalizzarci con più attenzione sulle situazioni in cui possibili problemi possono verificarsi. Ovviamente ogni pattern può essere ulteriormente approfondito seguendo i riferimenti bibliografici, in cui sono approfonditi contesti applicativi dei singoli pattern, situazioni di matching tra i vari pattern e anche situazioni di svantaggio della loro applicazione in determinati contesti. Desidero inoltre ringraziare i lettori che si sono interessati a questo argomento: siete sempre voi il sostegno di chi scrive.
Riferimenti bibliografici
[1] Thomas Erl, “SOA Design Patterns”, Prentice Hall. Un lavoro fondamentale sulle tematiche trattate, che si consiglia per ulteriori approfondimenti.
[2] SOA Patterns. A community site for SOA design patterns. Un sito di riferimento sulla materia.