Remote Procedure Call

Lezione del 2019-04-18 gio.

Remote Procedure Call (RPC) consiste nell’eseguire una chiamata su un sistema diverso da quello del chiamante.

Modello a strati

Considerando che i due sistemi predetti possono essere fisicamente separati, non possono comunicare tramite una memoria condivisa. Dovranno comunicare tramite messaggi.

Di conseguenza è necessario analizzare diversi aspetti legati al movimento dell’informazione. Per questo è utile considerare un modello a strati analogo al modello di riferimento dell’OSI, riportato qui di seguito.

_images/13_modello_osi.svg

Si ricordi che il modello di riferimento OSI non è un protocollo, è la definizione di una serire di servizi, resi disponibili da ogni strato rispetto lo strato superiore. Come sono implementati questi servizi non è d’interesse.

Lo strato physical offre il servizio di trasferimento dei dati.

Il data link permette a più nodi di condividere lo stesso mezzo fisico, e organizza le informazioni in frame.

Il network permette la costruzione di percorsi che fanno comunicare nodi che non sono connessi direttamente tra loro.

Il transport usa i percorsi predetti per costruire canali di comunicazione, ad es. anche affidabili.

Segue poi la sessione (session), il formato dei dati (presentation), ed infine il livello applicativo.

L’utilizzo del modello a strati predetto porta alla strutturazione dei dati da trasmettere come nel seguente schema.

_images/14_schema_dati_osi.svg

Il software di middleware si pone tipicamente tra il livello application ed il livello transport, come illustrato qui di seguito.

_images/15_posizione_middleware.svg

Il middleware usualmente mette a disposizione dell’applicazione una serie di servizi generali:

  • autenticazione, autorizzazione,

  • protocolli di commit distribuiti,

  • fault tolerance,

  • servizi di comunicazione ad alto livello (trasparenza).

Tipi di comunicazioni

I servizi di comunicazione di alto livello proposti dai middleware possono avere caratteristiche diverse:

  • essere persistenti, in tal caso il middleware immagazzina il messaggio finché il destinatario non lo riceve (es.: email);

  • essere transienti, la memorizzazione del messaggio avviene solo se mittente e destinatario sono in esecuzione (ad es. a livelloo di tasporto).

Indipendentemente dal fatto che siano persistenti o transienti, la comunicazione può anche essere:

  • asincrona;

  • sincrona.

Le chiamate sincrone sono le più semplici da utilizzare, ma sono bloccanti sino al momento in cui si riceve la risposta del servizio richiesto.

Le chiamate asincrone, in cui non si attende l’arrivo della risposta per proseguire l’attività, sono più complesse. Si consideri il fatto che si possono differenziare in relazione al momento in cui si ritiene di avere effettuata la consegna della richiesta.

Posso ritenere di avere consegnato la richiesta se l’ha presa in carico il middleware locale al mittente (synchronize at request submission). Oppure posso ritenere di avere consegnato la richiesta quando la prende in carico il middleware del destinatario (synchronize at request delivery). Addirittura, potrei decidere di attendere che il server abbia cominciato ad elaborare la richiesta.

Si noti che quelli esposti sono punti di sincronismo, cioè, il richiedente aspetta finchè la richiesta non è presa in carico in uno dei modi indicati. Dopo di che riprende l’elaborazione, prima dell’arrivo della risposta del destinatario.

Ad esempio, la Message Passing Interface (MPI) permette di indicare quale tipo di sincronismo si vuole.

IBM WebSphere è un esempio di middleware persistente. Con questo software un client (richiedente) può fare una richiesta senza che il server (destinatario) stia girando. WebSphere memorizza la richiesta permanentemente. Quando il server prende vita, WebSphere gli consegna il messaggio e attende la risposta, anche se il client non è online. La risposta viene memorizzata, e quando il client va online WebSphere consegna la risposta ricevuta dal server.

Questi oggetti vengono chiamati solitamente broker. Ovvero mediatori tra client e server. Possono essere centralizzati, o distribuiti. Un esempio di middleware centralizzato transiente è Oracle Java Message Service (JMS). Questo permette di costruire canali pub/sub (analogamente a quanto fa MQTT) e di farci messaging 1.

