Des méthodes efficaces pour stocker des dizaines de millions d'objets à interroger, avec un nombre élevé d'insertions par seconde?

15

Il s'agit essentiellement d'une application de journalisation / comptage qui compte le nombre de paquets et le type de paquet, etc. sur un réseau de discussion p2p. Cela équivaut à environ 4 à 6 millions de paquets sur une période de 5 minutes. Et parce que je ne prends qu'un "instantané" de ces informations, je ne supprime que les paquets de plus de 5 minutes toutes les cinq minutes. Donc, le maximum d'articles qui seront dans cette collection est de 10 à 12 millions.

Étant donné que je dois établir 300 connexions avec différents super-utilisateurs, il est possible que chaque paquet essaie d'être inséré au moins 300 fois (c'est probablement la raison pour laquelle la conservation de ces données en mémoire est la seule option raisonnable).

Actuellement, j'utilise un dictionnaire pour stocker ces informations. Mais en raison de la grande quantité d'articles que j'essaie de stocker, je rencontre des problèmes avec le tas d'objets volumineux et la quantité d'utilisation de la mémoire augmente continuellement au fil du temps.

Dictionary<ulong, Packet>

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

J'ai essayé d'utiliser mysql, mais il n'a pas pu suivre la quantité de données que j'ai besoin d'insérer (tout en vérifiant qu'il ne s'agissait pas d'un doublon), et c'était lors de l'utilisation de transactions.

J'ai essayé mongodb, mais l'utilisation du processeur pour cela était folle et je ne l'ai pas gardée non plus.

Mon principal problème survient toutes les 5 minutes, car je supprime tous les paquets qui datent de plus de 5 minutes et je prends un "instantané" de ces données. Comme j'utilise des requêtes LINQ pour compter le nombre de paquets contenant un certain type de paquet. J'appelle également une requête distinct () sur les données, où je supprime 4 octets (adresse IP) de la clé de keyvaluepair, et la combine avec la valeur requestingport dans la valeur de keyvalupair et l'utilise pour obtenir un nombre distinct de pairs de tous les paquets.

L'application oscille actuellement autour de 1,1 Go d'utilisation de la mémoire, et lorsqu'un instantané est appelé, il peut aller jusqu'à doubler l'utilisation.

Maintenant, ce ne serait pas un problème si j'ai une quantité folle de RAM, mais le vm sur lequel je tourne est limité à 2 Go de RAM pour le moment.

Existe-t-il une solution simple?

Josh
la source
Son scénario très gourmand en mémoire et en plus de cela, vous utilisez un VM pour exécuter l'application, wow. Quoi qu'il en soit, avez-vous exploré memcached pour stocker les paquets. Fondamentalement, vous pouvez exécuter memcached sur une machine distincte et l'application peut continuer à s'exécuter sur le vm lui-même.
Comme vous avez déjà essayé MySQL et MongoDB, il semblerait que les exigences de votre application (si vous voulez le faire correctement) dictent que vous avez simplement besoin de plus de puissance. Si votre application est importante pour vous, renforcez le serveur. Vous pouvez également revoir votre code de «purge». Je suis sûr que vous pourriez trouver un moyen plus optimisé de gérer cela, dans la mesure où cela ne rend pas votre application inutilisable.
Matt Beckman
4
Que vous dit votre profileur?
jasonk
Vous n'obtiendrez rien de plus rapide que le tas local. Ma suggestion serait d'appeler manuellement la collecte des ordures après la purge.
vartec
@vartec - en fait, contrairement à la croyance populaire, invoquer manuellement le ramasse-miettes ne garantit pas en fait immédiat, eh bien ... la collecte des ordures. Le GC peut reporter l'action à une période ultérieure selon son propre algorithme gc. L'invoquer toutes les 5 minutes pourrait même augmenter la tension au lieu de la soulager. Je dis juste;)
Jas

Réponses:

12

