03 September 2014 Engineering

Performance-Aspekte für Elasticsearch-Indexierung

Von Michael McCandless

Update November 2, 2015: If you're running Elasticsearch 2.0, check out this updated post about performance considerations for Elasticsearch 2.0 indexing.

Elasticsearch wird für unterschiedlichste Anwendungsfälle verwendet. Angefangen von kleinen Log-Ausgaben über Indexierung großer, im Web verfügbarer Dokumentensammlungen – Maximierung des Durchsatzes ist in den meisten Fällen ein wichtiges Ziel. Wir arbeiten hart daran, gute und allgemeingültige Defaults für „typische“ Anwendungen zu finden. Jedoch können Sie Indexierungs-Performance schnell optimieren, indem Sie die hier beschriebenen und leicht umsetzbaren Best Practices beachten.

Fangen wir mit dem Java Heap an. Halten Sie ihn so gering wie möglich. Legen Sie den Wert nur so hoch fest, wie es wirklich nötig ist (idealerweise nicht mehr als 50% des verfügbaren RAM), sodass dies dem maximalen Nutzungsprofil genügt. Dadurch bleibt der übrige (hoffentlich umfangreiche) RAM für das Betriebssystem, das sich um das IO-Caching-Management kümmern muss. Achten Sie darauf, dass das OS nicht den Java-Prozess auslagert..

Aktualisieren Sie Elasticsearch auf die aktuellste Version (momentan 1.3.2): zahlreiche Indexierungs-Probleme wurden in den aktuellen Releases behoben.

Bevor wir in die Tiefe gehen, hier noch eine Vorwarnung: Denken Sie daran, dass alle Informationen hier momentan auf dem neusten Stand sind (1.3.2), dass aber Elasticsearch sich ständig rapide weiter entwickelt. Diese Informationen könnten also bereits veraltet sein, wenn Sie, Google-Nutzer aus der Zukunft, sie finden. Wenn Sie sich nicht sicher sind, fragen Sie einfach auf der Benutzerliste nach..

Marvel ist besonders nützlich, wenn es darum geht, Ihren Cluster auf Indexierungsdurchsatz zu trimmen: Während Sie diverse Einstellungen vornehmen, können Sie mit Marvel die Auswirkung der Änderung auf den Cluster visualisieren.

Client-Seite

Nutzen Sie immer die Bulk-API, die mehrere Dokumente mit einer Anfrage indexiert, und experimentieren Sie mit der richtigen Dokumentanzahl pro Anfrage. Sie hängt von vielen Faktoren ab. Tendenziell sollten Sie bei der Dokumentenanzahl eher nach unten als nach oben gehen. Nutzen Sie simultane Bulk-Anfragen mit client-seitigen Threads oder separate, asynchrone Anfragen.

Bevor Sie zur Schlussfolgerung kommen, die Indexierung sei zu langsam, sollten Sie wirklich sicherstellen, dass Sie die Hardware Ihres Clusters vollständig ausnutzen: Tools wie iostat, top und ps können prüfen, ob entweder CPU oder IO node-übergreifend ausgelastet werden. Wenn sie das nicht werden, brauchen sie mehr simultane Anfragen, aber wenn Ihnen der Java-Client ein EsRejectedExecutionException wirft oder Sie auf REST-Anfragen die Antwort TOO_MANY_REQUESTS (429) HTTP erhalten, senden Sie zu viele simultane Anfragen. Wenn Sie Marvel benutzen, können Sie die Anzahl der Rejects im Abschnitt THREAD POOLS – BULK im Node-Statistik-Dashboard sehen. Es ist üblicherweise keine gute Idee, die Poolgröße von Bulk-Threads zu steigern (üblicherweise entspricht die Anzahl der Threads der Anzahl der CPU-Cores), weil das wahrscheinlich den Gesamt-Indexierungsdurchsatz verringert. Vielmehr sollten Sie die client-seitige Simultanität zurückfahren oder mehr Nodes hinzufügen.