Analisi di RPC

Uno dei servizi messi a disposizione dai middleware di solito è RPC.

RPC è il precursore di RMI. Sono chiamate a procedure remote. Disaccoppiano la logica di chi sviluppa il servizio remoto da quella di chi ne richiede l’esecuzione. Il richiedente deve conoscere solo la sintassi di chiamata: nome della procedura, nomi e tipi dei parametri di input e di output. Dopo di che, come 2 e dove sia implementata la procedura non è di alcun interesse per il chiamante.

Lezione del 2019-05-02 gio.

Per capire i concetti di base in una chiamata a procedura remota, è necessario conoscere il funzionamento di una chiamata a procedura convenzionale, ovvero presente localmente, nello stesso processo del chiamante.

In questo caso la procedura chiamata viene estratta da una libreria, e inserita nell’ambito del codice chiamante, gestendo i dati di chiamata come mostrato nel seguente diagramma:

_images/16_chiamata_procedura_convenzionale.svg

Come si vede, la chiamata carica nello stack:

  • i parametri di chiamata,

  • l’indirizzo di rientro del chiamante,

  • le variabili locali del chiamato.

Dopo di che il program counter viene modificato con l’indirizzo del chiamato, avviando la relativa routine.

Un aspetto da considerare è il modo in cui sono caricati i parametri di chiamata. Esistono tre possibili modalità:

  • copy-by-value, ovvero i valori dei dati vengono copiati dal chiamante allo stack; il chiamato li può utilizzare/modificare, ma eventuali modifiche non hanno effetto nello spazio di memoria del chiamate;

  • copy-by-reference, in questo caso in stack si copia l’indirizzo del dato presente nella memoria del chiamante; in tal modo il chiamato può modificare il dato passato, e la modifica viene osservata dal chiamante;

  • call-by-copy/restore, usato raramente: il valore del dato viene copiato in stack, dove può essere modificato dal chiamato; al rientro verso il chiamante, il valore del dato viene copiato dallo stack alla posizione originaria nello spazio di memoria del chiamante 3

Ora, si consideri un parametro passato copy-by-value, ad esempio un intero. Se si è in una macchina Intel, questo è codificato in RAM come little endian. Se si è in una macchina SUN (processore SPARC), lo stesso parametro sarebbe codificato big endian. Questo aspetto lo dovremo riconsiderare tra poco.

Passiamo alla chiamata RPC, il cui scopo consiste nel rendere trasparente al chiamante il fatto che la procedura richiesta è allocata in altro processo, verosimilmente in un’altra macchina, collegata tramite rete dati.

Quindi il chiamante, si comporterà analogamente a quanto descritto per la chiamata a procedura locale, caricando lo stack come illustrato. Solo che invece di partire direttamente la routine richiesta partirà uno stub che ha il compito di passare alla procedura remota i dati che le sono necessari.

A questo fine, nella macchina remota vi sarà un altro stub incaricato di collezionare i dati inviati e passarli alla procedura remota (che è locale a questo stub ricevente).

Nel fare questo, gli stub in questione si occuperanno sia del trasferimento fisico dei dati, che delle eventuali conversioni necessarie affinché i dati trasmessi abbiano senso per il chiamato 4. Il caso plateale è quello del little/big endian precedentemente illustrato. Ma si pensi anche a differenti linguaggi, ad es. un chiamante sviluppato in C++ e un chiamato sviluppato in Java. Oppure a diversi tipi di codifica: ANSI rispetto EBCDIC, …

Quindi la chiamata di una RPC dal client prevede che:

  • il programma client prepari i dati della chiamata e avvii lo stub su client;

  • lo stub su client modifica opportunamente i dati per la trasmissione 5, e chiama il sistema operativo del client per effettuare la trasmissione;

  • il sistema operativo del client effettua la trasmissione dei dati;

  • il sistema operativo remoto riceve i dati e li passa allo stub del server;

  • lo stub del server converte i dati per la procedura su server e glieli passa;

  • infine viene avviata la procedura sul server.

