Java e le architetture a 64 bit: i puntatori compressi

II parte: Sfruttare i vantaggi in ottica SOAdi

Nell'articolo precedente si è visto come un'applicazione disegnata per architetture a 32 bit fatta girare su architetture a 64 bit senza particolari accorgimenti necessita di un incremento di spazio di memoria (heap) dell'ordine del 50% per presentare prestazioni analoghe. Inoltre si è accennato brevemente al meccanismo dei puntatori compressi, feature introdotta con Java 6 per arginare questo problema. In questo articolo, dopo aver spiegato in dettaglio in cosa consiste questo meccanismo, si illustreranno alcuni vantaggi offerti alle architetture di sistemi SOA dallo spazio di indirizzamento a 64bit.

Puntatori compressi

Come visto nel corso dell'articolo precedente [1], per adeguare le prestazioni di applicazioni disegnate per architetture a 32 bit fatte eseguire su piattaforme a 64 bit è stato implementato il meccanismo della compressione dei puntatori introdotto nella Java HotSpot VM 14.0 (JDK 1.6.0_14). Il meccanismo è attivabile per mezzo dell'opzione: -XX +UseCompressedOops.

Il nome di questa feature è dovuto al fatto che l'acronimo "oop" nel contesto del HotSpot è utilizzato per indicare Ordinary Object Pointer ("puntatore ordinario ad oggetto"). La dimensione di tali puntatori è normalmente data dalla dimensione dell'architettura dove gira la JVM, quindi 32 bit su architetture a 32 bit e 64 su quelle a 64.

La strategia impiegata è, dal punto di vista teorico, molto semplice e consiste nell'utilizzare riferimenti relativi a 32 bit che a partire da un indirizzo di base puntano ad uno spazio di memoria contiguo. La traduzione degli indirizzi compressi richiede di aggiungere un offset a 32 bit per ottenere quindi l'indirizzo a 64. La procedura inversa, compressione degli indirizzi, si ottiene sottraendo l'offset all'indirizzo a 64 bit e quindi utilizzando i 32 bit meno significativi. Questo però è vero solo da un punto di vista teorico: in realtà la situazione è molto più complessa.

I riflessi sulla Java Virtual Machine

Il problema da risolvere consisteva nell'introdurre questo meccanismo nel sistema di gestione dei puntatori della JVM, sistema che già di per se' è tutt'altro che banale. Inoltre, questa introduzione, era vincolata da una serie di requisiti stringenti, tra i quali i più importanti erano

  • non peggiorare le performance,
  • mantenere la JVM indipendentemente dal fatto che il meccanismo dei puntatori compressi fosse più o meno abilitato.

L'unico effetto collaterale accettato era quello di ridurre lo spazio di indirizzamento a 32 Giga oggetti (che comunque è, allo stato attuale, uno spazio enorme). In sostanza si trattava di avere un'occupazione di memoria paragonabile a quella richiesta alla JVM per funzionare su architetture a 32 bit e una capacità di gestire spazi di indirizzamento simili a quelli permessi da applicazioni a 64 bit.

Attenzione: alcuni lettori a questo punto potrebbero essere tratti in inganno dal valore di 32 Giga. Il dubbio è legittimo perche' se si paralasse di un indirizzamento a 32 bit allora le applicazioni potrebbe gestire uno spazio di memoria limitato ai 4 Gb (Giga byte) e non di 32 Gb. Ma in questo contesto si tratta di scalare puntatori ad oggetti di un fattore pari ad 8 (vedi [2]) che consente alle applicazioni di indirizzare fino a 4 Giga oggetti (in modalità puntatori compressi), che equivale ad uno spazio heap d 32 Gb. Il tutto è illustrato di seguito.

Gestire gli oggetti: Object header