Da die hier besprochenen Einstellungen sich auf die Maximierung des Indexierungsdurchsatzes für ein einzelnes Shard konzentrieren, sollten Sie zuerst eine einzelne Node mit einem einzigen Shard und ohne Replika testen, um zu messen, welche Kapazitäten ein einzelner Lucene-Index für eines Ihrer Dokumente hat. Auf dieser Grundlage können Sie dann weitere Feinabstimmungen vornehmen, bevor sie auf den gesamten Cluster skalieren. Dadurch erhalten Sie auch einen Ausgangswert, mit dem Sie grob bestimmen können, wie viele Nodes Sie im gesamten Cluster benötigen, um Ihre Anforderungen an den Indexierungsdurchsatz zu erfüllen.

Sobald ein einzelnes Shard gut funktioniert, können Sie die Skalierbarkeit von Elasticsearch und mehreren Nodes in Ihrem Cluster voll ausnutzen, indem Sie die Shard- und Replikanzahl steigern.

Bevor Sie irgendwelche Rückschlüsse ziehen, sollten Sie die Performance des gesamten Clusters für eine gewisse Zeit (etwa 60 Minuten) messen, damit Ihr Test den gesamten Lebenszyklus einschließlich Ereignissen wie großen Zusammenführungen, GC-Zyklen, Shard-Verschiebungen, der Überschreitung des IO-Caches beim Betriebssystem, möglicherweise unerwartetes Auslagern etc. umfasst.

Plattenspeicher

Es überrascht nicht, dass die Speichergeräte, auf denen der Index gelagert wird, großen Einfluss auf die Indexierungsleistung haben:

  • Nutzen Sie moderne Solid-State-Disks (SSDs): Diese sind weit schneller als selbst neueste Spinning Disks. Sie haben nicht nur geringere Latenzen bei Random und Sequential IO, sie sind auch besser beim hochsimultanen IO, das für die simultane Indexierung, Merging und Suche erforderlich ist.
  • Speichern Sie den Index nicht auf einem Remote-Dateisystem (z. B. NFS oder SMB/CIFS). Nutzen Sie stattdessen den lokalen Speicher des Rechners.
  • Hüten Sie sich vor virtuellen Speichern wie beispielsweise dem Elastic Block Storage von Amazon. Ein virtueller Speicher funktioniert gut mit Elasticsearch und ist daher verlockend, weil er so schnell und einfach einzurichten ist, aber er ist auf lange Sicht gesehen leider auch zwangsläufig langsamer als ein dedizierter lokaler Speicher. Bei einem kürzlichen inoffiziellem Test war selbst die Hochleistungs-EBS-Option mit Provisioned IOPS und SSD-Unterstützung immer noch um einiges langsamer als die lokale, instanzgebundene SSD. Dabei gilt es auch zu bedenken, dass die lokale instanzgebundene SSD immer noch mit allen virtuellen Maschinen auf dem physischen Rechner geteilt wird, sodass Sie sonst nicht erklärbare Verlangsamungen bemerken werden, wenn die anderen virtuellen Maschinen auf diesem Rechner plötzlich an IO-Intesivität zulegen.
  • Verteilen Sie Ihren Index auf mehrere SSDs, indem Sie mehrere path.data-Verzeichnisse anlegen, oder konfigurieren Sie einfach einen RAID 0-Array. Die beiden Lösungen sind ähnlich, nur dass Elasticsearch nicht auf Dateiblockebene verteilt, sondern Striping in individuellen Indexdateien stattfindet. Denken Sie immer daran, dass beide Ansätze das Ausfallrisiko eines einzelnen Shards (im Austausch gegen höhere IO-Performance) erhöhen, weil der Ausfall einer einzigen SSD den Index zerstört. Demgegenüber ist folgender Kompromiss vorzuziehen: Optimieren Sie einzelne Shards auf maximale Leistung und fügen Sie anschließend Replikas über verschiedene Nodes hinzu, um eine Redundanz für Node-Ausfälle zu schaffen. Sie können auch snapshot and restore nutzen, um zur weiteren Absicherung ein Backup vom Index anzulegen.