Il ritorno della RPC dal server al client avrà una procedura analoga, anche se inversa:

  • la procedura su server prepara la propria risposta, e la passa allo stub su server;

  • lo stub su server modifica opportunamente i dati per la trasmissione, e chiama il sistema operativo del server per effettuare la trasmissione;

  • il sistema operativo del server effettua la trasmissione dei dati;

  • il sistema operativo del client riceve i dati e li passa allo stub del client;

  • lo stub del client converte i dati per il programma chiamante sul client e glieli passa;

  • infine viene (ri)avviato il programma chiamante sul client.

I problema della gestione del passaggio dei dati è vasto. Oltre le casistiche predette, si consideri la necessità di poter gestire il passaggio di dati copy-by-reference, in cui i dati non sono atomici. Ad esempio vettori. Oppure oggetti. Il copy-by-reference ha senso solo nello spazio dei dati del processo. Quando ci si sposta da un processo ad un’altro, gli spazi degli indirizzi tipicamente non coincidono. Ne consegue che è necessario trasferire una copia dell’oggetto (o del dato complesso: vettore, struttura, …), con le relative complessità (oggetto nidificato, oggetto con rappresentazione diversa tra linguaggio nel client e linguaggio nel server, …) 6.

Da quanto detto, si capisce che è necessario un protocollo di comunicazione per affrontare questi problemi, visto che in generale sarà necessario cambiare la struttura del dato da trasmettere. Ad esempio, per trasferire un array, bisognerà comunicare la sua lunghezza e il tipo degli elementi. Dove sono queste informazioni? In testa al vettore? Quale viene prima? Il tipo degli elementi come lo esprimo? … Questo è, appunto, un protocollo di comunicazione.

RPC affronta questi problemi, ad esempio decidendo di usare il formato di rete (ovvero big endian) per la trasmissione degli interi. Invece per i float con il relativo standard IEEE.

Una considerazione a parte meritano i caratteri, che sono standardizzabili più facilmente. Questo è il motivo per cui JSON utilizza esclusivamente la codifica a caratteri per trasmettere i dati tra i diversi servizi. Ciò da un lato permette una gestione semplificata, ad esempio MQTT usa JSON. Ma introduce un notevole overhead quando si devono trasmettere numeri. Per questo motivo middleware che trasmettono solo numeri evitano JSON. Caso tipico: i middleware per IoT.

Attenzione al fatto che in RPC non si definisce la logica della procedura chiamata, ma solo la sua interfaccia di chiamata. Ovvero quali sono i parametri di chiamata e il loro tipo, nonché i parametri di ritorno e il relativo tipo.

Le interfacce possono essere definite tramite appositi linguaggi formali, detti interface definition language (IDL). Una interfaccia definita via IDL, è poi compilata nello stub del client e quello del server. Infatti lo stub deve costruire i pacchetti che viaggiano tra client e server; non deve costruire la logica applicativa.

Un esempio di middleware RPC è Corba, che interfaccia diversi sistemi/linguaggi fornendo non solo i servizi RPC, ma anche servizi transazionali, autenticazione, sicurezza, …

D’altro canto l’uso di questi middleware sta perdendo terreno nei confronti delle architetture basate su web server.

Rimarchiamo nuovamente il fatto che RPC può essere sia sincrono che asincrono. Usualmente l’interazione è sincrona. Ma è possibile trovare esempi di interazione asincrona; sopratutto nei casi in cui il servizio chiamato non ritorna valori al chiamante. In tal caso per il chiamante è banale gestire una chiamata asincrona: se non vi è ritorno non è necessario montare una callback che lo gestisca.

Comunque è possibile simulare richieste sincrone tramite chiamate asincrone. Basta che il server, al termine della sua elaborazione invii al client una richiesta RPC one-way contenente la risposta. Il client la riceverà, inviando al volo il solo ack di ricezione.

Il one-way RPC ha il problema che non garantisce l’affidabilità. Chi invia questa chiamata sa che è stata ricevuta (l’ack di cui si diceva), ma non sa se il ricevente la processerà o meno.

Comunicazione orientata ai messaggi

In tutto quel che si è detto finora, ancora non si è definito con precisione le modalità di comunicazione tra i processi.

Modalità di comunicazione che avranno diverse caratteristiche: sincrone, asincrone, persistenti, transienti, … Ma, alla fine si tratta sempre di definire le interfacce (API) con cui si comunica verso il middleware.

