Dec 1st, 2021: [fr] L'impact du filtrage de champs sur les performances d'Elasticsearch

:information_source: This post is also available in English: Dec 1st, 2021: [en] The impact of Elasticsearch source filtering on performance

Il y a quelques années, je voulais récupérer de nombreux documents d'Elasticsearch, et ça allait prendre une heure. Quand j'ai mentionné ça à un collègue, il m'a immédiatement demandé : « As-tu utilisé _source pour ne récupérer que les champs dont tu as besoin ? ». Je ne l'avais pas fait, et appliquer son conseil a considérablement accéléré mes requêtes.

Est-ce que ce conseil est toujours valable ? S'il l'est, à quelles améliorations s'attendre ? Je ne sais pas : c'est pour ça que j'écris ce billet.

Pagination

Supposons qu'on veuille récupérer 25 000 documents depuis Elasticsearch. Le faire en une requête pourrait surcharger Elasticsearch, échouer avec un timeout ou même être bloqué par un proxy. À la place, il faut effectuer plusieurs petites requêtes, chacune d'entre elles récupérant 1 000 documents à la fois grâce aux paramètres size et search_after.

Mais entre deux requêtes, les données sous-jacentes pourraient avoir changé ! C'est là que l'API dite point in time nous aide: grâce à elle, on peut faire comme si rien ne changeait et que le temps était gelé entre deux requêtes.

Rally

Pour benchmarker Elasticsearch, nous allons utiliser Rally, conçu pour donner des résultats fiables tout en étant facile à utiliser.

Avec Rally, des voitures (clusters Elasticsearch) font des courses (benchmarks) sur des pistes (étapes comme ingestion de données et requêtes). Dans ce billet, nous utiliserons la piste (ou track) PMC. Voilà à quoi ses documents ressemblent :

{
 "name": "3_Biotech_2015_Dec_13_5(6)_1007-1019",
 "journal": "3 Biotech",
 "date": "2015 Dec 13",
 "volume": "5(6)",
 "issue": "1007-1019",
 "accession": "PMC4624133",
 "timestamp": "2015-10-30 20:08:11",
 "pmid": "",
 "body": "\n==== Front\n3 Biotech3 Biotech3 Biotech2190-572X2190-5738Springer ..."
}

Ce qui nous intéresse ici, c'est que body est de loin le champ contenant le plus de données, donc ne pas le demander devrait donner des requêtes plus performantes. Après avoir cloné le dépôt elastic/rally-tracks, j'ai installé Rally, appliqué cette modification et modifié le challenge append-no-conflicts pour tester la pagination.

Pour ce faire, je me suis inspiré de la documentation de Rally pour ajouter le fichier pmc/operations/pagination.json:

{
  "name": "search-after-with-pit-default",
  "operation-type": "composite",
  "requests": [
    {
      "stream": [
        {
          "operation-type": "open-point-in-time",
          "name": "open-pit",
          "index": "pmc"
        },
        {
          "operation-type": "paginated-search",
          "name": "paginate",
          "index": "pmc",
          "with-point-in-time-from": "open-pit",
          "pages": 25,
          "results-per-page": 1000,
          "body": {
            "sort": [
              {"timestamp": "desc"}
            ],
            "query": {
              "match_all": {}
            }
          }        
        },
        {
          "name": "close-pit",
          "operation-type": "close-point-in-time",
          "with-point-in-time-from": "open-pit"
        }
      ]
    }
  ]
}

Ensuite, j'ai utilisé cette nouvelle opération en modifiant le challenge append-no-conflict dans pmc/challenges/default.json:

{
  "operation": "search-after-with-pit-default",
  "warmup-iterations": 10,
  "iterations": 20
}

J'ai aussi supprimé toutes les opérations après wait-until-merges-finish comme on n'est qu'intéressés par la pagination ici. Voilà le commit où je fais ça.

Mise en place du benchmark

Pour obtenir des résultats reproductibles et ne concernant qu'Elasticsearch (et pas l'environnement autour), j'ai pris les précautions suivantes :

  • Le générateur de charge (i3en.6xlarge) et le nœud Elasticsearch (m5d.4xlarge) sont deux machines différentes dans le même data center, ce qui évite un goulot d'étranglement côté réseau ou client.
  • Avant de lancer les benchmarks, j'ai vidé le cache Linux et lancé la commande TRIM sur les SSDs.
  • Je me suis assuré qu'aucune mise à jour ne tournait en tâche de fond

esrally race

Si vous suivez depuis chez vous, vous pouvez lancer le benchmark comme ceci, puis attendre quelques minutes.