Segmente und Merging

Im Hintergrund werden neu indexierte Dokumente zuerst vom Lucene IndexWriter im RAM gelagert. In regelmäßigen Abständen, d. h. wenn der RAM-Puffer voll ist oder Elasticsearch einen flush oder refresh auslöst, werden diese Dokumente auf neue On-Disk-Segmente geschrieben. Irgendwann gibt es dann zu viele Segmente, die gemäß der Merge Policy und dem Scheduler zusammengeführt werden. Dieser Prozess ist kaskadierend: Die zusammengeführten Segmente bilden ein größeres Segment und nach genügend kleinen Zusammenführungen werden auch die größeren Segmente zusammengeführt. Hier sehen Sie eine schöne Veranschaulichung dieses Vorgangs.

Insbesondere große Merges können sehr lange dauern. Normalerweise ist das kein Problem, da solche Zusammenführungen selten vorkommen und die amortisierten Kosten so niedrig bleiben. Wenn die Zusammenführung allerdings nicht mit der Indexierung mithält, drosselt Elasticsearch eingehende Indexierungsanfragen auf einen einzigen Thread (ab 1.2), um schwerwiegende Probleme bei zu vielen Segmenten im Index zu verhindern.

Wenn Sie INFO-Level Logs sehen, in denen „now throttling indexing“ vorkommt oder immer weiter wachsende Segmentzahlen in Marvel beobachten, wissen Sie, dass das Merging hinterherhinkt. Marvel visualisiert die Segmentanzahl im Abschnitt MANAGEMENT EXTENDED im Index-Statistik-Dashboard, und diese sollte nur mit einem sehr geringen logarithmischen Faktor steigen, möglicherweise sogar mit Auf- und Abbewegungen, wenn große Merges abgeschlossen werden:

Warum sollten Merges hinterherhinken? Standardmäßig beschränkt Elasticsearch die zulässige Bytemenge, die über alle Zusammenführungen hinweg geschrieben werden darf, auf dürftige 20 MB/s. Bei Festplatten wird so sichergestellt, dass die Zusammenführung nicht die IO-Kapazität des Laufwerks überlastet, sodass die Simultansuche immer noch gut funktioniert. Wenn allerdings während Ihrer Indexierung keine Suchen stattfinden, Ihnen die Suchleistung weniger wichtig ist als der Indexierungsdurchsatz oder sich Ihr Index auf SSDs befindet, sollten Sie die Merge Throttling vollständig deaktivieren, indem Sie settingindex.store.throttle.type auf none setzen. Einzelheiten finden Sie unter store. Übrigens gab es vor der Version 1.2 einen ziemlich üblen Bug, der zur Folge hatte, dass die IO-Drosselung bei der Zusammenführung wesentlich strenger war, als man es wollte. Also bitte aktualisieren!

Wenn Sie leider immer noch Festplatten benutzen, die Simultan-IO nicht annähernd so gut bewältigen können wir SSDs, sollten Sie index.merge.scheduler.max_thread_count auf 1 setzen. Andernfalls lässt der Standardwert (der SSDs bevorzugt) zu viele Merges auf einmal zu.

Rufen Sie kein optimize für einen Index auf, der noch aktiv aktualisiert wird. Dies ist ein sehr ressourcen-intensiver Vorgang (es werden dabei alle Segmente zusammengeführt). Allerdings ist es nach dem Hinzufügen von Dokumenten zu einem Index durchaus ratsam, den Index an diesem Punkt zu optimieren, weil so die für die Suche erforderlichen Ressourcen reduziert werden. Wenn Sie zum Beispiel zeitbasierte Indizes nutzen, bei denen neue Logs nach Ablauf eines Tages gerollt und zu einem neuen Index hinzugefügt werden, sollten Sie den Index optimieren, insbesondere, wenn Nodes viele (z. B. zeitbasierte) Indizes beinhalten.

