Optimierung langsamer Elasticsearch-Abfragen: Best Practices für die Leistungsoptimierung

Diagnostizieren und verbessern Sie langsame Elasticsearch-Abfragen durch bessere Abfragestruktur, Paginierung, Caching, Mappings und die Profile-API.

Optimierung langsamer Elasticsearch-Abfragen: Best Practices für die Leistungsoptimierung

Langsame Elasticsearch-Abfragen haben in der Regel eine von vier Ursachen: Die Abfrage fordert zu viele Daten an, das Mapping macht die Abfrage teuer, der Cluster hat zu wenig Ressourcen, oder die Anwendung wiederholt teure Suchvorgänge, die gecached oder neu gestaltet werden sollten. Die Lösung hängt davon ab, welche Ursache zutrifft.

Bevor Sie alles umschreiben, erfassen Sie eine echte langsame Anfrage mit ihrem Index, Filtern, Sortierung, Aggregationen, Seitentiefe, Antwortgröße und Zeitmessung. Eine Dashboard-Aggregation, eine Autocomplete-Abfrage und ein Export-Job belasten Elasticsearch unterschiedlich.

Verständnis von Leistungsengpässen bei Abfragen

Bevor Sie in Lösungen eintauchen, ist es hilfreich, die häufigsten Gründe für langsame Elasticsearch-Abfragen zu verstehen. Dazu gehören oft:

  • Komplexe Abfragen: Abfragen mit mehreren bool-Klauseln, verschachtelten Abfragen oder teuren Operationen wie wildcard oder regexp auf großen Datensätzen.
  • Ineffizienter Datenabruf: Unnötiges Abrufen von _source oder Abrufen großer Dokumentenmengen für die Paginierung.
  • Ressourcenbeschränkungen: Unzureichende CPU, Arbeitsspeicher oder Festplatten-I/O auf Datenknoten.
  • Suboptimale Mappings: Verwendung falscher Datentypen oder Nichtnutzung von doc_values für Aggregationen.
  • Shard-Ungleichgewicht oder -Überlastung: Zu viele Shards, zu wenige Shards oder ungleichmäßige Verteilung von Shards/Daten.
  • Cache-Fehlschläge oder schlechte Cache-Passung: Wiederholung teurer Suchvorgänge ohne Verwendung von Request-Caching, Filterkontext oder anwendungsspezifischem Caching, wo angemessen.

Optimierung der Abfragestruktur

Die Art und Weise, wie Sie Ihre Abfragen konstruieren, hat einen tiefgreifenden Einfluss auf ihre Leistung. Kleine Änderungen können zu erheblichen Verbesserungen führen.

1. Nur notwendige Felder abrufen (_source-Filterung & stored_fields)

Standardmäßig gibt Elasticsearch das gesamte _source-Feld für jedes übereinstimmende Dokument zurück. Wenn Ihre Dokumente groß sind und die Benutzeroberfläche nur einen Titel, eine ID und einen Zeitstempel benötigt, verschwendet das Abrufen des gesamten Dokuments Netzwerkbandbreite und Parsing-Zeit.

  • _source-Filterung: Verwenden Sie den _source-Parameter, um ein Array von Feldern anzugeben, die ein- oder ausgeschlossen werden sollen.

    GET /my-index/_search
    {
      "_source": ["title", "author", "publish_date"],
      "query": {
        "match": {
          "content": "Elasticsearch performance"
        }
      }
    }
    
  • stored_fields: Wenn Sie bestimmte Felder explizit in Ihrem Mapping gespeichert haben ("store": true), können Sie diese mit stored_fields abrufen. Die meisten Bereitstellungen speichern auf diese Weise nicht viele Felder, daher ist die _source-Filterung die häufigere Lösung.

    GET /my-index/_search
    {
      "stored_fields": ["title", "author"],
      "query": {
        "match": {
          "content": "Elasticsearch performance"
        }
      }
    }
    

2. Bevorzugen Sie effiziente Abfragetypen

