Salvataggio di eventi ad alta frequenza in un database vincolato dal limite di connessione


13

Abbiamo una situazione in cui devo affrontare un massiccio afflusso di eventi in arrivo sul nostro server, in media a circa 1000 eventi al secondo (il picco potrebbe essere ~ 2000).

Il problema

Il nostro sistema è ospitato su Heroku e utilizza un DB Heroku Postgres relativamente costoso , che consente un massimo di 500 connessioni DB. Usiamo il pool di connessioni per connetterci dal server al DB.

Gli eventi arrivano più velocemente di quanto il pool di connessioni DB sia in grado di gestire

Il problema che abbiamo è che gli eventi arrivano più velocemente di quanto il pool di connessioni possa gestire. Quando una connessione ha terminato il round trip di rete dal server al DB, in modo che possa essere rilasciata nuovamente nel pool, più di naltri eventi entrano.

Alla fine gli eventi si accumulano, in attesa di essere salvati e poiché non ci sono connessioni disponibili nel pool, scadono e l'intero sistema viene reso non operativo.

Abbiamo risolto l'emergenza emettendo gli eventi offensivi ad alta frequenza a un ritmo più lento dai clienti, ma vogliamo ancora sapere come gestire questi scenari nel caso in cui dobbiamo gestire quegli eventi ad alta frequenza.

vincoli

Altri client potrebbero voler leggere eventi contemporaneamente

Altri client richiedono continuamente di leggere tutti gli eventi con una chiave particolare, anche se non sono ancora stati salvati nel DB.

Un client può interrogare GET api/v1/events?clientId=1e ottenere tutti gli eventi inviati dal client 1, anche se quegli eventi non sono ancora stati salvati nel DB.

Ci sono esempi di "classe" su come affrontarlo?

Possibili soluzioni

Accoda gli eventi sul nostro server

Potremmo accodare gli eventi sul server (con la coda con una concorrenza massima di 400, quindi il pool di connessioni non si esaurisce).

Questa è una cattiva idea perché:

  • Consumerà la memoria disponibile del server. Gli eventi accatastati accumulati consumeranno enormi quantità di RAM.
  • I nostri server si riavviano ogni 24 ore . Questo è un limite rigido imposto da Heroku. Il server può riavviarsi mentre gli eventi vengono accodati causandoci la perdita degli eventi accodati.
  • Introduce lo stato sul server, danneggiando così la scalabilità. Se disponiamo di un'impostazione multi-server e un client desidera leggere tutti gli eventi accodati + salvati, non sapremo su quale server vivono gli eventi accodati.

Utilizzare una coda messaggi separata

Presumo che potremmo usare una coda di messaggi, (come RabbitMQ ?), Dove pompiamo i messaggi in esso e dall'altra parte c'è un altro server che si occupa solo di salvare gli eventi sul DB.

Non sono sicuro che le code dei messaggi consentano di eseguire query sugli eventi accodati (che non sono stati ancora salvati), quindi se un altro client desidera leggere i messaggi di un altro client, posso semplicemente ottenere i messaggi salvati dal DB e i messaggi in sospeso dalla coda e concatenarli insieme in modo da poterli rispedire al client di richiesta di lettura.

Utilizzare più database, ognuno dei quali salva una parte dei messaggi con un server coordinatore DB centrale per gestirli

Un'altra soluzione che abbiamo pensato è usare più database, con un "coordinatore / bilanciamento del carico" centrale. Al ricevimento di un evento, questo coordinatore sceglierebbe uno dei database in cui scrivere il messaggio. Ciò dovrebbe consentirci di utilizzare più database Heroku, aumentando così il limite di connessione a 500 x numero di database.

Su una query di lettura, questo coordinatore può inviare SELECTquery a ciascun database, unire tutti i risultati e inviarli al client che ha richiesto la lettura.