Prima di procedere oltre è necessario illustrare brevemente la struttura utilizzata dalla JVM per tenere traccia e quindi gestire gli oggetti creati. Senza entrare nei dettagli più approfonditi, diciamo che questa struttura con cui la JVM gestisce gli oggetti è la cosiddetta intestazione degli oggetti (object header) che contiene le seguenti due word (figura 1):

  • mark. Si tratta della prima parola (word) gestita secondo una struttura logica (bit gestiti attraverso opportune maschere) che include una serie di importanti informazioni utilizzate dai vari thread della JVM. Vi è per esempio l'identità dell'oggetto (hash code), l'età (age) e il relativo stato di sincronizzazione (è consentita, anche se raramente utilizzata, l'alternativa di inserire un puntatore a un altro oggetto che incapsula tali informazioni). Inoltre, durante il processo di Garbage Collection, può contenere informazioni relative allo stato di GC.
  • klass. Contiene il riferimento in memoria (puntatore) a un oggetto, chiamato meta-oggetto, che descrive la struttura e il comportamento dell'oggetto originale. Questo meta-oggetto è, a tutti gli effetti, la "classe" di cui l'oggetto è istanza. Pertanto, rappresenta una vtable C++. Questi meta-oggetti sono memorizzati in uno spazio ben definito dell'heap denominato "generazione permanente" (PermGen, Permanent Generation), e viene mantenuto separato dallo spazio riservato alle relative istanze denominato " generazione giovane " (young generation). Questa suddivisione dell'heap è un'ottimizzazione molto utile al GC: evita di esaminare una larga porzione di memoria, la PermGen, che di per se' contiene elementi molto meno volatili.

A queste due word, si può eventualmente aggiungere anche la lista dei campi opportunamente ordinata.

Figura 1 - Rappresentazione della struttura object header

 

Le dimensioni di queste word sono date dall'architettura sottostante (si sta parlando della JVM che, tra i vari gravosi compiti, deve mediare tra il livello di astrazione del bytecode e l'architettura fisica e del sistema operativo sottostanti).

Data la struttura riportata in figura 1 è possibile, per esempio, comprendere, almeno da un punto di vista concettuale, il funzionamento del processo di accesso degli oggetti Java alle porzioni di codice marcate come sincronizzate. In particolare, quando un thread richiede di accedere a un oggetto sincronizzato, la JVM verifica lo stato dell'apposito sottocampo della work mark del corrispondente object header. Verifica che i due bit meno significativi siano impostati al valore 01. Se tale configurazione è verificata, allora il thread può accedere alla parte di codice sincronizzata e la JVM si fa carico di cambiare la configurazione di tali bit (configurazione 00) e di memorizzare l'indirizzo del thread bloccante. In realtà l'algoritmo è più complesso e tra le altre cose, deve a sua volta gestire l'accesso concorrente a questa struttura, ma comunque il meccanismo descritto è alla base della strategia di lock leggero ("thin lock" [3]).

Puntatori compressi al lavoro

Una volta compresa la struttura dell'object header, è possibile comprendere meglio il meccanismo della feature dei puntatori compressi. È importante notare che l'implementazione di questa feature non poteva essere ottenuta riscrivendo grandi porzioni della JVM. Particolarmente problematica sarebbe risultata la riscrittura di tutta una serie di meccanismi che si occupano di ottimizzare il bytecode prima di eseguirlo. Questi meccanismi, oltre a essere stati ben studiati e verificati accuratamente in laboratorio, sono spesso scritti in assembler. Pertanto la soluzione finale è, come spesso accade, un buon compromesso tra, da una parte, la quantità di lavoro richiesto per reimplementare intere porzioni della JVM e testarne il funzionamento e l'efficacia e, dall'altra, il beneficio ottenuto effettivamente.

Nell'attuale implementazione, è compresso il seguente insieme di indirizzi memorizzati nell'heap:

  • il campo klass di ogni oggetto (il puntatore al meta-oggetto)
  • tutti gli oggetti puntatore
  • tutti gli elementi di un array di oggetti (ossia di un array di puntatori)

Quindi esiste tutta una serie di indirizzi che non sono mai compressi; gli elementi non compressi più importanti sono:

  • la struttura utilizzata per gestire i meta-oggetti e quindi tutta l'area PermGen;
  • tutti gli indirizzi utilizzati all'interno dell'interprete: questi includono gli elementi dello stack, gli argomenti passati e ricevuti dalle invocazioni di metodi. L'interprete si fa quindi carico di decomprimere gli indirizzi caricati dall'heap e di comprimerli prima di memorizzarli nuovamente.

Da tener presente che le JVM più sofisticate, prima di eseguire il bytecode, eseguono una serie di tentativi atti a ottimizzarne l'esecuzione. Quindi, molti indirizzi sono effettivamente compressi o meno a seconda del risultato di questo tentativo di ottimizzazione. Ciò significa che esiste tutta una serie di strutture che non sono necessariamente compresse o non compresse: possono venir compresse o meno proprio nell'ottica di una ottimizzazione.

Architetture a 64 bit e architetture SOA