Hier sind einige weitere Einstellungen für das Feintuning:

  • Tunen Sie Ihre Mappings und deaktivieren Sie alle Felder, die Sie nicht benötigen wie zum Beispiel das Feld _all. Bei Feldern, die Sie behalten möchten, können Sie auch festlegen, ob und wie diese indexiert oder gespeichert werden sollen.
  • Sie mögen versucht sein, das Feld _source zu deaktivieren, aber dessen Indexierungskosten sind höchstwahrscheinlich gering (wird nur gespeichert, nicht indexiert) und der Wert des Feldes für zukünftige Updates oder die vollständige Neuindexierung eines bestehenden Index ist so groß, dass sich die Deaktivierung normalerweise nicht lohnt, es sei denn, die Speicherbelegung macht Ihnen Sorgen. Das sollte es aber eigentlich nicht, denn Festplattenspeicher ist heutzutage kostengünstig.
  • Wenn Sie etwas Verzögerung bei der Suche kürzlich indexierter Dokumente hinnehmen können, steigern Sie den index.refresh_interval auf 30 Sekunden oder deaktivieren Sie ihn komplett, indem Sie ihn auf -1 setzen. So entstehen größere Segmente und ein kostspieliges Merge der Segmente kann vermieden werden.
  • Wenn Sie mindestens auf Elasticsearch 1.3.2aktualisiert haben, d. h. die Probleme behoben haben , die zu exzessiver RAM-Nutzung bei seltenen flush-Operationen führen können, steigern Sie index.translog.flush_threshold_size vom Standardwert (aktuell 200 MB) auf 1 GB, um die Aufrufhäufigkeit von fsync für Indexdateien zu senken.
    Marvel visualisiert die flush-Rate im Dashboard Index Statistic und Abschnitt MANAGEMENT.
  • Verwenden Sie beim Aufbau eines großen Index keine Replikation (Replica) und aktivieren Sie diese später. Denken Sie aber auch daran, dass ein Node-Ausfall mit ausgeschalteter Replikation bedeutet, dass Sie Daten verlieren (Ihr Cluster wird rot gekennzeichnet), weil es keine Redundanz gibt. Wenn Sie optimize aufrufen wollen (weil keine weiteren Dokumente hinzugefügt werden), ist es ratsam, dies nach der Indexierung und vor der Aktivierung der Replikation zu tun. Damit werden für die Replikation nur optimierte Segmente kopiert. Einzelheiten finden Sie unter Indexeinstellungen aktualisieren.

Indexpuffergröße

Wenn Ihre Node nur für umfangreiche Indexierungsarbeiten herangezogen wird, achten Sie darauf, dass indices.memory.index_buffer_size groß genug ist, um mindestens ~512 MB Indexierungspuffer pro aktivem Fragment bereitzustellen (darüber hinaus verbessert sich die Indexierungsleistung nicht wesentlich). Elasticsearch nimmt diese Einstellung (als Prozentsatz des Java-Heap oder als Absolutwert in Bytes) und teilt sie gleichmäßig unter den aktuell aktiven Shards der Node auf, natürlich unter Berücksichtigung der Werte min_index_buffer_size und max_index_buffer_size. Größere Werte bedeuten für Lucene das Schreiben größerer Ausgangssegmente, wodurch sich die Notwendigkeit für ein späteres Merge verringert.

Der Standardwert von 10 % ist oft sehr gut bemessen: Wenn Sie zum Beispiel 5 aktive Shards auf einer Node haben und Ihr Heap 25 GB beträgt, bekommt jedes Shard 1/5 von 10 % der 25 GB = 512 MB (bereits das Maximum). Nach dedizierten umfangreichen Indexierungsvorgängen können Sie diese Einstellung wieder auf den Standardwert (aktuell 10 %) zurücksetzen, damit für Datenstrukturen zur Suchzeit genug RAM zur Verfügung steht. Beachten Sie, dass diese Einstellung noch nicht dynamisch ist. Die Behebung dieses Problems wird jedoch derzeit aktiv in Griff genommen.

