PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Prestazioni delle applicazioni basate su PostgreSQL:latenza e ritardi nascosti

Goldfields Pipeline, di SeanMac (Wikimedia Commons)

Se stai cercando di ottimizzare le prestazioni della tua applicazione basata su PostgreSQL, probabilmente ti stai concentrando sui soliti strumenti:EXPLAIN (BUFFERS, ANALYZE) , pg_stat_statements , auto_spiegazione , log_statement_min_duration , ecc.

Forse stai esaminando una contesa di blocco con log_lock_waits , monitorando le prestazioni del checkpoint, ecc.

Ma hai pensato alla latenza di rete ? I giocatori conoscono la latenza di rete, ma pensavi che fosse importante per il tuo server delle applicazioni?

La latenza è importante

Le tipiche latenze di rete di andata e ritorno client/server possono variare da 0,01 ms (localhost) a circa 0,5 ms di una rete commutata, 5 ms di Wi-Fi, 20 ms di ADSL, 300 ms di routing intercontinentale e anche di più per cose come collegamenti satellitari e WWAN .

Una banale SELEZIONE può richiedere nell'ordine di 0,1 ms per l'esecuzione lato server. Un banale INSERTO può richiedere 0,5 ms.

Ogni volta che l'applicazione esegue una query, deve attendere che il server risponda con successo/fallimento e possibilmente un set di risultati, metadati della query, ecc. Ciò comporta almeno un ritardo di andata e ritorno della rete.

Quando lavori con query piccole e semplici, la latenza di rete può essere significativa rispetto al tempo di esecuzione delle tue query se il tuo database non si trova sullo stesso host della tua applicazione.

Molte applicazioni, in particolare gli ORM, sono molto inclini a eseguire molti di query abbastanza semplici. Ad esempio, se la tua app Hibernate sta recuperando un'entità con un @OneToMany recuperato pigramente in relazione a 1000 elementi figlio, probabilmente eseguirà 1001 query grazie al problema di selezione n+1, se non di più. Ciò significa che probabilmente sta spendendo 1000 volte la latenza di andata e ritorno della rete solo in attesa . Puoi recuperare unisciti a sinistra per evitarlo... ma poi trasferisci l'entità padre 1000 volte nel join e devi deduplicarla.

Allo stesso modo, se stai compilando il database da un ORM, probabilmente stai facendo centinaia di migliaia di banali INSERT s... e aspettando dopo ognuno di loro che il server confermi che è OK.

È facile cercare di concentrarsi sul tempo di esecuzione delle query e cercare di ottimizzarlo, ma c'è solo così tanto che puoi fare con un banale INSERT INTO ...VALUES ... . Elimina alcuni indici e vincoli, assicurati che sia raggruppato in una transazione e il gioco è fatto.

Che ne dici di sbarazzarti di tutte le attese della rete? Anche su una LAN iniziano a sommare migliaia di query.

COPIA

Un modo per evitare la latenza è usare COPIA . Per utilizzare il supporto COPY di PostgreSQL, la tua applicazione o driver deve produrre un insieme di righe simile a CSV e trasmetterle al server in una sequenza continua. Oppure può essere chiesto al server di inviare alla tua applicazione un flusso simile a CSV.

In ogni caso, l'app non può intercalare una COPIA con altre query e gli inserti di copia devono essere caricati direttamente in una tabella di destinazione. Un approccio comune è quello di COPIA in una tabella temporanea, quindi da lì esegui un INSERT INTO ... SELECT ... , AGGIORNAMENTO ... DA .... , ELIMINA DA... UTILIZZANDO... , ecc per utilizzare i dati copiati per modificare le tabelle principali in un'unica operazione.

È utile se stai scrivendo direttamente il tuo SQL, ma molti framework applicativi e ORM non lo supportano, inoltre può solo sostituire direttamente il semplice INSERT . La tua applicazione, framework o driver client deve gestire la conversione per la rappresentazione speciale richiesta da COPY , cerca da solo i metadati di tipo richiesto, ecc.

(Driver notevoli che fa supporto COPIA includono libpq, PgJDBC, psycopg2 e la gemma Pg... ma non necessariamente i framework e gli ORM costruiti su di essi.)

PgJDBC – modalità batch

Il driver JDBC di PostgreSQL ha una soluzione per questo problema. Si basa sul supporto presente nei server PostgreSQL a partire dalla 8.4 e sulle funzionalità di batching dell'API JDBC per inviare un batch di query al server, quindi attendere solo una volta la conferma che l'intero batch è stato eseguito correttamente.

Beh, in teoria. In realtà, alcune sfide di implementazione limitano questo in modo che i batch possano essere eseguiti solo in blocchi di poche centinaia di query nella migliore delle ipotesi. Il driver può anche eseguire query che restituiscono righe di risultati in blocchi batch solo se riesce a capire quanto saranno grandi i risultati in anticipo. Nonostante queste limitazioni, utilizzare Statement.executeBatch() può offrire un enorme aumento delle prestazioni alle applicazioni che eseguono attività come il caricamento di dati in blocco di istanze di database remote.