Einige Abfragetypen sind von Natur aus ressourcenintensiver als andere.

  • Vermeiden Sie führende Wildcards und breite Regexps: wildcard- und regexp-Abfragen können teuer sein, insbesondere mit führenden Wildcards wie *test. Präfix-Abfragen sind in der Regel besser handhabbar als Suchvorgänge mit führenden Wildcards, benötigen aber dennoch sinnvolle Mappings und begrenzte Eingaben.

    # Ineffizient - führende Wildcard vermeiden
    {
      "query": {
        "wildcard": {
          "name.keyword": {
            "value": "*search"
          }
        }
      }
    }
    
    # Besser - wenn Sie das Präfix kennen
    {
      "query": {
        "prefix": {
          "name.keyword": {
            "value": "Elastic"
          }
        }
      }
    }
    
  • Verwenden Sie match_phrase für Phrasenabsicht: Wenn der Benutzer nach einer exakten Phrase sucht, drückt match_phrase diese Absicht besser aus als mehrere nicht zusammenhängende match-Klauseln. Es ist nicht immer günstiger, vermeidet aber die Rückgabe von Dokumenten, die die Wörter nur weit voneinander entfernt enthalten.

  • Filterkontext für Ja/Nein-Bedingungen: Wenn Sie nur wissen möchten, ob ein Dokument einer Bedingung entspricht, setzen Sie diese Bedingung in den filter-Kontext oder verwenden Sie constant_score. Dies vermeidet unnötige Bewertungsarbeit und ist cache-freundlicher.

    GET /my-index/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "term": {
              "status": "active"
            }
          }
        }
      }
    }
    

3. Optimieren Sie Boolesche Abfragen

  • Verwenden Sie Filter für strukturierte Einschränkungen: Setzen Sie Mandanten-IDs, Statuswerte, Datumsbereiche und exakte Tags in filter, nicht in must, es sei denn, sie benötigen eine Bewertung. Elasticsearch kann Klauseln intern neu anordnen und optimieren, verlassen Sie sich daher nicht auf die JSON-Reihenfolge als Ihr Hauptleistungswerkzeug.
  • Verwenden Sie minimum_should_match gezielt: Es kann die Relevanz verbessern und breite Übereinstimmungen reduzieren, aber eine zu hohe Einstellung kann gültige Ergebnisse verbergen.

4. Effiziente Paginierung (search_after und scroll)

Die traditionelle from/size-Paginierung wird für tiefe Seiten (z. B. from: 10000, size: 10) sehr ineffizient. Elasticsearch muss alle Dokumente bis zu from + size auf jedem Shard abrufen und sortieren, dann from-Dokumente verwerfen.

  • search_after: Für Echtzeit-Tiefenpaginierung wird search_after empfohlen. Es verwendet die Sortierreihenfolge des letzten Dokuments der vorherigen Seite, um den nächsten Satz von Ergebnissen zu finden, ähnlich wie Cursor in traditionellen Datenbanken. Es ist zustandslos und skaliert besser.

    # Erste Anfrage
    GET /my-index/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
    # Nachfolgende Anfrage unter Verwendung der Sortierwerte des letzten Dokuments der ersten Anfrage
    GET /my-index/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "search_after": [1678886400000, "doc_id_XYZ"],
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
  • scroll-API: Für den Massenabruf großer Datensätze, wie z. B. beim Reindexing oder Export, kann scroll weiterhin nützlich sein. Für neuere Elasticsearch-Versionen und langlaufende Vollindex-Scans sollten Sie auch Point-in-Time plus search_after in Betracht ziehen. Scroll ist nicht für benutzerorientierte Echtzeit-Paginierung geeignet.

5. Optimierung von Aggregationen

Aggregationen können ressourcenintensiv sein, insbesondere bei Feldern mit hoher Kardinalität.

  • Vorberechnung von Aggregationen: Erwägen Sie, komplexe, nicht in Echtzeit durchgeführte Aggregationen während der Indexierung oder nach einem Zeitplan auszuführen, um Ergebnisse vorzuberechnen und in einem separaten Index zu speichern.
  • doc_values: Stellen Sie sicher, dass für Felder, die in Aggregationen verwendet werden, doc_values aktiviert sind (was für die meisten Nicht-Text-Felder der Standard ist). Dadurch kann Elasticsearch Daten für Aggregationen effizient laden, ohne _source laden zu müssen.
  • eager_global_ordinals: Für keyword-Felder, die häufig in terms-Aggregationen verwendet werden, kann das Setzen von eager_global_ordinals: true im Mapping die Leistung verbessern, indem globale Ordinals vorab erstellt werden. Dies verursacht Kosten zum Zeitpunkt der Indexaktualisierung, beschleunigt aber Aggregationen zur Abfragezeit.

Nutzung von Caching-Techniken

Elasticsearch bietet mehrere Caching-Ebenen, die wiederholte Abfragen erheblich beschleunigen können.

1. Node Query Cache

  • Mechanismus: Cached die Ergebnisse von Filterklauseln innerhalb von bool-Abfragen, die häufig verwendet werden. Es ist ein In-Memory-Cache auf Knotenebene.
  • Effektivität: Am effektivsten für wiederholte Filterklauseln. Verlassen Sie sich nicht darauf für jede Abfrage; Elasticsearch entscheidet, was sich zu cachen lohnt.
  • Konfiguration: Standardmäßig aktiviert. Sie können seine Größe mit indices.queries.cache.size steuern (Standard 10% des Heaps).