Au lieu d'avoir un dictionnaire et de rechercher dans ce dictionnaire des entrées trop anciennes; avoir 10 dictionnaires. Toutes les 30 secondes environ, créez un nouveau dictionnaire "actuel" et jetez le plus ancien dictionnaire sans aucune recherche.

Ensuite, lorsque vous supprimez le dictionnaire le plus ancien, placez tous les anciens objets dans une file d'attente FILO pour plus tard, et au lieu d'utiliser "nouveau" pour créer de nouveaux objets, retirez un vieil objet de la file d'attente FILO et utilisez une méthode pour reconstruire l'ancien objet (sauf si la file d'attente des anciens objets est vide). Cela peut éviter beaucoup d'allocations et beaucoup de frais généraux de collecte de déchets.

Brendan
la source
1
Partitionnement par tranche de temps! Juste ce que j'allais suggérer.
James Anderson
Le problème avec ceci est que je devrais interroger tous ces dictionnaires qui ont été créés au cours des cinq dernières minutes. Comme il y a 300 connexions, le même paquet va arriver à chacune au moins une fois. Donc, pour ne pas gérer plus d'une fois le même paquet, je dois les conserver pendant au moins 5 minutes.
Josh
1
Une partie du problème des structures génériques est qu'elles ne sont pas personnalisées dans un but spécifique. Vous devriez peut-être ajouter un champ "nextItemForHash" et un champ "nextItemForTimeBucket" à votre structure de paquets et implémenter votre propre table de hachage, et cesser d'utiliser Dictionary. De cette façon, vous pouvez trouver rapidement tous les paquets qui sont trop anciens et ne rechercher qu'une seule fois lorsqu'un paquet est inséré (c'est-à-dire avoir votre gâteau et le manger aussi). Cela aiderait également à la gestion de la mémoire (car "Dictionnaire" ne serait pas allouer / libérer des structures de données supplémentaires pour la gestion de Dictionnaire).
Brendan
@Josh, le moyen le plus rapide de déterminer si vous avez déjà vu quelque chose est un hachage . Les ensembles de hachage découpés dans le temps seraient rapides et vous n'auriez toujours pas besoin de rechercher pour expulser les anciens éléments. Si vous ne l'avez pas vu auparavant, vous pouvez le stocker dans votre dictionnaire (y / ies).
Basic
3

La première pensée qui vous vient à l'esprit est la raison pour laquelle vous attendez 5 minutes. Pourriez-vous faire les clichés plus souvent et ainsi réduire la grosse surcharge que vous voyez à la limite de 5 minutes?

Deuxièmement, LINQ est idéal pour le code concis, mais en réalité LINQ est du sucre syntaxique sur C # "normal" et il n'y a aucune garantie qu'il générera le code le plus optimal. En tant qu'exercice, vous pouvez essayer de réécrire les points chauds sans LINQ, vous n'améliorerez peut-être pas les performances, mais vous aurez une idée plus claire de ce que vous faites et cela faciliterait le travail de profilage.

Une autre chose à considérer est la structure des données. Je ne sais pas ce que vous faites de vos données, mais pourriez-vous simplifier les données que vous stockez de quelque manière que ce soit? Pourriez-vous utiliser une chaîne ou un tableau d'octets, puis extraire les parties pertinentes de ces éléments selon vos besoins? Pourriez-vous utiliser une structure au lieu d'une classe et même faire quelque chose de mal avec stackalloc pour mettre de côté la mémoire et éviter les exécutions GC?

Steve
la source
1
N'utilisez pas un tableau de chaînes / octets, utilisez quelque chose comme un BitArray: msdn.microsoft.com/en-us/library/… pour éviter d'avoir à bit-twiddle manuellement. Sinon, c'est une bonne réponse, il n'y a pas vraiment une option facile autre que de meilleurs algorithmes, plus de matériel ou un meilleur matériel.
Ed James
1
La chose de cinq minutes est due au fait que ces 300 connexions peuvent recevoir le même paquet. Je dois donc garder une trace de ce que j'ai déjà géré, et 5 minutes est le temps nécessaire pour que les paquets se propagent entièrement à tous les nœuds de ce réseau particulier.
Josh
3

Approche simple: essayez memcached .

  • Il est optimisé pour exécuter des tâches comme celle-ci.
  • Il peut réutiliser la mémoire disponible sur des boîtiers moins occupés, pas seulement sur votre box dédiée.
  • Il a un mécanisme d'expiration de cache intégré, qui est paresseux donc pas de hoquet.

L'inconvénient est qu'il est basé sur la mémoire et n'a aucune persistance. Si une instance est arrêtée, les données disparaissent. Si vous avez besoin de persistance, sérialisez les données vous-même.

Approche plus complexe: essayez Redis .

  • Il est optimisé pour exécuter des tâches comme celle-ci.
  • Il a un mécanisme d'expiration de cache intégré .
  • Il se balance / éclate facilement.
  • Il a de la persistance.

L'inconvénient est qu'il est légèrement plus complexe.

9000
la source
1
Memcached peut être réparti sur plusieurs machines pour augmenter la quantité de RAM disponible. Vous pourriez avoir un deuxième serveur sérialisant les données vers le système de fichiers afin que vous ne perdiez rien si une boîte memcache tombe en panne. L'API Memcache est très simple à utiliser et fonctionne dans n'importe quelle langue vous permettant d'utiliser différentes piles à différents endroits.
Michael Shopsin
1

Vous n'avez pas besoin de stocker tous les packages pour les requêtes que vous avez mentionnées. Par exemple - compteur de type de package:

Vous avez besoin de deux tableaux:

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

Le premier tableau conserve la trace du nombre de packages dans différents types. Le deuxième tableau conserve la trace du nombre de packages supplémentaires ajoutés toutes les minutes de manière à savoir combien de packages doivent être supprimés à chaque minute d'intervalle. J'espère que vous pouvez dire que le deuxième tableau est utilisé comme une file d'attente FIFO ronde.

Ainsi, pour chaque package, les opérations suivantes sont effectuées:

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;
}

À tout moment, les compteurs de packages peuvent être récupérés par l'index instantanément et nous ne stockons pas tous les packages.

Codisme
la source
La principale raison de devoir stocker les données que je fais, est le fait que ces 300 connexions peuvent recevoir le même paquet exact. J'ai donc besoin de garder chaque paquet vu pendant au moins cinq minutes afin de m'assurer de ne pas les gérer / les compter plus d'une fois. C'est à cela que sert l'ulong de la clé de dictionnaire.
Josh
1

(Je sais que c'est une vieille question, mais je l'ai rencontrée en cherchant une solution à un problème similaire où la passe de collecte des ordures de deuxième génération suspendait l'application pendant plusieurs secondes, donc l'enregistrement pour d'autres personnes dans une situation similaire).

Utilisez une structure plutôt qu'une classe pour vos données (mais rappelez-vous qu'elle est traitée comme une valeur avec une sémantique passe-par-copie). Cela supprime un niveau de recherche que le GC doit faire à chaque passe.

Utilisez des tableaux (si vous connaissez la taille des données que vous stockez) ou List - qui utilise des tableaux en interne. Si vous avez vraiment besoin d'un accès aléatoire rapide, utilisez un dictionnaire d'indices de tableaux. Cela supprime un autre couple de niveaux (ou une douzaine ou plus si vous utilisez un SortedDictionary) pour que le GC doive rechercher.

Selon ce que vous faites, la recherche d'une liste de structures peut être plus rapide que la recherche de dictionnaire (en raison de la localisation de la mémoire) - le profil de votre application particulière.

La combinaison de struct & list réduit à la fois l'utilisation de la mémoire et la taille du balayage du ramasse-miettes.

Malcolm
la source
J'ai une expérience récente, qui génère des collections et des dictionnaires sur disque aussi rapidement, en utilisant sqlite github.com/modma/PersistenceCollections
ModMa