Come migliorare significativamente le prestazioni Java?


23

Il team di LMAX ha fatto una presentazione di come sono stati in grado di eseguire 100k TPS a meno di 1 ms di latenza . Hanno eseguito il backup di quella presentazione con un blog , un documento tecnico (PDF) e il codice sorgente stesso.

Di recente, Martin Fowler ha pubblicato un eccellente documento sull'architettura LMAX e menziona che ora sono in grado di gestire sei milioni di ordini al secondo e sottolinea alcuni dei passi che il team ha intrapreso per salire di un altro ordine di grandezza nelle prestazioni.

Finora ho spiegato che la chiave della velocità del Business Logic Processor sta facendo tutto in sequenza, in memoria. Basta fare questo (e niente di veramente stupido) consente agli sviluppatori di scrivere codice in grado di elaborare TPS 10K.

Hanno poi scoperto che concentrarsi sui semplici elementi del buon codice potrebbe portarlo nella gamma TPS 100K. Ciò richiede solo codice ben ponderato e metodi di piccole dimensioni; in sostanza, ciò consente a Hotspot di svolgere un lavoro migliore di ottimizzazione e di rendere le CPU più efficienti nella memorizzazione nella cache del codice mentre è in esecuzione.

Ci volle un po 'più di intelligenza per salire di un altro ordine di grandezza. Ci sono diverse cose che il team LMAX ha trovato utili per arrivarci. Uno era quello di scrivere implementazioni personalizzate delle raccolte Java progettate per essere cache-friendly e attente ai rifiuti.

Un'altra tecnica per raggiungere quel massimo livello di prestazioni è porre attenzione ai test delle prestazioni. Ho notato da tempo che le persone parlano molto delle tecniche per migliorare le prestazioni, ma l'unica cosa che fa davvero la differenza è testarle

Fowler ha menzionato che sono state trovate diverse cose, ma ne ha menzionato solo un paio.

Ci sono altre architetture, librerie, tecniche o "cose" che sono utili per raggiungere tali livelli di prestazione?


11
"Quali altre architetture, librerie, tecniche o" cose "sono utili per raggiungere tali livelli di prestazione?" Perchè chiedere? Quella citazione è la lista definitiva. Ci sono molte altre cose, nessuna delle quali ha il gentile impatto degli articoli in quella lista. Qualunque altra cosa chiunque possa nominare non sarà utile come quella lista. Perché chiedere idee sbagliate quando hai citato una delle migliori liste di ottimizzazione mai prodotte?
S.Lott

Sarebbe bello sapere quali strumenti hanno usato per vedere come il codice generato è stato eseguito sul sistema.

1
Ho sentito parlare di gente imprecare con ogni tipo di tecnica. Quello che ho trovato più efficace è la profilazione a livello di sistema. Può mostrarti colli di bottiglia nel modo in cui il tuo programma e carico di lavoro esercitano il sistema. Suggerirei di aderire a linee guida ben note per quanto riguarda le prestazioni e la scrittura di codice modulare in modo da poterlo sintonizzare facilmente in seguito ... Non penso che tu possa sbagliare con la profilazione del sistema.
ritesh

Risposte:


21

Esistono tutti i tipi di tecniche per l'elaborazione delle transazioni ad alte prestazioni e quella nell'articolo di Fowler è solo una delle tante al limite. Piuttosto che elencare un gruppo di tecniche che possono o meno essere applicabili alla situazione di qualcuno, penso che sia meglio discutere i principi di base e come LMAX affronta un gran numero di essi.

Per un sistema di elaborazione delle transazioni su larga scala si desidera eseguire quanto segue quanto segue:

  1. Riduci al minimo il tempo speso nei livelli di archiviazione più lenti. Dal più veloce al più lento su un server moderno hai: CPU / L1 -> L2 -> L3 -> RAM -> Disco / LAN -> WAN. Il salto dal disco magnetico moderno più veloce alla RAM più lenta è oltre 1000x per l' accesso sequenziale ; l'accesso casuale è anche peggio.

  2. Ridurre al minimo o eliminare il tempo di attesa . Ciò significa condividere il minor stato possibile e, se lo stato deve essere condiviso, evitare blocchi espliciti ogni volta che è possibile.

  3. Distribuire il carico di lavoro. Le CPU non sono diventate molto più veloci negli ultimi anni, ma sono diventate più piccole e 8 core sono piuttosto comuni su un server. Oltre a ciò, puoi persino distribuire il lavoro su più macchine, che è l'approccio di Google; la cosa grandiosa è che ridimensiona tutto, incluso l'I / O.