Questa è una cattiva idea perché:

  • Questa idea sembra ... ehm ... troppo ingegneristica? Sarebbe un incubo anche da gestire (backup ecc.). È complicato da costruire e mantenere e, a meno che non sia assolutamente necessario, suona come una violazione dei KISS .
  • Sacrifica la coerenza . Effettuare transazioni su più DB non è necessario se seguiamo questa idea.

3
Dov'è il tuo collo di bottiglia? Stai citando il tuo pool di connessioni, ma ciò influenza solo il parallelismo, non la velocità per inserimento. Se hai 500 connessioni e, ad esempio, 2000QPS, questo dovrebbe funzionare bene se ogni query viene completata entro 250 ms, che è un tempo troppo lungo. Perché è superiore a 15 ms? Si noti inoltre che utilizzando un PaaS si stanno offrendo importanti opportunità di ottimizzazione, come ad esempio ridimensionare l'hardware del database o utilizzare le repliche di lettura per ridurre il carico sul database primario. Heroku non ne vale la pena a meno che la distribuzione non sia il tuo più grande problema.
amon,

@amon Il collo di bottiglia è davvero il pool di connessioni. Ho eseguito ANALYZEle query stesse e non sono un problema. Ho anche creato un prototipo per testare l'ipotesi del pool di connessioni e verificato che questo è davvero il problema. Il database e il server stesso vivono su macchine diverse, quindi la latenza. Inoltre, non vogliamo rinunciare a Heroku a meno che non sia assolutamente necessario, non essere preoccupati per le distribuzioni è un vantaggio enorme per noi.
Nik Kyriakides,

1
Detto questo, capisco che ci sono microottimizzazioni che potrei fare che mi aiuteranno a risolvere il problema attuale . Mi chiedo se esiste una soluzione architettonica scalabile al mio problema.
Nik Kyriakides,

3
Come hai verificato esattamente che il problema è il pool di connessioni? @amon ha ragione nei suoi calcoli. Prova a emettere select nullsu 500 connessioni. Scommetto che scoprirai che il pool di connessioni non è il problema.
usr

1
Se selezionare null è problematico, allora probabilmente hai ragione. Anche se sarebbe interessante dove tutto quel tempo è trascorso. Nessuna rete è così lenta.
usr,

Risposte:


9

Flusso di input

Non è chiaro se i tuoi 1000 eventi / secondo rappresentano picchi o se si tratta di un carico continuo:

  • se è un picco, è possibile utilizzare una coda di messaggi come buffer per distribuire il carico sul server DB più a lungo;
  • se è a carico costante, la coda dei messaggi da sola non è sufficiente, poiché il server DB non sarà mai in grado di recuperare. Quindi dovresti pensare a un database distribuito.

La soluzione proposta

Intuitivamente, in entrambi i casi, sceglierei un flusso di eventi basato su Kafka :

  • Tutti gli eventi sono sistematicamente pubblicati su un argomento kafka
  • Un consumatore si iscriverebbe agli eventi e li memorizzerebbe nel database.
  • Un elaboratore di query gestirà le richieste dai client e interrogherà il DB.

Questo è altamente scalabile a tutti i livelli:

  • Se il server DB è il collo di bottiglia, è sufficiente aggiungere diversi utenti. Ciascuno potrebbe iscriversi all'argomento e scrivere su un altro server DB. Tuttavia, se la distribuzione avviene in modo casuale tra i server DB, il Query Processor non sarà in grado di prevedere il server DB da prendere e dovrà interrogare diversi server DB. Ciò potrebbe comportare un nuovo collo di bottiglia sul lato della query.
  • Lo schema di distribuzione del DB potrebbe quindi essere anticipato organizzando il flusso di eventi in diversi argomenti (ad esempio, utilizzando gruppi di chiavi o proprietà, per partizionare il DB secondo una logica prevedibile).
  • Se un server di messaggi non è sufficiente per gestire un flusso crescente di eventi di input, è possibile aggiungere partizioni kafka per distribuire argomenti kafka su più server fisici.