Per la loro definizione avremo diversi paradigmi disponibili:

  • Berkeley sockets;

  • interfacce per il passaggio di messaggi (messagge-passing i/f);

  • sistemi ad accodamento di messaggi (message-queuing systems);

  • message brokers.

Di notevole interesse è la standardizzazione dei socket: i Berkeley socket.

In questo caso abbiamo un sistema transiente (client e server comunicano solo se sono entrambi online). Ed è sincronizzato, nel senso che quando un attore spedisce, l’altro deve essere in ricezione, altrimenti la comunicazione si perde. Ed è punto punto. Tutto ciò definisce un modello. Se poi ogni sistema operativo sviluppasse la sua implementazione, comunque l’utente sarebbe bloccato nell’ambito del sistema operativo scelto. Fortunatamente si è imposta una implementazione come standard trasversale tra i diversi sistemi operativi: i Berkley socket 7.

In questa, un socket è un punto di comunicazione dove una applicazione può inviare e/o ricevere dati. Questo punto di comunicazione astrae dal reale punto di comunicazione usato dal sistema operativo locale per uno specifico protocollo di trasporto.

Le primitive disponibili sono:

  • socket, crea il punto di comunicazione;

  • bind, lega un indirizzo locale al socket;

  • listen, annuncia la disponibilità a ricevere connessioni;

  • accept, ferma il richiedente (al sistema operativo) finché non arriva una richiesta di connessione;

  • connect, tenta di stabilire una connessione;

  • send, invia dati utilizzando la connessione;

  • receive, riceve dati dalla connessione;

  • close, rilascia la connessione.

Tipicamente le prime 4 primitive sono eseguite nei server, e le altre dai client. Salvo le send/receive che sono eseguite da entrambi.

Lo schema di utilizzo è illustrato dal seguente diagramma.

_images/17_uso_socket.svg

Si noti che gli indirizzi IP fanno comunicare le macchine, mentre le porte fanno comunicare i processi (nell’ambito di una stessa macchina o di macchine diverse). Quindi il canale di comunicazione è identificato dalla quadrupla TCP: (IP_mittente, porta_mittente, IP_destinatario, porta_destinatario). Astraendo (poco) dalla quadrupla TCP predetta, vengono costruiti i socket.

I socket hanno il problema di essere poco flessibili per effettuare programmazione distribuita a basso livello. Da qui la costruzione della message-passing interface (MPI).

Questa ritocca le interfacce di comunicazione a basso livello per facilitare la comunicazione di task distribuiti 8. Le MPI sono molto usate in fisica per modellare sistemi distribuiti. Le MPI permettono di gestire tutti i punti di sincronia nell’effettuare le chiamate. Ad esempio:

  • MPI_bsend, appendi un messaggio in uscita al buffer locale d’invio 9;

  • MPI_send, invia un messaggio e attendi finché non è copiato al buffer locale o remoto;

  • MPI_ssend, invia un messagggio e attendi finché il ricevitore non è partito;

I message-queuing systems forniscono interfacce per la comunicazione asincrona persistente. In questo caso il middleware fornisce capacità di memorizzazione dei messaggi, senza richiedere che mittente e/o destinatario siano attivi durante la trasmissione del messaggio.

Usualmente i message-queuing systems sono pensati per supportare messaggistica il cui tempo di trasmissione è dell’ordine dei minuti, a differenza dei Berkeley sockets e di MPI, in cui il tempo di trasmissione dei messaggi è dell’ordine dei secondi o dei millisecondi.

In questo caso il dialogo tra mittente e destinatario avviene tramite una coda, incaricata di immagazzinare i messaggi nel caso in cui il mittente o il destinatario (o entrambi) non siano attivi. Questo tipo di comunicazione è detta loosely-coupled.

Le primitive disponibili, tipicamente sono le seguenti:

  • put, appende un messaggio nella coda di trasmissione richiesta;

  • get, prende il primo messaggio in coda, se la coda è vuota il richiedente rimane bloccato;

  • poll, controlla se una determinata coda contiene messaggi; questa chiamata non blocca mai il chiamante;

  • notify, installa un handler da chiamare quando arriva un messaggio nella coda specificata.

