web-dev-qa-db-fra.com

MongoDB Java API performance de lecture lente

Nous lisons dans une MongoDB locale tous les documents d’une collection et la performance n’est pas très brillante.

Nous devons vider toutes les données, ne vous inquiétez pas, mais croyez-en que c'est vraiment nécessaire et qu'il n'y a pas de solution de contournement possible.

Nous avons 4 millions de documents qui ressemblent à:

{
    "_id":"4d094f58c96767d7a0099d49",
    "exchange":"NASDAQ",
    "stock_symbol":"AACC",
    "date":"2008-03-07",
    "open":8.4,
    "high":8.75,
    "low":8.08,
    "close":8.55,
    "volume":275800,
    "adj close":8.55
}

Et nous utilisons ceci pour le code trivial à lire:

MongoClient mongoClient = MongoClients.create();
MongoDatabase database = mongoClient.getDatabase("localhost");
MongoCollection<Document> collection = database.getCollection("test");

MutableInt count = new MutableInt();
long start = System.currentTimeMillis();
collection.find().forEach((Block<Document>) document -> count.increment() /* actually something more complicated */ );
long start = System.currentTimeMillis();

Nous lisons toute la collection en 16 secondes (250 000 lignes/sec), ce qui n’est vraiment pas impressionnant du tout avec de petits documents. N'oubliez pas que nous voulons charger 800 millions de lignes. Aucun agrégat, carte réduite ou similaire n'est possible.

Est-ce aussi rapide que MongoDB ou existe-t-il d'autres moyens de charger des documents plus rapidement (autres techniques, déplacement de Linux, plus de mémoire vive, paramètres ...)? 

23
ic3

Vous n'avez pas spécifié votre cas d'utilisation, il est donc très difficile de vous dire comment ajuster votre requête. (I.e: Qui voudrait charger 800mil à la fois juste pour compter?).

Compte tenu de votre schéma, je pense que vos données sont presque en lecture seule et que votre tâche est liée à l’agrégation de données. 

Votre travail actuel consiste simplement à lire les données (votre pilote lira probablement par lot), puis à vous arrêter, puis à effectuer des calculs (ouais, un enveloppeur int est utilisé pour augmenter davantage le temps de traitement), puis recommencer. Ce n'est pas une bonne approche. La base de données n’est pas aussi rapide que par magie si vous n’y accédez pas correctement.

Si le calcul n'est pas trop complexe, je vous suggère d'utiliser le framework d'agrégation au lieu de tout charger dans votre RAM.

Vous devriez envisager quelque chose pour améliorer votre agrégation:

  1. Divisez votre ensemble de données en un ensemble plus petit. (Par exemple: Partition par date, partition par exchange...). Ajoutez un index pour prendre en charge cette partition et opérez l'agrégation sur une partition, puis combinez le résultat (approche typique divide-n-conquer)
  2. Projetez uniquement les champs nécessaires
  3. Filtrer les documents inutiles (si possible)
  4. Autorisez l'utilisation de disque si vous ne pouvez pas effectuer d'agrégation sur la mémoire (si vous atteignez la limite de 100 Mo par pipeline).
  5. Utilisez le pipeline intégré pour accélérer vos calculs (par exemple: $count pour votre exemple)

Si votre calcul est trop complexe et que vous ne pouvez pas l'exprimer avec une structure d'agrégation, utilisez mapReduce . Il fonctionne sur le processus mongod et les données n'ont pas besoin d'être transférées sur le réseau vers votre mémoire.

Mis à jour

Alors, on dirait que vous voulez effectuer un traitement OLAP et que vous êtes bloqué à l'étape ETL.

Vous n'avez pas besoin et devez éviter de charger toutes les données OLTP dans OLAP à chaque fois. Il suffit de charger les nouvelles modifications dans votre entrepôt de données. Alors le premier chargement/vidage des données prend plus de temps, ce qui est normal et acceptable.

Lors du premier chargement, prenez en compte les points suivants:

  1. Divide-N-Conquer divise encore une fois vos données en un jeu de données plus petit (avec un prédicat comme date/échange/étiquette de stock ...)
  2. Faites un calcul parallèle, puis combinez votre résultat (vous devez partitionner votre jeu de données correctement)
  3. Effectuez le calcul par lots au lieu du traitement dans forEach: chargez la partition de données, puis calculez au lieu de calculer un par un.
