Metodi efficienti per la memorizzazione di decine di milioni di oggetti per l'interrogazione, con un numero elevato di inserti al secondo?


15

Questa è fondamentalmente un'applicazione di registrazione / conteggio che sta contando il numero di pacchetti e contando il tipo di pacchetto, ecc. Su una rete di chat p2p. Ciò equivale a circa 4-6 milioni di pacchetti in un periodo di 5 minuti. E poiché prendo solo una "istantanea" di queste informazioni, rimuovo solo i pacchetti più vecchi di 5 minuti ogni cinque minuti. Quindi il massimo degli articoli che saranno in questa raccolta è da 10 a 12 milioni.

Poiché ho bisogno di effettuare 300 connessioni a diversi superpeer, è possibile che ogni pacchetto stia cercando di essere inserito almeno 300 volte (motivo per cui probabilmente tenere questi dati in memoria è l'unica opzione ragionevole).

Attualmente, sto usando un dizionario per memorizzare queste informazioni. Ma a causa della grande quantità di oggetti che sto cercando di archiviare, ho riscontrato problemi con l'heap di oggetti di grandi dimensioni e la quantità di utilizzo della memoria aumenta continuamente nel tempo.

Dictionary<ulong, Packet>

public class Packet
{
    public ushort RequesterPort;
    public bool IsSearch;
    public string SearchText;
    public bool Flagged;
    public byte PacketType;
    public DateTime TimeStamp;
}

Ho provato a utilizzare mysql, ma non è stato in grado di tenere il passo con la quantità di dati che devo inserire (verificando per accertarmi che non fosse un duplicato), e che era durante l'utilizzo delle transazioni.

Ho provato mongodb, ma l'utilizzo della cpu per questo era folle e non ha tenuto neanche.

Il mio problema principale si presenta ogni 5 minuti, perché rimuovo tutti i pacchetti più vecchi di 5 minuti e faccio uno "snapshot" di questi dati. Come sto usando le query LINQ per contare il numero di pacchetti contenenti un determinato tipo di pacchetto. Sto anche chiamando una query distinta () sui dati, in cui rimuovo 4 byte (indirizzo IP) dalla chiave del keyvaluepair e lo combino con il valore di richiesta richiedente nel valore del keyvalupair e lo uso per ottenere un numero distinto di colleghi di tutti i pacchetti.

L'applicazione attualmente copre circa 1,1 GB di utilizzo della memoria e quando viene chiamata un'istantanea può arrivare a raddoppiare l'utilizzo.

Ora questo non sarebbe un problema se avessi una quantità folle di RAM, ma al momento il vm su cui ho in esecuzione è limitato a 2 GB di RAM.

C'è qualche soluzione semplice?


È uno scenario molto dispendioso in termini di memoria e per di più stai usando un VM per eseguire l'applicazione, wow. Ad ogni modo, hai esplorato memcached per archiviare i pacchetti. Fondamentalmente è possibile eseguire memcached su un computer separato e l'applicazione può continuare a funzionare sulla VM stessa.

Dato che hai già provato sia MySQL che MongoDB, sembrerebbe che forse i requisiti della tua applicazione (se vuoi farlo nel modo giusto) impongano che hai semplicemente bisogno di più potenza. Se la tua applicazione è importante per te, rinforza il server. Potresti anche voler rivisitare il tuo codice di "eliminazione". Sono sicuro che potresti trovare un modo più ottimizzato di gestirlo, in quanto non rende la tua app inutilizzabile.
Matt Beckman,

4
Cosa ti dice il tuo profiler?
Jasonk,

Non otterrai nulla più veloce dell'heap locale. Il mio suggerimento sarebbe di invocare manualmente la raccolta dei rifiuti dopo l'eliminazione.
vartec,

@vartec - di fatto, contrariamente alla credenza popolare, invocare manualmente il Garbage Collector non garantisce in realtà, ... raccolta dei rifiuti immediata. Il GC potrebbe rimandare l'azione a un periodo successivo secondo il proprio algoritmo gc. Invocarlo ogni 5 minuti potrebbe anche aggiungere alla tensione, invece di alleviarlo. Sto solo dicendo;)
Jas

Risposte:


12

Invece di avere un dizionario e cercare quel dizionario per voci che sono troppo vecchie; hanno 10 dizionari. Ogni 30 secondi circa, crea un nuovo dizionario "attuale" e scarta il dizionario più vecchio senza effettuare alcuna ricerca.

Successivamente, quando scarti il ​​dizionario più vecchio, metti tutti gli oggetti vecchi su una coda FILO per dopo e, invece di usare "nuovo" per creare nuovi oggetti, togli un vecchio oggetto dalla coda FILO e usa un metodo per ricostruire il vecchio oggetto (a meno che la coda dei vecchi oggetti non sia vuota). Questo può evitare un sacco di allocazioni e un sacco di sovraccarico di garbage collection.


1
Partizionare per fascia oraria! Proprio quello che stavo per suggerire.
James Anderson,

Il problema è che dovrei interrogare tutti quei dizionari creati negli ultimi cinque minuti. Poiché ci sono 300 connessioni, lo stesso pacchetto arriverà a ciascuna almeno una volta. Quindi, al fine di non gestire lo stesso pacchetto più di una volta, devo conservarli per almeno 5 minuti.
Josh

1
Parte del problema con le strutture generiche è che non sono personalizzate per uno scopo specifico. Forse dovresti aggiungere un campo "nextItemForHash" e un campo "nextItemForTimeBucket" alla struttura del pacchetto e implementare la tua tabella hash e smettere di usare Dizionario. In questo modo puoi trovare rapidamente tutti i pacchetti che sono troppo vecchi e cercare solo una volta quando viene inserito un pacchetto (cioè prendi la tua torta e mangiale anche tu). Sarebbe anche utile per l'overhead di gestione della memoria (poiché "Dizionario" non starebbe allocando / liberando strutture di dati extra per la gestione del Dizionario).
Brendan,

@Josh il modo più veloce per determinare se hai visto qualcosa prima è un hashset . I set di hash time-sliced ​​sarebbero veloci e non avresti ancora bisogno di cercare per sfrattare vecchi oggetti. Se non l'hai mai visto prima, puoi memorizzarlo nel tuo dizione (sì).
Base


3

Il primo pensiero che mi viene in mente è il motivo per cui aspetti 5 minuti. Potresti fare le istantanee più spesso e ridurre così il grande sovraccarico che vedi al limite di 5 minuti?

In secondo luogo, LINQ è ottimo per il codice conciso, ma in realtà LINQ è lo zucchero sintattico su C # "normale" e non vi è alcuna garanzia che genererà il codice più ottimale. Come esercizio potresti provare a riscrivere i punti caldi senza LINQ, potresti non migliorare le prestazioni ma avrai un'idea più chiara di ciò che stai facendo e renderebbe più semplice il lavoro di profilazione.

Un'altra cosa da guardare sono le strutture di dati. Non so che cosa fai con i tuoi dati, ma potresti semplificare i dati archiviati in qualche modo? Potresti usare una stringa o un array di byte e quindi estrarre parti rilevanti da quegli elementi di cui hai bisogno? Potresti usare uno struct invece di una classe e persino fare qualcosa di male con stackalloc per mettere da parte la memoria ed evitare le corse GC?


1
Non utilizzare una stringa / array di byte, l'uso qualcosa di simile a un BitArray: msdn.microsoft.com/en-us/library/... per evitare di dover manualmente bit-Twiddle. Altrimenti, questa è una buona risposta, non c'è davvero un'opzione facile oltre a algoritmi migliori, più hardware o hardware migliore.
Ed James,

1
La cosa di cinque minuti è dovuta al fatto che queste 300 connessioni possono ricevere lo stesso pacchetto. Quindi devo tenere traccia di ciò che ho già gestito, e 5 minuti è il tempo necessario per la propagazione completa dei pacchetti su tutti i nodi di questa particolare rete.
Josh

3

Approccio semplice: prova memcached .

  • È ottimizzato per eseguire attività come questa.
  • Può riutilizzare la memoria di riserva su scatole meno occupate, non solo sulla tua casella dedicata.
  • Ha un meccanismo di scadenza della cache incorporato, che è pigro, quindi nessun singhiozzo.

Il rovescio della medaglia è che è basato sulla memoria e non ha alcuna persistenza. Se un'istanza non è attiva, i dati spariscono. Se hai bisogno di persistenza, serializza i dati da solo.

Approccio più complesso: prova Redis .

L'aspetto negativo è che è leggermente più complesso.


1
Memcached può essere suddiviso tra macchine per aumentare la quantità di RAM disponibile. Potresti avere un secondo server che serializza i dati sul filesystem in modo da non perdere nulla se una casella memcache non funziona. L'API Memcache è molto semplice da usare e funziona da qualsiasi lingua, permettendoti di utilizzare stack diversi in luoghi diversi.
Michael Shopsin

1

Non è necessario archiviare tutti i pacchetti per le query menzionate. Ad esempio: contatore del tipo di pacchetto:

Sono necessari due array:

int[] packageCounters = new int[NumberOfTotalTypes];
int[,] counterDifferencePerMinute = new int[6, NumberOfTotalTypes];

Il primo array tiene traccia di quanti pacchetti di diversi tipi. Il secondo array tiene traccia di quanti altri pacchetti sono stati aggiunti ogni minuto in modo tale da sapere quanti pacchetti devono essere rimossi ad ogni intervallo di minuti. Spero che tu possa dire che il secondo array è usato come una coda FIFO rotonda.

Pertanto, per ciascun pacchetto, vengono eseguite le seguenti operazioni:

packageCounters[packageType] += 1;
counterDifferencePerMinute[current, packageType] += 1;
if (oneMinutePassed) {
  current = (current + 1) % 6;
  for (int i = 0; i < NumberOfTotalTypes; i++) {
    packageCounters[i] -= counterDifferencePerMinute[current, i];
    counterDifferencePerMinute[current, i] = 0;
}

In qualsiasi momento, i contatori dei pacchetti possono essere recuperati dall'indice all'istante e non memorizziamo tutti i pacchetti.


Il motivo principale per cui devo archiviare i dati che faccio è il fatto che queste 300 connessioni potrebbero ricevere lo stesso pacchetto esatto. Quindi devo conservare ogni pacchetto visto per almeno cinque minuti per essere sicuro di non gestirli / contarli più di una volta. È a questo che serve l'ulong per la chiave del dizionario.
Josh

1

(So ​​che questa è una vecchia domanda, ma mi sono imbattuto in esso mentre cercavo una soluzione a un problema simile in cui il passaggio della garbage collection di seconda generazione stava mettendo in pausa l'app per diversi secondi, quindi registrando per altre persone in situazioni simili).

Usa una struttura piuttosto che una classe per i tuoi dati (ma ricorda che viene trattato come un valore con semantica pass-by-copy). Questo elimina un livello di ricerca che il gc deve eseguire ogni mark mark.

Usa matrici (se conosci la dimensione dei dati che stai memorizzando) o Elenco - che utilizza matrici internamente. Se hai davvero bisogno di un accesso casuale veloce, usa un dizionario di indici di array. Questo elimina un altro paio di livelli (o una dozzina o più se stai usando un SortedDictionary) per cui gc deve cercare.

A seconda di ciò che stai facendo, la ricerca di un elenco di strutture potrebbe essere più veloce della ricerca nel dizionario (a causa della localizzazione della memoria) - profilo per la tua particolare applicazione.

La combinazione di struct & list riduce in modo significativo sia l'utilizzo della memoria che le dimensioni della spazzatura del garbage collector.


Ho un recente esperimento, che genera raccolte e dizionari su disco più velocemente, usando sqlite github.com/modma/PersistenceCollections
ModMa
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.