$ esrally race --distribution-version=7.15.2 --track-path=~/src/rally-tracks/pmc

    ____        ____
   / __ \____ _/ / /_  __
  / /_/ / __ `/ / / / / /
 / _, _/ /_/ / / / /_/ /
/_/ |_|\__,_/_/_/\__, /
                /____/

[INFO] Race id is [79992a48-57cd-41fb-9549-68f71354c5dd]
[INFO] Preparing for race ...
[INFO] Racing on track [pmc], challenge [append-no-conflicts] and car ['defaults'] with version [7.15.2].

Running put-settings                      [100% done]
Running delete-index                      [100% done]
Running create-index                      [100% done]
Running check-cluster-health              [100% done]
Running index-append                      [100% done]
Running refresh-after-index               [100% done]
Running force-merge                       [100% done]
Running refresh-after-force-merge         [100% done]
Running wait-until-merges-finish          [100% done]
Running search-after-with-pit-default     [100% done]

------------------------------------------------------
    _______             __   _____
   / ____(_)___  ____ _/ /  / ___/_________  ________
  / /_  / / __ \/ __ `/ /   \__ \/ ___/ __ \/ ___/ _ \
 / __/ / / / / / /_/ / /   ___/ / /__/ /_/ / /  /  __/
/_/   /_/_/ /_/\__,_/_/   /____/\___/\____/_/   \___/
------------------------------------------------------

|                    Metric |                          Task |       Value |   Unit |
|--------------------------:|------------------------------:|------------:|-------:|
...
|   50th percentile latency | search-after-with-pit-default |     3314.37 |     ms |
|   90th percentile latency | search-after-with-pit-default |     3366.62 |     ms |
|  100th percentile latency | search-after-with-pit-default |     3400.26 |     ms |
|                error rate | search-after-with-pit-default |           0 |      % |

Ouvrir/fermer le point-in-time et chercher 25 000 documents a pris environ 3,3 secondes.

Comparer différentes configurations de _source

Maintenant que nous avons cette valeur de 3,3 secondes pour récupérer toutes les données (dont le champ volumineux body), comparons différentes options en changeant nos requêtes dans pmc/operations/pagination.json:

  1. Filtrer l'ensemble du document avec _source: false
  2. Ne lire que le petit champ journal
  3. Ne lire que le champ volumineux body

Pour ne lire qu'un champ, nous testons les méthodes _source and fields. Voici une table montrant la latence médiane pour chaque option :

Méthode Latence médiane
Pas de filtrage 3.3s
"_source": false 1.1s
journal seulement via _source 1.3s
journal seulement via fields 1.3s
body seulement via _source 3.7s
body seulement via fields 4.8s

Logiquement, demander peu ou pas de données est plus rapide, ici 3 fois plus ! On peut donc recommander le filtrage pour améliorer les performances de la pagination.

Par contre, ne demander que body est plus lent que de demander le document complet, que ce soit avec _source ou fields. Quand un champ représente l'essentiel d'un document, filtrer semble être une mauvaise idée. (Et passer par fields était pire !)

Recommandations

Au vu de ces résultats, voilà ce que nous pouvons recommander en pratique pour la pagination :

  1. Si vous n'utilisez qu'une petite partie d'un document, alors le filtrage ne peut qu'aider, en transmettant moins de données, demandant moins de compression/décompression et moins d'encodage/décodage JSON.
  2. Si vous utilisez la plupart du document, alors il faudra mesurer l'exécution de votre code. En pratique, le client peut être moins puissant. Dans ce cas, la compression et le décodage JSON peuvent devenir des goulots d'étranglement: il faut alors envisager de désactiver la compression et d'utiliser un parser de JSON plus rapide (par exemple orjson en Python).

Travaux futurs

Malgré les précautions prises, j'ai quand même utilisé des instances AWS. Utiliser des serveurs dédiés à la place (comme nous le faisons pour les benchmarks Elasticsearch) pourrait donner des résultats encore plus précis et plus fiables.

Un autre point important : ici les documents étaient assez petits pour être dans le cache mémoire, donc Elasticsearch n'avait rien à lire depuis le disque après la phase dite d'échauffement. Pour des jeu de données plus importants, lire depuis le disque pourrait devenir le goulot d'étranglement. À ce moment, le filtrage peut aider Lucene à lire depuis le disque plus efficacement grâce à l'optimisation dite des doc values. Mais ce serait à confirmer avec un benchmark !

Et enfin, comme mentionné dans les recommandations, en pratique il est facile d'introduire de nouveaux goulots d'étranglement accidentellement : utilisez un profileur statistique tel que py-spy en Python pour savoir ce qui prend du temps !

2 Likes

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.