12

Ce que je pense que je devrais faire dans votre cas était une solution simple et simultanément, un moyen efficace est de maximiser le débit global en utilisant parallelCollectionScan

Permet aux applications d'utiliser plusieurs curseurs parallèles lors de la lecture de tous les documents d'une collection, augmentant ainsi le débit. Le La commande parallelCollectionScan renvoie un document contenant un fichier tableau d'informations de curseur.

Chaque curseur donne accès au retour d’un ensemble partiel de documents d'une collection. Une itération de chaque curseur retourne chaque document dans la collection. Les curseurs ne contiennent pas les résultats du commande de base de données. Le résultat de la commande de base de données identifie le fichier curseurs, mais ne contient ni ne constitue les curseurs.

Un exemple simple avec parallelCollectionScan devrait être quelque chose comme celui-ci

 MongoClient mongoClient = MongoClients.create();
 MongoDatabase database = mongoClient.getDatabase("localhost");
 Document commandResult = database.runCommand(new Document("parallelCollectionScan", "collectionName").append("numCursors", 3));
2

Premièrement, comme l'a commenté @ xtreme-biker, les performances dépendent grandement de votre matériel. Plus précisément, mon premier conseil serait de vérifier si vous utilisez une machine virtuelle ou un hôte natif. Dans mon cas, avec un CentOS VM sur un i7 avec un lecteur SDD, je peux lire 123 000 documents par seconde, mais le code identique qui s'exécute sur l'hôte Windows du même lecteur lit jusqu'à 387 000 documents par seconde.

Ensuite, supposons que vous ayez vraiment besoin de lire la collection complète. Cela signifie que vous devez effectuer une analyse complète. Et supposons que vous ne pouvez pas modifier la configuration de votre serveur MongoDB mais seulement optimiser votre code.

Puis tout se résume à quoi

collection.find().forEach((Block<Document>) document -> count.increment());

fait réellement.

Un rapide déroulement de MongoCollection.find () montre qu’il fait ceci:

ReadPreference readPref = ReadPreference.primary();
ReadConcern concern = ReadConcern.DEFAULT;
MongoNamespace ns = new MongoNamespace(databaseName,collectionName);
Decoder<Document> codec = new DocumentCodec();
FindOperation<Document> fop = new FindOperation<Document>(ns,codec);
ReadWriteBinding readBinding = new ClusterBinding(getCluster(), readPref, concern);
QueryBatchCursor<Document> cursor = (QueryBatchCursor<Document>) fop.execute(readBinding);
AtomicInteger count = new AtomicInteger(0);
try (MongoBatchCursorAdapter<Document> cursorAdapter = new MongoBatchCursorAdapter<Document>(cursor)) {
    while (cursorAdapter.hasNext()) {
        Document doc = cursorAdapter.next();
        count.incrementAndGet();
    }
}

Ici, la FindOperation.execute() est assez rapide (moins de 10 ms) et la plupart du temps est passée dans la boucle while, et plus précisément dans la méthode privée QueryBatchCursor.getMore()

getMore() appelle DefaultServerConnection.command() et son temps est consommé essentiellement en deux opérations: 1) extraire des données de chaîne du serveur et 2) convertir des données de chaîne en BsonDocument.

Il s’avère que Mongo est assez intelligent en ce qui concerne le nombre d’allers-retours sur le réseau qu’il va effectuer pour obtenir un ensemble de résultats volumineux. Il va d'abord extraire 100 résultats avec une commande firstBatch, puis extraire des lots plus volumineux, nextBatch étant la taille du lot en fonction de la taille de la collection, dans la limite d'une limite.

Donc, sous le bois, quelque chose comme cela arrivera pour aller chercher le premier lot.