Offrire agli utenti eventi non ancora scritti nel DB

Volete che i vostri clienti siano in grado di accedere anche alle informazioni ancora nella pipe e non ancora scritte nel DB. Questo è un po 'più delicato.

Opzione 1: utilizzo di una cache per integrare le query db

Non ho analizzato in modo approfondito, ma la prima idea che mi viene in mente sarebbe quella di rendere il / i processore / i di query un consumatore / i degli argomenti kafka, ma in un diverso gruppo di consumatori kafka . Il processore di richiesta riceverà quindi tutti i messaggi che il writer DB riceverà, ma in modo indipendente. Potrebbe quindi tenerli in una cache locale. Le query verrebbero quindi eseguite su DB + cache (+ eliminazione dei duplicati).

Il design sarebbe quindi simile a:

inserisci qui la descrizione dell'immagine

La scalabilità di questo livello di query potrebbe essere ottenuta aggiungendo più processori di query (ciascuno nel proprio gruppo di consumatori).

Opzione 2: progettare una doppia API

Un approccio migliore IMHO sarebbe quello di offrire una doppia API (utilizzare il meccanismo del gruppo di consumatori separato):

  • un'API di query per l'accesso agli eventi nel DB e / o per effettuare analisi
  • un'API di streaming che inoltra i messaggi direttamente dall'argomento

Il vantaggio è che lasci che il cliente decida ciò che è interessante. Ciò potrebbe evitare di unire sistematicamente i dati DB con dati appena incassati, quando il cliente è interessato solo a nuovi eventi in arrivo. Se è davvero necessaria la delicata fusione tra eventi nuovi e archiviati, il cliente dovrebbe organizzarlo.

varianti

Ho proposto kafka perché è progettato per volumi molto elevati con messaggi persistenti in modo da poter riavviare i server se necessario.

È possibile costruire un'architettura simile con RabbitMQ. Tuttavia, se sono necessarie code persistenti, è possibile che le prestazioni vengano ridotte . Inoltre, per quanto ne so, l'unico modo per ottenere il consumo parallelo degli stessi messaggi da parte di diversi lettori (ad esempio writer + cache) con RabbitMQ è clonare le code . Quindi una maggiore scalabilità potrebbe avere un prezzo più elevato.


Stellare; Cosa intendi con a distributed database (for example using a specialization of the server by group of keys)? Anche perché Kafka invece di RabbitMQ? C'è un motivo particolare per scegliere l'uno rispetto all'altro?
Nik Kyriakides,

@NicholasKyriakides Grazie! 1) Stavo semplicemente pensando a diversi server di database indipendenti ma con un chiaro schema di partizionamento (chiave, geografia, ecc.) Che poteva essere usato per inviare efficacemente i comandi. 2) Intuitivamente , forse perché Kafka è progettato per un throughput molto elevato con messaggi persistenti, è necessario riavviare i server?). Non sono sicuro che RabbitMQ sia flessibile per gli scenari distribuiti e che le code persistenti riducano le prestazioni
Christophe,

Per 1) Quindi questo è abbastanza simile alla mia Use multiple databasesidea, ma stai dicendo che non dovrei distribuire casualmente (o round-robin) i messaggi a ciascuno dei database. Giusto?
Nik Kyriakides,

Sì. Il mio primo pensiero sarebbe di non optare per la distribuzione casuale perché potrebbe aumentare il carico di elaborazione per le query (ovvero la query di entrambi i DB multipli per la maggior parte del tempo). Potresti anche prendere in considerazione motori DB distribuiti (ad es. Ignite?). Ma fare una scelta informata richiederebbe una buona comprensione dei modelli di utilizzo del DB (cos'altro c'è nel db, quanto spesso viene interrogato, che tipo di query, ci sono vincoli transazionali oltre i singoli eventi, ecc ...).
Christophe,