Secondo Fowler, LMAX adotta il seguente approccio per ciascuno di questi:

  1. Tenere tutto lo stato in memoria tutti i tempi. La maggior parte dei motori di database lo farà comunque, se l'intero database può adattarsi alla memoria, ma non vuole lasciare nulla al caso, il che è comprensibile su una piattaforma di trading in tempo reale. Per farcela senza aggiungere un sacco di rischi, hanno dovuto costruire un'infrastruttura leggera di backup e failover.

  2. Utilizzare una coda senza blocchi ("disruptor") per il flusso di eventi di input. Contrasto per tradizionali durevoli code di messaggi che sono definitivamente non bloccano liberi, e infatti di solito coinvolgono dolorosamente lenti transazioni distribuite .

  3. Non tanto. LMAX lancia questo sotto il bus sulla base del fatto che i carichi di lavoro sono interdipendenti; il risultato di uno cambia i parametri per gli altri. Questo è un avvertimento critico , e quello che Fowler chiama esplicitamente. Fanno fare un po 'di uso della concorrenza al fine di fornire capacità di failover, ma tutta la logica di business vengono elaborati su un singolo thread .

LMAX non è l'unico approccio all'OLTP su larga scala. E sebbene sia abbastanza brillante di per sé, non è necessario utilizzare tecniche all'avanguardia per ottenere quel livello di prestazioni.

Di tutti i principi di cui sopra, # 3 è probabilmente il più importante e il più efficace, perché, francamente, l'hardware è economico. Se riesci a suddividere correttamente il carico di lavoro su una mezza dozzina di core e diverse dozzine di macchine, allora il cielo è il limite per le tecniche di calcolo parallelo convenzionali . Sareste sorpresi di quanto tempo riuscirete a realizzare con nient'altro che un mucchio di code di messaggi e un distributore round robin. Ovviamente non è così efficiente come LMAX - in realtà non è nemmeno vicino - ma la velocità effettiva, la latenza e l'efficacia in termini di costi sono preoccupazioni separate, e qui stiamo parlando specificamente della velocità effettiva.

Se hai lo stesso tipo di esigenze speciali di LMAX - in particolare, uno stato condiviso che corrisponde a una realtà aziendale al contrario di una scelta frettolosa di design - allora suggerirei di provare il loro componente, perché non ho visto molto altrimenti è adatto a tali requisiti. Ma se stiamo semplicemente parlando di alta scalabilità, ti esorto a fare ulteriori ricerche sui sistemi distribuiti, perché sono l'approccio canonico utilizzato oggi dalla maggior parte delle organizzazioni (Hadoop e progetti correlati, ESB e architetture correlate, CQRS che anche Fowler menzioni e così via).

Anche gli SSD diventeranno un punto di svolta; probabilmente lo sono già. Ora puoi avere una memoria permanente con tempi di accesso simili alla RAM, e sebbene gli SSD di livello server siano ancora orribilmente costosi, alla fine diminuiranno di prezzo una volta che i tassi di adozione cresceranno. È stato studiato a fondo e i risultati sono piuttosto sbalorditivi e miglioreranno nel tempo, quindi l'intero concetto di "tenere tutto in memoria" è molto meno importante di quanto non fosse in passato. Quindi, ancora una volta, proverei a concentrarmi sulla concorrenza ogni volta che è possibile.


Discutere sui principi su cui si basano i principi è fantastico e il tuo commento è eccellente e ... a meno che il documento di Fowler non avesse avuto un riferimento in una nota a piè di pagina per memorizzare nella cache algoritmi ignari en.wikipedia.org/wiki/Cache-oblivious_algorithm (che si adatta perfettamente a categoria numero 1 che hai sopra) Non mi sarei mai imbattuto in loro. Quindi ... rispetto ad ogni categoria che hai sopra, conosci le prime 3 cose che una persona dovrebbe sapere?
Dakotah North,