Fin qui questi due articoli dedicati alle architetture a 64 bit hanno affrontato tali architetture a un livello di astrazione molto basso: si è parlato di cache interne ai microprocessori, della Java Virtual Machine, dei puntatori compressi e così via. Ora si vuole compiere un notevole balzo verticale per raggiungere discussioni a maggiore grado di astrazione: si voglio raggiungere i famosi 10,000 metri di quota, la troposfera.

In particolare, si vuole effettuare una veloce incursione nel mondo delle architetture SOA. Quando si disegnano architetture orientate ai servizi, si cerca di organizzare il sistema quanto più possibilmente in termini di self contained stateless macro servizi (coarse grained service), possibilmente allineati ai servizi business; eventualmente vi si aggiunge un sistema di orchestrazione, e così via (figura 2). Fin qui tutto abbastanza familiare: esiste tutta una letteratura dedicata all'argomento ([4], [5], [6]).

Figura 2 - Tipica architettura orientata ai servizi.

 

Un problema che invece è spesso trascurato è relativo alla condivisione dei dati tra i diversi servizi. Si tratta di un problema che investe diverse sfere: disponibilità dei dati nell'azienda, assicurazione del livello di qualità desiderato, infrastruttura necessaria per la relativa gestione ed efficiente condivisione, e così via. Tutti coloro che hanno lavorato in sistemi globali conoscono bene quanto sensibili e complesse siano queste problematiche. In questo contesto, tuttavia, si vuole focalizzare l'attenzione sull'infrastruttura. L'esperienza insegna che se i vari servizi non condividono gli stessi dati (almeno quelli"statici" o di riferimento) allora realizzare sistemi SOA diventa un semplice esercizio accademico senza alcuna utilità pratica. Si consideri il servizio del Pre-deal checking, ossia del controllo del rischio associato al cliente che si deve eseguire nell'area del front office delle investment bank prima di stipulare un trade. A tal fine il sistema di front office esegue un'invocazione specificando i pochi dati necessari, come il codice univoco del cliente, l'ammontare del trade, la moneta di riferimento, la tipologia del trade etc. Ecco quindi che, oltre ad assicurarsi che il servizio sia stato implementato rispettando le regole SOA, è anche necessario che ci sia un totale accordo sui dati. Il client X deve corrispondere alla stessa entità in entrambi i servizi (molto interessante è il caso in cui un sistema chiede di eseguire il controllo di un'entità X, e che il sistema demandato a tale funzionalità verifichi invece il cliente Y per un mancato allineamento dei dati), la tipologia di trade Y deve corrispondere allo stesso tipo per entrambi i servizi e così via. Ovviamente, ciò non vale solo per il sistema di front office e quello del calcolo del rischio associato al cliente, ma anche per tutti gli altri (enrich e validation, settlment, reporting, etc.). Ecco quindi che il problema di assicurarsi che i diversi sistemi condividano gli stessi dati di riferimento diviene un'esigenza assolutamente centrale.

Un esempio con la sua soluzione

Una prima soluzione a questo problema potrebbe consistere nel realizzare un sistema di gestione dei dati di riferimento (Data Manager) che a sua volta esponga una serie di servizi SOA disegnati per fornire, a fronte del codice univoco di un dato statico, l'insieme dei corrispondenti dati (figura 3).

Figura 3 - Data Manager system.

 

Per esempio, dato il client X, dovrebbe restituire in output tutte le relative informazioni: codice univoco, nome abbreviato, nome esteso, nazione di residenza, nazione di registrazione, lista di rating, e così via. Oppure, dato il tipo di trade "EXCTS", riporti tutte le informazioni relative allo specifico tipo di trade esotico. Quindi ciò farebbe sì che i dati scambiati all'atto dell'invocazione dei vari servizi abbiano significato univoco in entrambi i sistemi: cliente e fornitore. Il sistema di gestione della UI potrebbe ottenere le varie liste dei dati, il trader potrebbe quindi compilare un ordine selezionando i dati di riferimento dalle varie liste quindi inviare il trade ai necessari servizi a valle e così via. Questi a loro volta potrebbero accedere al servizio di gestione dei dati per "tradurre" i vari codice inclusi nello scambio dati scaturito dall'originaria invocazione del servizio e così via. Tutto potrebbe funzionare bene se non fosse per il fatto che alcuni servizi sono gravati da stringenti requisiti non funzionali.