3
Voglio solo dire che anche se la kafka può fornire un throughput molto elevato, è probabilmente al di là della maggior parte delle persone. Ho scoperto che trattare con kafka e la sua API è stato un grosso errore per noi. RabbitMQ non è slouch e ha un'interfaccia che ti aspetteresti da un MQ
imel96

11

La mia ipotesi è che devi esplorare più attentamente un approccio che hai rifiutato

  • Accoda gli eventi sul nostro server

Il mio suggerimento sarebbe di iniziare a leggere i vari articoli pubblicati sull'architettura LMAX . Sono riusciti a far funzionare il batch ad alto volume per il loro caso d'uso e potrebbe essere possibile rendere i tuoi compromessi più simili ai loro.

Inoltre, potresti voler vedere se riesci a toglierti di mezzo le letture - idealmente ti piacerebbe essere in grado di ridimensionarle indipendentemente dalle scritture. Ciò può significare esaminare CQRS (segregazione responsabilità responsabilità query comando).

Il server può riavviarsi mentre gli eventi vengono accodati causandoci la perdita degli eventi accodati.

In un sistema distribuito, penso che tu possa essere abbastanza sicuro che i messaggi andranno persi. Potresti essere in grado di mitigare un po 'dell'impatto di ciò essendo attento alle barriere della sequenza (ad esempio, assicurando che la scrittura su memoria duratura avvenga prima che l'evento sia condiviso al di fuori del sistema).

  • Utilizzare più database, ognuno dei quali salva una parte dei messaggi con un server coordinatore DB centrale per gestirli

Forse - Sarebbe più probabile che guardi i tuoi confini aziendali per vedere se ci sono luoghi naturali in cui condividere i dati.

Ci sono casi in cui la perdita di dati è un compromesso accettabile?

Beh, suppongo che ci possa essere, ma non è dove stavo andando. Il punto è che il design avrebbe dovuto incorporare la robustezza richiesta per progredire di fronte alla perdita di messaggi.

Ciò che appare spesso è un modello basato su pull con notifiche. Il provider scrive i messaggi in un negozio durevole ordinato. Il consumatore estrae i messaggi dal negozio, monitorando il proprio marchio di qualità. Le notifiche push vengono utilizzate come dispositivo di riduzione della latenza, ma se la notifica viene persa, il messaggio viene comunque recuperato (eventualmente) perché il consumatore esegue una pianificazione regolare (la differenza è che se la notifica viene ricevuta, l'estrazione avviene prima ).

Vedi Messaggistica affidabile senza transazioni distribuite, di Udi Dahan (già indicato da Andy ) e Polyglot Data di Greg Young.


In a distributed system, I think you can be pretty confident that messages are going to get lost. Veramente? Ci sono casi in cui la perdita di dati è un compromesso accettabile? Avevo l'impressione che perdere i dati = fallimento.
Nik Kyriakides,

1
@ NicholasKyriakides, di solito non è accettabile, quindi OP ha suggerito la possibilità di scrivere in un negozio durevole prima di emettere l'evento. Leggi questo articolo e questo video di Udi Dahan dove affronta il problema in modo più dettagliato.
Andy,

6

Se capisco correttamente il flusso corrente è:

  1. Ricevi ed eventi (presumo tramite HTTP?)
  2. Richiedi una connessione dal pool.
  3. Inserisci l'evento nel DB
  4. Rilasciare la connessione al pool.

In tal caso, penso che la prima modifica al design sarebbe quella di smettere di avere il codice di gestione uniforme per restituire connessioni al pool in ogni evento. Creare invece un pool di thread / processi di inserimento che è da 1 a 1 con il numero di connessioni DB. Ciascuno conterrà una connessione DB dedicata.

Usando una sorta di coda simultanea, hai questi thread che estraggono i messaggi dalla coda simultanea e li inseriscono. In teoria non hanno mai bisogno di restituire la connessione al pool o richiederne una nuova, ma potrebbe essere necessario compilare la gestione nel caso in cui la connessione non funzioni. Potrebbe essere più semplice eliminare il thread / processo e avviarne uno nuovo.

Ciò dovrebbe eliminare efficacemente l'overhead del pool di connessioni. Ovviamente dovrai essere in grado di inviare almeno 1000 eventi / connessioni al secondo su ogni connessione. Potresti voler provare diversi numeri di connessioni poiché avere 500 connessioni funzionanti sulle stesse tabelle potrebbe creare controversie sul DB ma questa è una domanda completamente diversa. Un'altra cosa da considerare è l'uso di inserimenti batch, ovvero ogni thread estrae un numero di messaggi e li inserisce tutti in una volta. Inoltre, evitare di avere più connessioni nel tentativo di aggiornare le stesse righe.


5

ipotesi

Suppongo che il carico che descrivi sia costante, poiché questo è lo scenario più difficile da risolvere.

Suppongo anche che tu abbia un modo di eseguire carichi di lavoro attivati ​​e di lunga durata al di fuori del processo di applicazione web.

Soluzione

Supponendo di aver correttamente identificato il collo di bottiglia - la latenza tra il processo e il database Postgres - questo è il problema principale da risolvere. La soluzione deve tenere conto del vincolo di coerenza con altri clienti che desiderano leggere gli eventi non appena possibile dopo che sono stati ricevuti.

Per risolvere il problema di latenza, è necessario lavorare in modo da ridurre al minimo la quantità di latenza sostenuta per evento da archiviare. Questa è la cosa chiave che devi raggiungere se non sei disposto o in grado di cambiare hardware . Dato che sei sui servizi PaaS e non hai alcun controllo su hardware o rete, l'unico modo per ridurre la latenza per evento sarà con una sorta di scrittura in batch degli eventi.

Dovrai archiviare localmente una coda di eventi che viene scaricata e scritta periodicamente sul tuo db, una volta raggiunta una determinata dimensione o dopo un periodo di tempo trascorso. Sarà necessario un processo per monitorare questa coda per attivare il flush nel negozio. Dovrebbero esserci molti esempi su come gestire una coda simultanea che viene scaricata periodicamente nella tua lingua preferita - Ecco un esempio in C # , dal popolare sink di batch periodico della libreria di log di Serilog.

Questa risposta SO descrive il modo più veloce per scaricare i dati in Postgres, anche se richiederebbe che il tuo batch batch memorizzi la coda su disco, e probabilmente c'è un problema da risolvere lì quando il tuo disco scompare al riavvio in Heroku.

costrizione

Un'altra risposta ha già menzionato CQRS e questo è l'approccio corretto da risolvere per il vincolo. Si desidera idratare i modelli di lettura mentre ogni evento viene elaborato: un modello Mediatore può aiutare a incapsulare un evento e distribuirlo a più gestori in-process. Quindi un gestore può aggiungere l'evento al modello di lettura in memoria che i client possono interrogare e un altro gestore può essere responsabile dell'accodamento dell'evento per l'eventuale scrittura in batch.

Il vantaggio principale di CQRS è il disaccoppiamento dei modelli concettuali di lettura e scrittura, il che è un modo fantasioso per dire che scrivi in ​​un modello e leggi da un altro modello totalmente diverso. Per ottenere vantaggi in termini di scalabilità da CQRS, in genere è necessario assicurarsi che ciascun modello sia archiviato separatamente in modo ottimale per i suoi schemi di utilizzo. In questo caso, possiamo utilizzare un modello di lettura aggregato, ad esempio una cache Redis o semplicemente in memoria, per garantire che le nostre letture siano veloci e coerenti, mentre utilizziamo ancora il nostro database transazionale per scrivere i nostri dati.


3

Gli eventi arrivano più velocemente di quanto il pool di connessioni DB sia in grado di gestire

Questo è un problema se ogni processo necessitava di una connessione al database. Il sistema deve essere progettato in modo da disporre di un pool di lavoratori in cui ogni lavoratore necessita solo di una connessione al database e ogni lavoratore può elaborare più eventi.

La coda dei messaggi può essere utilizzata con tale progetto, sono necessari i produttori di messaggi che inviano gli eventi alla coda dei messaggi e i lavoratori (consumatori) elaborano i messaggi dalla coda.

Altri client potrebbero voler leggere eventi contemporaneamente

Questo vincolo è possibile solo se gli eventi sono archiviati nel database senza elaborazione (eventi non elaborati). Se gli eventi vengono elaborati prima di essere archiviati nel database, l'unico modo per ottenere gli eventi è dal database.

Se i clienti vogliono semplicemente eseguire una query sugli eventi non elaborati, suggerirei di utilizzare un motore di ricerca come Elastic Search. Otterrai anche l'API di query / ricerca gratuitamente.

Dato che sembra che interrogare gli eventi prima che vengano salvati nel database è importante per te, una soluzione semplice come Elastic Search dovrebbe funzionare. Fondamentalmente semplicemente memorizzi tutti gli eventi e non duplica gli stessi dati copiandoli nel database.

Il ridimensionamento della ricerca elastica è semplice, ma anche con la configurazione di base è abbastanza performante.

Quando è necessario l'elaborazione, il processo può ottenere gli eventi da ES, elaborarli e archiviarli nel database. Non so quale sia il livello di prestazioni richiesto da questa elaborazione, ma sarebbe completamente separato dalla query degli eventi da ES. Non dovresti avere comunque problemi di connessione, poiché puoi avere un numero fisso di worker e ognuno con una connessione al database.


2

Gli eventi 1k o 2k (5KB) al secondo non sono così tanto per un database se ha uno schema e un motore di archiviazione appropriati. Come suggerito da @eddyce, un master con uno o più slave può separare le query di lettura dalle operazioni di scrittura. L'uso di un numero inferiore di connessioni DB offre un throughput complessivo migliore.

Altri client potrebbero voler leggere eventi contemporaneamente

Per queste richieste, dovrebbero anche leggere dal master db in quanto vi sarebbe un ritardo di replica sugli slave di lettura.

Ho usato (Percona) MySQL con motore TokuDB per scritture di volume molto elevato. C'è anche il motore MyRocks basato su LSMtrees che è buono per i carichi di scrittura. Per entrambi questi motori e probabilmente anche PostgreSQL ci sono impostazioni per l'isolamento delle transazioni e il comportamento di sincronizzazione del commit che può aumentare notevolmente la capacità di scrittura. In passato abbiamo accettato fino a 1 dati persi che sono stati segnalati al client db come impegnati. In altri casi c'erano SSD alimentati a batteria per evitare perdite.

Si afferma che Amazon RDS Aurora in stile MySQL abbia un throughput di scrittura 6 volte maggiore con una replica a costo zero (simile agli slave che condividono un filesystem con il master). Il sapore Aurora PostgreSQL ha anche un diverso meccanismo di replica avanzato.


TBH qualsiasi database ben amministrato su hardware sufficiente dovrebbe essere in grado di far fronte a questo carico. Il problema di OP non sembra essere la prestazione del database ma la latenza della connessione; suppongo che Heroku sia un fornitore PaaS che sta vendendo loro un'istanza di Postgres in un'altra regione AWS.
amon

1

Abbandonerei Heroku tutti insieme, vale a dire, lascerei cadere un approccio centralizzato: più scritture che raggiungono il picco della massima connessione al pool è uno dei motivi principali per cui i cluster db sono stati inventati, principalmente perché non carichi la scrittura db (s) con richieste di lettura che possono essere eseguite da altri db nel cluster, proverei con una topologia master-slave, inoltre - come qualcun altro ha già detto, avere le tue installazioni db renderebbe possibile mettere a punto il tutto sistema per assicurarsi che il tempo di propagazione della query venga gestito correttamente.

In bocca al lupo

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.