@Dakotah: Non inizierei nemmeno a preoccuparmi della localizzazione della cache a meno che e fino a quando non avessi eliminato completamente l'I / O del disco, che è dove la maggior parte del tempo viene spesa in attesa nella stragrande maggioranza delle applicazioni. A parte ciò, cosa intendi con "prime 3 cose che una persona dovrebbe sapere"? Top 3 cosa, sapere cosa?
Aaronaught,

Il salto dalla latenza di accesso RAM (~ 10 ^ -9s) alla latenza del disco magnetico (~ 10 ^ -3s caso medio) è un altro paio di ordini di grandezza maggiore di 1000x. Anche gli SSD hanno ancora tempi di accesso misurati in centinaia di microsecondi.
Sedate Alien,

@Sedate: latenza sì, ma si tratta più di una velocità effettiva che di una latenza non elaborata e, una volta superati i tempi di accesso e raggiungendo la velocità totale di trasferimento, i dischi non sono poi così male. Ecco perché ho fatto la distinzione tra accesso casuale e sequenziale; per gli scenari di accesso casuale che fa in primo luogo diventare un problema di latenza.
Aaronaught,

@Aaronaught: rileggendo, suppongo che tu abbia ragione. Forse si dovrebbe sottolineare che tutti gli accessi ai dati dovrebbero essere il più sequenziali possibile; benefici significativi possono anche essere ottenuti quando si accede ai dati in ordine dalla RAM.
Sedate Alien,

10

Penso che la lezione più grande da imparare da questo sia che devi iniziare con le basi:

  • Buoni algoritmi, strutture dati appropriate e non fare nulla di "veramente stupido"
  • Codice ben ponderato
  • Test delle prestazioni

Durante il test delle prestazioni, si profila il codice, si trovano i colli di bottiglia e li si risolve uno per uno.

Troppe persone passano direttamente alla parte "correggili uno per uno". Passano un sacco di tempo a scrivere "implementazioni personalizzate delle raccolte Java", perché sanno solo che l'intero motivo per cui il loro sistema è lento è a causa di mancati cache. Questo può essere un fattore che contribuisce, ma se salti a destra per modificare il codice di basso livello in quel modo, probabilmente perderai il problema più grande dell'uso di una ArrayList quando dovresti usare una LinkedList o che la vera ragione per cui il tuo sistema è lento perché l'ORM sta caricando in modo lento i figli di un'entità e quindi effettua 400 viaggi separati nel database per ogni richiesta.


7

Non commenterò particolarmente il codice LMAX perché penso che sia ampiamente descritto, ma qui ci sono alcuni esempi di cose che ho fatto che hanno portato a significativi miglioramenti misurabili delle prestazioni.