Die Größe des genutzten Indexpuffers kann seit der Version 1.3.0 über die indices stats API abgefragt werden. Sie finden den Wert unter indices.segments.index_writer_memory. Er wird noch nicht in Marvel angezeigt und erst in der nächsten Version hinzugefügt, aber Sie können selbst ein Diagramm hinzufügen (Marvel erfasst die Daten immerhin).

Mit der Version 1.4.0 wird die indices stats API auch genau anzeigen, wie viel RAM-Puffer jedem aktiven Shard zugewiesen wurde, und zwar in indices.segments.index_writer_max_memory. Wenn Sie diese Werte pro Shard für einen bestimmten Index sehen wollen, nutzen Sie http://host:9200//_stats?level=shards. Dadurch werden die statistischen Daten pro Shard und die shard-übergreifenden Gesamtwerte ausgegeben.

Nutzen Sie auto id oder wählen Sie eine gute ID

Wenn Ihnen die ID Ihrer Dokumente egal ist, können Sie diese automatisch von Elasticsearch zuweisen lassen: Dieser Fall ist optimiert (seit Version 1.2), um eine ID und ein Versions-Lookup pro Dokument zu speichern; Sie können den Performance-Unterschied in den Nightly Index Benchmarks von Elasticsearch sehen (vergleichen Sie die Zeilen Fast und FastUpdate).

Wenn Sie Ihre eigenen IDs haben wollen, wählen Sie nach Möglichkeit solche, die schnell für Lucene sind (falls Sie darauf Einfluss haben), und aktualisieren Sie mindestens auf Version 1.3.2, da es ab dieser Version weitere Verbesserungen an id lookup gab. Denken Sie einfach daran, dass die UUID.randomUUID() von Java die schlechteste Wahl für eine ID ist, weil diese Methode keine Vorhersagen oder Erkennung von Mustern zur ID-Vergabe für Segmente ermöglicht und im schlimmsten Fall eine Suche pro Segment verursacht.

Sie sehen die Unterschiede bei der Indexierungsgeschwindigkeit, die von Marvel gemeldet werden, wenn Sie Flake IDs nutzen:

gegenüber völlig zufälligen UUIDs:

Ab Version 1.4.0 werden wir die automatisch erzeugten IDs von Elasticsearch von zufälligen UUIDs auf Flake IDs umstellen..

Wenn Sie sich für die Low-Level-Operationen interessieren, die Lucene an Ihrem Index vornimmt, aktivieren Sie lucene.iw TRACE-Logging (verfügbar ab 1.2). Dadurch werden zwar sehr wortreiche Ausgaben erzeugt, die aber hilfreich sein können, wenn Sie verstehen wollen, was beim Lucene IndexWriter vor sich geht. Die Ausgabe erfolgt aber auf niedriger Ebene. Marvel stellt eine viel bessere grafische Echtzeit-Ansicht der Vorgänge in Indizes bereit.

Skalieren

Bedenken Sie, dass wir uns hier auf die Performance-Optimierung für ein einziges Shard (Lucene-Index) konzentriert haben. Wenn Sie jedoch damit zufrieden sind, kann Elasticsearch so richtig glänzen, indem Sie ganz einfach Ihre Indexierungs- und Suchvorgänge über einen ganzen Rechencluster skalieren können. Erhöhen Sie also unbedingt Ihre Shardanzahl weiter (der Standardwert liegt aktuell bei 5), sodass Sie mehr rechnerübergreifende Simultanität erhalten, die Indexgröße maximieren und gleichzeitig die Suchlatenzen verringern. Außerdem sollten Sie Ihre Replika auf mindestens 1 erhöhen, um Datenverlust bei Hardware-Ausfällen durch Redundanz vorzubeugen.

Wenn Sie immer noch Probleme haben, melden Sie sich einfach, z. B. über die Elasticsearch-Benutzerliste. Vielleicht gibt es einen interessanten Bug zu beheben (Patches sind immer willkommen!).