Per esempio, la tipica SLA (Service Level Agreement) di un servizio di Pre-Deal Checking consiste nel rispondere entro 10 millisecondi per il 90% dei casi; alcuni sistemi di trading automatico presentano SLA ancora più stringenti. In scenari di questo tipo, ogni invocazione esterna, compresi gli accessi al database, sono semplicemente un lusso non fattibile. Ecco quindi che il sistema Data Manager, da solo, non è più sufficiente per soddisfare i requisiti funzionali e non funzionali del sistema. È necessario assicurarsi che i sistemi che erogano servizi real-time abbiamo i dati già pronti al loro interno e che questi, ovviamente, siano condivisi. Un modo per ottenere ciò consiste nel ricorrere a concetti come il data fabric (figura 4). Si tratta di un'infrastruttura che fornisce meccanismi scalabili per accedere e distribuire dati a tutta una serie di servizi e/o applicazioni.

Figura 4 - Data Fabric.

 

Questo meccanismo deve assicurare che i dati siano accurati, aggiornati, sempre disponibili e accessibili istantaneamente. Questi meccanismi sono basati su cache distribuite. In questo contesto, il sistema Data Manager, si occupa di ricevere tutti gli aggiornamenti dei dati, sia manualmente attraverso apposite UI, sia attraverso feed interni ed esterni all'organizzazione (per esempio file dati forniti da Bloomberg) e quindi di organizzarli razionalmente nei propri domini di cache interna. Questi, sono quindi distribuiti automaticamente dall'infrastruttura ai soli sistemi/servizi gravati da stringenti requisiti non funzionali, gli altri possono tranquillamente funzionare in modalità request-reply.

Il succo della questione

Ora il problema classico di questi sistemi funzionanti in architetture a 32 bit era costituito dalla difficoltà di mantenere in memoria quantitativi di dati superiori al singolo Giga byte, limitazione che richiedeva tutta una serie di contromisure, inclusa una politica molto aggressiva di espulsione (eviction) dei dati non acceduti di recente, che spesso finivano per mettere in discussione i vantaggi dell'intero meccanismo di cache distribuita. Con la larga disponibilità di architetture a 64 bit tutto ciò dovrebbe essere un mero ricordo ed ecco come queste architetture siano in grado di fornire grandi vantaggi al disegno di sistemi SOA.

Conclusioni

In questo secondo articolo dedicato alle architetture a 64 bit si è descritto brevemente il meccanismo dei puntatori compressi per poi eseguire un notevole salto di astrazione fino a giungere a discutere di architetture SOA. In particolare, si è brevemente descritto un problema spesso trascurato nella realizzazione delle architetture SOA: la condivisione dei dati di riferimento. Poco serve implementare una serie di servizi esposti attraverso interfacce lineari e aperte se poi i diversi sistemi non scambiano dati basati su un insieme condiviso: sarebbe come creare sistemi di comunicazione ultra sofisticati per connettere persone che parlano lingue diverse. Pertanto, il successo di architetture SOA di grandi dimensioni è spesso subordinato all'adozione di un'efficace infrastruttura dotata di meccanismi scalabili per distribuire ai vari servizi dati accurati, aggiornati e accessibili istantaneamente o, meglio ancora, preventivamente. Queste infrastrutture, tradizionalmente, dovevano fare i conti con la difficoltà di mantenere in memoria quantitativi di dati superiori al singolo Gigabyte. Limiti come questo dovrebbero essere ormai un brutto ricordo grazie alle architetture a 64 bit e alla sempre crescente disponibilità di banchi di memoria di grandissime dimensioni con bassissimi tempi di accesso a costi contenuti.

Riferimenti

[1] Luca Vetti Tagliati, "Java e le architetture a 64 bit: i puntatori compressi - I parte: Cosa succede con i 64 bit", MokaByte 157, dicembre 2010

 

[2] John Rose, "Compressed OOPs"

http://wikis.sun.com/display/HotSpotInternals/CompressedOops

 

[3] O. Agesen - D. Detlefs - A. Garthwaite - R. Knippel, "Proceedings of the ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications", 1999

 

[4] Thomas Erl, "Service-Oriented Architecture: Concepts, Technology & Design", Prentice Hall

 

[5] Thomas Erl, "SOA Principles of Service Design", Prentice Hall

 

[6] Thomas Erl, "SOA Design Patterns", Prentice Hall

Condividi

Pubblicato nel numero
158 gennaio 2011
Luca Vetti Tagliati ha conseguito il PhD presso il Birkbeck College (University of London) e, negli anni, ha applicato la progettazione/programmazione OO, Component Based e SOA in diversi settori che variano dal mondo delle smart card a quello del trading bancario, prevalentemente in ambienti Java EE. Dopo aver lavorato a…
Articoli nella stessa serie
Ti potrebbe interessare anche