Si osserva che:

  • tipicamente, sono disponibili più code;

  • le code sono distribuite, ovvero realizzate su un insieme di macchine;

  • vi è una mappatura tra le code e gli indirizzi di rete: il mittente invia ad una coda, ma il middleware di comunicazione deve fisicamente inviare il messaggio ad una macchina che gestisce la coda, e questo avviene con il protocollo di rete dati; qui di seguito si illustra il concetto.

_images/18_message_queue_network.svg

Quando il mittente (sender) chiede di accodare un messaggio su una coda, il message-queuing system presente nel computer del sender verifica da una propria lookup table, il nome della coda richiesta a quale indirizzo di rete corrisponde, dopo di che invia il messaggio via TCP all’indirizzo di rete di destinazione. Quando il messaggio arriva nel computer che gestisce la coda, viene preso in carico dal relativo strato del message-queuing system che provvede alla sua gestione.

Più in generale un message-queuing system sarà organizzato tramite un gruppo di router per permettere l’invio dei messaggi tra i diversi computer che ospitano le code. Non vi è un servizio di naming delle code, per evitare di dover gestire il suo aggiornamento quando si aggiunge o si rimuove una coda.

Nell’ambito dei message-queuing system è stato sviluppato il concetto di message broker.

I message broker (intermediari) sono dei nodi speciali, che hanno il compito di gestire la conversione dei dati per permettere l’integrazione tra applicazioni esistenti e nuove applicazioni.

Concettualmente i message broker sono applicazioni, non implementano le code del sistema di messaging. Ma di fatto sono una componente importante, in quanto, tipicamente, sono posizionati tra le code e le applicazioni che devono ricevere i messaggi. Ad es., nei sistemi publish/subscribe, i broker consegnano i messaggi ai sottoscrittori.

_images/19_broker.svg

Oltre la trasparenza degli accessi, il broker può dover implementare altre funzionalità. Ad esempio la traduzione tra linguaggi. Di fatto è una componente fondamentale per implementare l’infrastruttura distribuita del middleware.

Un sistema di message-queuing è WebSphere di IBM. Qui di seguito la sua architettura generale.

_images/20_websphere.svg

Dal diagramma si osserva che WebSphere è un sistema complesso, in grado di integrare chiamate RPC sincrone su un nucleo di tipo message-queuing asincrono. Inoltre è in grado di pilotare sistemi in LAN e in WAN comunque interconnessi.


1

Nota. Il modo canonico di usare i canali pub/sub è tramite messaggi anonimi. D’altro canto, è possibile forzare il sistema aggiungendo al messaggio l’identificativo di chi lo invia, permettando in tal modo una eventuale analisi territoriale della situazione della misura osservata- Ad esempio è possibile avere la mappa della temperatura di un intero edifico, piano per piano.

2

Il come sia implementata non riguarda solo l’algoritmo, ma anche il linguaggio utilizzato. Non è detto sia quello del chiamante.

3

Se il dato passato è presente una sola volta nell’elenco dei parametri di chiamata, l’effetto di questo tipo di chiamata è lo stesso del copy-by-reference, con la differenza che il chiamante lavora direttamente sul valore del dato e non indirettamente tramite il suo indirizzo.

4

E, viceversa, affinché abbiano senso per il chiamante quando si tratterà di fare il procedimento inverso, comunicando al chiamante la risposta della procedura remota.

5

Questa attività di impacchettamento dei dati per la trasmissione è detta marshaling.

6

Quindi l’oggetto deve essere serializzable.

7

I Berkeley sockets sono stati implementati per la prima volta nel sistema operativo 4.2BSD Unix, sviluppato nell’università di Berkeley a partire da Unix dei Bell Laboratory. Divenuti uno standard di fatto, sono poi stati incorporati nello standard ufficiale POSIX (vedi [WIKIP02]).

8

Rispetto lo OSI Reference Model, MPI si colloca ai livelli 5 e superiori. Ma le relative implementazioni toccano anche i livelli dei socket e del TCP (vedi [WIKIP01]).

9

Il massimo dell’asincrono: appende in un buffer e prosegue.