2. Shard Request Cache

  • Mechanismus: Cached Suchergebnisse auf Shard-Ebene, am häufigsten für aggregationslastige Anfragen mit size=0. Es ist eine gute Wahl für wiederholte Dashboard-Abfragen über Daten, die sich nicht jede Sekunde ändern.

  • Effektivität: Hervorragend für Dashboard-Abfragen oder analytische Anwendungen, bei denen dieselbe Anfrage (einschließlich Aggregationen) wiederholt mit identischen Parametern ausgeführt wird.

  • Verwendung: Aktivieren Sie es explizit in Ihrer Abfrage mit "request_cache": true.

    GET /my-index/_search?request_cache=true
    {
      "size": 0,
      "query": {
        "bool": {
          "filter": [
            {"term": {"status.keyword": "active"}},
            {"range": {"timestamp": {"gte": "now-1h"}}}
          ]
        }
      },
      "aggs": {
        "messages_per_minute": {
          "date_histogram": {
            "field": "timestamp",
            "fixed_interval": "1m"
          }
        }
      }
    }
    
  • Einschränkungen: Der Cache wird ungültig, sobald ein Shard aktualisiert wird (neue Dokumente werden indiziert oder vorhandene aktualisiert). Nur nützlich für Abfragen, die häufig identische Ergebnisse zurückgeben.

3. Dateisystem-Cache (Betriebssystemebene)

  • Mechanismus: Der Dateisystem-Cache des Betriebssystems spielt eine entscheidende Rolle. Elasticsearch verlässt sich stark darauf, um häufig aufgerufene Indexsegmente zu cachen.
  • Effektivität: Entscheidend für die Abfrageleistung. Wenn sich Indexsegmente im RAM befinden, wird der Festplatten-I/O vollständig umgangen, was zu einer viel schnelleren Abfrageausführung führt.
  • Best Practice: Lassen Sie ausreichend RAM für den Dateisystem-Cache. Ein üblicher Ausgangspunkt ist, den JVM-Heap bei etwa der Hälfte des Systemspeichers zu halten, unter Berücksichtigung der üblichen Elasticsearch-Heap-Grenzen, und dann mit Ihrer Arbeitslast zu validieren.

4. Anwendungsspezifisches Caching

  • Mechanismus: Implementierung eines Caches auf Ihrer Anwendungsebene (z. B. mit Redis, Memcached oder einem In-Memory-Cache) für häufig angeforderte Suchergebnisse.
  • Effektivität: Kann die schnellsten Antwortzeiten bieten, indem Elasticsearch für wiederholte Anfragen vollständig umgangen wird. Am besten für statische oder sich langsam ändernde Suchergebnisse geeignet.
  • Überlegungen: Die Cache-Invalidierungsstrategie ist entscheidend. Erfordert sorgfältiges Design, um Datenkonsistenz zu gewährleisten.

Verwendung der Profile-API zur Identifizierung von Engpässen

Die Profile-API ist ein unschätzbares Werkzeug, um genau zu verstehen, wie Elasticsearch eine Abfrage ausführt und wo Zeit verbraucht wird. Sie unterteilt die Ausführungszeit für jede Komponente Ihrer Abfrage und Aggregation.

So verwenden Sie die Profile-API

Fügen Sie einfach "profile": true zu Ihrem Suchanfragekörper hinzu.

GET /my-index/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Elasticsearch"}},
        {"term": {"status.keyword": "published"}}
      ],
      "filter": [
        {"range": {"publish_date": {"gte": "2023-01-01"}}}
      ]
    }
  },
  "aggs": {
    "top_authors": {
      "terms": {
        "field": "author.keyword",
        "size": 10
      }
    }
  }
}

Interpretation der Profile-API-Ergebnisse

Die Antwort enthält einen profile-Abschnitt, der die Ausführung von Abfragen und Aggregationen auf jedem Shard detailliert beschreibt. Wichtige Metriken, auf die Sie achten sollten, sind:

  • description: Die spezifische Abfrage- oder Aggregationskomponente.
  • time_in_nanos: Die Zeit, die für die Ausführung dieser Komponente aufgewendet wurde.
  • breakdown: Detaillierte Untermetriken wie build_scorer_time, collect_time, set_weight_time für Abfragen und reduce_time für Aggregationen.
  • children: Verschachtelte Komponenten, die zeigen, wie die Zeit innerhalb komplexer Abfragen verteilt ist.

Beispielinterpretation:

Wenn Sie eine hohe time_in_nanos für eine WildcardQuery sehen, bestätigt dies, dass dies ein teurer Teil Ihrer Abfrage ist. Wenn collect_time hoch ist, deutet dies darauf hin, dass das Abrufen und Verarbeiten von Dokumenten nach einer Übereinstimmung ein Engpass ist, möglicherweise aufgrund von _source-Parsing oder tiefer Paginierung. Eine hohe reduce_time bei Aggregationen könnte auf eine hohe Last während der finalen Zusammenführungsphase hinweisen.

Durch die Untersuchung dieser Metriken können Sie bestimmte Abfrageklauseln oder Aggregationsfelder identifizieren, die die meisten Ressourcen verbrauchen, und dann die zuvor besprochenen Optimierungstechniken anwenden.

Allgemeine Best Practices für die Leistung

Über abfragespezifische Optimierungen hinaus tragen mehrere clusterweite und indexebene Best Practices zur allgemeinen Suchleistung bei.

1. Optimale Index-Mappings

  • text vs. keyword: Verwenden Sie text für die Volltextsuche und keyword für exakte Wertübereinstimmungen, Sortierung und Aggregationen. Nicht übereinstimmende Typen können zu ineffizienten Abfragen führen.
  • doc_values: Stellen Sie sicher, dass doc_values für Felder aktiviert sind, die Sie sortieren oder aggregieren möchten. Sie sind standardmäßig für die meisten Feldtypen aktiviert, die Sortierung und Aggregationen unterstützen, wie keyword, numerische, Datums-, Boolesche und IP-Felder. Reine text-Felder sind für die Volltextsuche; verwenden Sie ein keyword-Unterfeld, wenn Sie exakte Übereinstimmungen oder Aggregationen benötigen.
  • norms: Deaktivieren Sie norms ("norms": false) für Felder, bei denen Sie keine Dokumentlängennormalisierung benötigen (z. B. ID-Felder). Dies spart Speicherplatz und verbessert die Indexierungsgeschwindigkeit bei minimalen Auswirkungen auf die Abfrageleistung für nicht bewertende Abfragen.
  • index_options: Verwenden Sie für text-Felder index_options: docs, wenn Sie nur wissen müssen, ob ein Begriff in einem Dokument vorhanden ist, und index_options: positions (der Standard), wenn Sie Phrasenabfragen und Näherungssuchen benötigen.

2. Überwachen Sie die Cluster-Gesundheit und -Ressourcen

  • Cluster-Status: Grün ist das Ziel. Gelb bedeutet, dass ein oder mehrere Replikat-Shards nicht zugewiesen sind; Suchvorgänge können weiterhin funktionieren, aber die Ausfallsicherheit ist verringert und die Leistung kann leiden. Rot bedeutet, dass primäre Shards fehlen und einige Daten nicht verfügbar sind.
  • Ressourcenüberwachung: Überwachen Sie regelmäßig CPU, RAM, Festplatten-I/O und Netzwerkauslastung auf Ihren Datenknoten. Spitzen in diesen Metriken korrelieren oft mit langsamen Abfragen.
  • JVM-Heap: Behalten Sie die JVM-Heap-Auslastung im Auge. Eine hohe Auslastung kann zu häufigen Garbage-Collection-Pausen führen, die Abfragen verlangsamen. Optimieren Sie Abfragen, um den Heap-Druck zu reduzieren.

3. Richtige Shard-Zuweisung

  • Zu viele Shards: Jeder Shard verbraucht Ressourcen. Viele kleine Shards erzeugen Overhead. Shards im zweistelligen Gigabyte-Bereich sind üblich, aber die richtige Größe hängt von Heap, Abfragemuster, Wiederherstellungszielen und Hardware ab.
  • Zu wenige Shards: Begrenzt die Parallelität. Abfragen gegen einen Index mit zu wenigen Shards können nicht alle verfügbaren Datenknoten effizient nutzen.

4. Indexierungsstrategie

  • Aktualisierungsintervall: Ein niedrigeres refresh_interval (Standard 1 Sekunde) macht Daten schneller sichtbar, erhöht aber den Indexierungsaufwand. Für suchlastige Workloads sollten Sie es leicht erhöhen (z. B. 5-10 Sekunden), um den Aktualisierungsdruck zu verringern.

Der praktische Arbeitsablauf ist einfach: Finden Sie die echte langsame Abfrage, profilieren Sie sie, reduzieren Sie die Datenmenge, die sie berührt, und passen Sie das Mapping an die Art und Weise an, wie Benutzer suchen. Wenn die Abfrage bereits sauber ist, überprüfen Sie das Shard-Layout, den Heap-Druck, den Dateisystem-Cache und den Festplatten-I/O. Elasticsearch ist schnell, wenn das Indexdesign, die Abfrageform und die Cluster-Ressourcen übereinstimmen.