Come sempre, si tratta di tecniche che dovrebbero essere applicate una volta che si è consapevoli di avere un problema e di dover migliorare le prestazioni , altrimenti è probabile che si stia solo eseguendo un'ottimizzazione prematura.

  • Usa la giusta struttura di dati e creane una personalizzata se necessario : la corretta progettazione della struttura di dati sminuisce il miglioramento che otterrai dalle micro-ottimizzazioni, quindi fallo prima. Se il tuo algoritmo dipende dalle prestazioni di molte letture O (1) veloci di accesso casuale, assicurati di avere una struttura dati che supporti questo! Vale la pena saltare attraverso alcuni cerchi per farlo bene, ad esempio trovare un modo per rappresentare i dati in un array per sfruttare le letture indicizzate O (1) molto velocemente.
  • La CPU è più veloce dell'accesso alla memoria : puoi fare molti calcoli nel tempo necessario per leggere una memoria casuale se la memoria non è nella cache L1 / L2. Di solito vale la pena fare un calcolo se ti fa risparmiare una lettura di memoria.
  • Aiuta il compilatore JIT con i campi, i metodi e le classi final -making final consente specifiche ottimizzazioni che aiutano davvero il compilatore JIT. Esempi specifici:

    • Il compilatore può presumere che una classe finale non abbia sottoclassi, quindi può trasformare chiamate di metodo virtuali in chiamate di metodo statiche
    • Il compilatore può trattare i campi finali statici come una costante per un piacevole miglioramento delle prestazioni, specialmente se la costante viene quindi utilizzata nei calcoli che possono essere calcolati al momento della compilazione.
    • Se un campo contenente un oggetto Java viene inizializzato come finale, l'ottimizzatore può eliminare sia il controllo nullo sia l'invio del metodo virtuale. Bello.
  • Sostituisci le classi di raccolta con gli array : questo si traduce in un codice meno leggibile ed è più difficile da mantenere, ma è quasi sempre più veloce in quanto rimuove uno strato di indiretta e trae vantaggio da molte ottimizzazioni di accesso all'array. Di solito è una buona idea nei cicli interni / nel codice sensibile alle prestazioni dopo averlo identificato come un collo di bottiglia, ma evitare altrimenti per motivi di leggibilità!

  • Usa le primitive ove possibile - le primitive sono fondamentalmente più veloci dei loro equivalenti basati sugli oggetti. In particolare, il pugilato aggiunge un'enorme quantità di spese generali e può causare brutte pause GC. Non consentire l'inserimento di primitivi se ti preoccupi delle prestazioni / della latenza.

  • Ridurre al minimo i blocchi di basso livello : i blocchi sono molto costosi a un livello basso. Trova i modi per evitare il blocco completo o il blocco a un livello approssimativo, in modo che tu debba bloccare raramente blocchi di dati di grandi dimensioni e il codice di basso livello possa procedere senza doversi preoccupare di problemi di blocco o concorrenza.

  • Evita di allocare memoria : questo potrebbe effettivamente rallentarti in generale poiché la garbage collection di JVM è incredibilmente efficiente, ma è molto utile se stai cercando di arrivare a una latenza estremamente bassa e devi ridurre al minimo le pause GC. Esistono strutture dati speciali che è possibile utilizzare per evitare allocazioni: la libreria http://javolution.org/ in particolare è eccellente e notevole per queste.

Non sono d'accordo con la messa a punto dei metodi . La SIC è in grado di capire che un metodo non viene mai ignorato. Inoltre, nel caso in cui una sottoclasse venga caricata in un secondo momento, è possibile annullare l'ottimizzazione. Si noti inoltre che "evitare l'allocazione della memoria" può anche rendere più difficile il lavoro del GC e quindi rallentarlo, quindi utilizzare con cautela.
maaartinus,

@maaartinus: per quanto riguarda finalalcuni SIC potrebbe capirlo, altri no. Dipende dall'implementazione (così come molti suggerimenti per l'ottimizzazione delle prestazioni). D'accordo sulle allocazioni - devi fare un benchmark. Di solito ho scoperto che è meglio eliminare le allocazioni, ma YMMV.
Mikera,

4

A parte quanto già affermato in un'eccellente risposta di Aaronaught, vorrei notare che un codice del genere potrebbe essere piuttosto difficile da sviluppare, comprendere e eseguire il debug. "Mentre molto efficiente ... è molto facile rovinare ..." come uno dei loro ragazzi ha menzionato nel blog LMAX .

  • Per uno sviluppatore abituato a query e blocchi tradizionali , la codifica per un nuovo approccio potrebbe sembrare come cavalcare il cavallo selvaggio. Almeno questa è stata la mia esperienza quando ho sperimentato Phaser quale concetto è menzionato nel documento tecnico LMAX. In tal senso direi che questo approccio scambia la contesa con il blocco con la contesa cerebrale con gli sviluppatori .

Dato sopra, penso che coloro che scelgono Disruptor e approcci simili si assicurino che dispongano di risorse di sviluppo sufficienti per mantenere la loro soluzione.

Nel complesso, l'approccio Disruptor mi sembra abbastanza promettente. Anche se la tua azienda non può permettersi di utilizzarla, ad esempio per i motivi sopra menzionati, considera di convincere il tuo management a "investire" qualche sforzo nello studio (e SEDA in generale) - perché se non lo fanno allora c'è la possibilità che un giorno i loro clienti li lasceranno a favore di alcune soluzioni più competitive che richiedono server 4x, 8x ecc. in meno.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.