ReadPreference readPref = ReadPreference.primary();
ReadConcern concern = ReadConcern.DEFAULT;
MongoNamespace ns = new MongoNamespace(databaseName,collectionName);
FieldNameValidator noOpValidator = new NoOpFieldNameValidator();
DocumentCodec payloadDecoder = new DocumentCodec();
Constructor<CodecProvider> providerConstructor = (Constructor<CodecProvider>) Class.forName("com.mongodb.operation.CommandResultCodecProvider").getDeclaredConstructor(Decoder.class, List.class);
providerConstructor.setAccessible(true);
CodecProvider firstBatchProvider = providerConstructor.newInstance(payloadDecoder, Collections.singletonList("firstBatch"));
CodecProvider nextBatchProvider = providerConstructor.newInstance(payloadDecoder, Collections.singletonList("nextBatch"));
Codec<BsonDocument> firstBatchCodec = fromProviders(Collections.singletonList(firstBatchProvider)).get(BsonDocument.class);
Codec<BsonDocument> nextBatchCodec = fromProviders(Collections.singletonList(nextBatchProvider)).get(BsonDocument.class);
ReadWriteBinding readBinding = new ClusterBinding(getCluster(), readPref, concern);
BsonDocument find = new BsonDocument("find", new BsonString(collectionName));
Connection conn = readBinding.getReadConnectionSource().getConnection();

BsonDocument results = conn.command(databaseName,find,noOpValidator,readPref,firstBatchCodec,readBinding.getReadConnectionSource().getSessionContext(), true, null, null);
BsonDocument cursor = results.getDocument("cursor");
long cursorId = cursor.getInt64("id").longValue();

BsonArray firstBatch = cursor.getArray("firstBatch");

Ensuite, la cursorId est utilisée pour extraire chaque prochain lot.

À mon avis, le "problème" avec l'implémentation du pilote est que le décodeur String to JSON est injecté, mais pas le JsonReader - dans lequel la méthode decode () s'appuie -. C’est ainsi que même jusqu’à com.mongodb.internal.connection.InternalStreamConnection où vous êtes déjà proche de la communication par socket.

Par conséquent, je pense qu’il n’ya pratiquement rien que vous puissiez faire pour améliorer MongoCollection.find() à moins que vous n’alliez aussi loin que InternalStreamConnection.sendAndReceiveAsync()

Vous ne pouvez pas réduire le nombre d'allers-retours et vous ne pouvez pas changer la façon dont la réponse est convertie en BsonDocument. Non sans contourner le pilote et écrire votre propre client, ce qui, j'en doute, est une bonne idée.

P.D. Si vous voulez essayer une partie du code ci-dessus, vous aurez besoin de la méthode getCluster () qui nécessite un piratage de base dans mongo-Java-driver .

private Cluster getCluster() {
    Field cluster, delegate;
    Cluster mongoCluster = null;
    try {
        delegate = mongoClient.getClass().getDeclaredField("delegate");
        delegate.setAccessible(true);
        Object clientDelegate = delegate.get(mongoClient);
        cluster = clientDelegate.getClass().getDeclaredField("cluster");
        cluster.setAccessible(true);
        mongoCluster = (Cluster) cluster.get(clientDelegate);
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
        System.err.println(e.getClass().getName()+" "+e.getMessage());
    }
    return mongoCluster;
}
1
Serg M Ten

À mon avis, vous traitez environ 50 Mio/s (250 k lignes/s * 0,2 Kio/lignes). Cela entre à la fois dans le lecteur et dans le goulot d’étranglement du réseau. Quel type de stockage utilise MongoDB? Quel type de bande passante avez-vous entre le client et le serveur MongoDB? Avez-vous essayé de co-localiser le serveur et le client sur un réseau haute vitesse (> = 10 gib/s) avec une latence minimale (<1,0 ms)? N'oubliez pas que si vous utilisez un fournisseur d'informatique en nuage tel qu'AWS ou GCP, des goulots d'étranglement en matière de virtualisation s'ajouteront aux gisements physiques. 

Vous avez demandé quels paramètres pourraient vous aider. Vous pouvez essayer de modifier les paramètres de compression sur connection et sur collection (les options sont "none", snappy et zlib). Même si aucune amélioration n'est apportée à snappy, voir la différence que le réglage fait (ou ne fait pas) peut aider à déterminer quelle partie du système est la plus sollicitée.

Java ne présente pas de bonnes performances en termes de compression des chiffres par rapport à C++ ou Python. Vous pouvez donc envisager de réécrire cette opération particulière dans l’un de ces langages, puis de l’intégrer à votre code Java. Je vous suggère de tester en boucle les données en Python et de les comparer au même en Java. 

0
Old Pro