Poiché è un'API standard, può essere utilizzata da applicazioni che funzionano su più motori di database. Hibernate, ad esempio, può utilizzare il batch JDBC anche se non lo fa per impostazione predefinita.

libpq e batch

La maggior parte (tutti?) Gli altri driver PostgreSQL non supportano il batching. PgJDBC implementa il protocollo PostgreSQL in modo completamente indipendente, mentre la maggior parte degli altri driver utilizza internamente la libreria C libpq fornito come parte di PostgreSQL.

libpq non supporta il batch. Ha un'API asincrona non bloccante, ma il client può comunque avere solo una query "in volo" alla volta. Deve attendere fino alla ricezione dei risultati di quella query prima di poterne inviare un'altra.

Il server di PostgreSQL supporta bene il batching e PgJDBC lo usa già. Quindi ho scritto il supporto batch per libpq e lo ha presentato come candidato per la prossima versione di PostgreSQL. Dal momento che cambia solo il client, se accettato accelererà comunque le cose durante la connessione a server meno recenti.

Sarei davvero interessato al feedback degli autori e degli utenti avanzati di libpq driver client basati su e sviluppatori di libpq applicazioni basate. La patch si applica bene su PostgreSQL 9.6beta1 se vuoi provarlo. La documentazione è dettagliata e c'è un programma di esempio completo.

Prestazioni

Ho pensato che un servizio di database ospitato come RDS o Heroku Postgres sarebbe stato un buon esempio di dove questo tipo di funzionalità sarebbe stato utile. In particolare, l'accesso da parte nostra alle loro reti mostra davvero quanto la latenza può far male.

Con una latenza di rete di circa 320 ms:

  • 500 inserti senza batch:167.0s
  • 500 inserti con batching:1.2s

… che è oltre 120 volte più veloce.

Di solito non eseguirai la tua app su un collegamento intercontinentale tra il server dell'app e il database, ma questo serve a evidenziare l'impatto della latenza. Anche su un socket unix su localhost ho riscontrato un miglioramento delle prestazioni di oltre il 50% per 10000 inserti.

Inserimento in batch di app esistenti

Sfortunatamente non è possibile abilitare automaticamente il batch per le applicazioni esistenti. Le app devono utilizzare un'interfaccia leggermente diversa in cui inviano una serie di query e solo dopo chiedono i risultati.

Dovrebbe essere abbastanza semplice adattare le app che già utilizzano l'interfaccia asincrona libpq, soprattutto se utilizzano la modalità non bloccante e un select() /sondaggio() /epoll() /WaitForMultipleObjectsEx ciclo continuo. App che utilizzano la libpq sincrona le interfacce richiederanno più modifiche.

Inserimento in batch di altri driver client

Allo stesso modo, i driver client, i framework e gli ORM avranno generalmente bisogno dell'interfaccia e di modifiche interne per consentire l'uso del batch. Se stanno già utilizzando un loop di eventi e un I/O non bloccante, dovrebbero essere abbastanza semplici da modificare.

Mi piacerebbe vedere utenti Python, Ruby, ecc. in grado di accedere a questa funzionalità, quindi sono curioso di vedere chi è interessato. Immagina di poterlo fare:

import psycopg2
conn = psycopg2.connect(...)
cur = conn.cursor()

# this is just an idea, this code does not work with psycopg2:
futures = [ cur.async_execute(sql) for sql in my_queries ]
for future in futures:
    result = future.result  # waits if result not ready yet
    ... process the result ...
conn.commit()

L'esecuzione in batch asincrona non deve essere complicata a livello di client.

COPIA è la più veloce

Dove i clienti pratici dovrebbero ancora favorire COPIA . Ecco alcuni risultati dal mio laptop:

inserting 1000000 rows batched, unbatched and with COPY
batch insert elapsed:      23.715315s
sequential insert elapsed: 36.150162s
COPY elapsed:              1.743593s
Done.

Il raggruppamento del lavoro fornisce un aumento sorprendentemente grande delle prestazioni anche su una connessione socket Unix locale…. ma COPIA lascia entrambi i singoli inserti che si avvicinano molto indietro nella polvere.

Usa COPIA .

L'immagine

L'immagine di questo post è del gasdotto Goldfields Water Supply Scheme da Mundaring Weir vicino a Perth, nell'Australia occidentale, ai giacimenti auriferi interni (deserti). È rilevante perché ci è voluto così tanto tempo per finire ed è stato oggetto di critiche così intense che il suo designer e principale sostenitore, C. Y. O'Connor, si è suicidato 12 mesi prima che fosse messo in servizio. La gente del posto spesso (erroneamente) dice che è morto dopo l'oleodotto è stato costruito quando l'acqua non scorreva, perché ci è voluto così tanto tempo che tutti pensavano che il progetto dell'oleodotto fosse fallito. Poi settimane dopo, l'acqua è stata versata.