Bulk-API-Tuning für hohen Indexierungsdurchsatz
AI generated
_doc
_index
Elasticsearch / Ingest & Pipelines
Bulk-API-Tuning
hoher Indexierungsdurchsatz für die initiale Katalog-Indexierung

Eine initiale Katalog-Indexierung mit mehreren Millionen Produktdokumenten läuft mit den Standardeinstellungen von Elasticsearch spürbar langsamer, als es die zugrunde liegende Hardware eigentlich hergeben würde. Die Bulk-API selbst ist dabei selten der Engpass, sondern das Zusammenspiel aus Batch-Größe, Refresh-Interval, Replica-Anzahl und Thread-Pool-Konfiguration, das bei den Standardwerten auf einen ausgewogenen Dauerbetrieb optimiert ist, nicht auf einen kurzen, intensiven Massenimport. Dieser Artikel zeigt, wie sich die optimale Batch-Größe systematisch ermitteln lässt, welche temporären Einstellungsänderungen während einer Massenindexierung den größten Effekt haben, und wie eine praktische Tuning-Checkliste für die initiale Indexierung eines großen Katalogs aussieht.

13 Min. Lesezeit _bulk · Batch-Größe Refresh-Interval · Replica-Tuning

1. Die Bulk-API im Grundaufbau

Die Bulk-API fasst mehrere Index-, Update- oder Delete-Operationen in einem einzigen HTTP-Request zusammen, kodiert als NDJSON mit abwechselnden Aktions- und Quellzeilen. Statt für jedes einzelne Produktdokument einen separaten Request zu senden, überträgt eine Anwendung mehrere hundert oder tausend Dokumente in einem Rutsch, was den Overhead pro Dokument für HTTP-Header, TCP-Handshake und Verbindungsaufbau drastisch reduziert.

Elasticsearch verarbeitet einen Bulk-Request intern über einen dedizierten Write-Thread-Pool, dessen Größe an die Anzahl verfügbarer CPU-Kerne gekoppelt ist. Läuft mehr Arbeit gleichzeitig ein, als dieser Pool verarbeiten kann, landen weitere Requests zunächst in einer begrenzten Warteschlange, bevor sie mit einem Rejection-Fehler abgelehnt werden, sobald auch diese Warteschlange voll ist.


POST _bulk
{ "index": { "_index": "products", "_id": "4711" } }
{ "sku": "abc-123", "price": 19.90 }
{ "index": { "_index": "products", "_id": "4712" } }
{ "sku": "abc-124", "price": 24.50 }

2. Die optimale Batch-Größe systematisch finden

Es gibt keine universell richtige Batch-Größe, da sie von Dokumentgröße, Netzwerklatenz und Cluster-Hardware abhängt. Eine zu kleine Batch-Größe, etwa nur wenige Dutzend Dokumente pro Request, erzeugt unnötig viele Round-Trips und lässt den relativen Overhead pro Request dominieren. Eine zu große Batch-Größe belastet dagegen den Heap des koordinierenden Knotens, erhöht das Risiko, den Indexing-Circuit-Breaker auszulösen, und kann durch längere Garbage-Collection-Pausen die Latenz einzelner Requests unvorhersehbar erhöhen.

Ein bewährter Ausgangspunkt ist eine Batch-Größe zwischen fünf und fünfzehn Megabyte Payload oder ein paar tausend Dokumente, je nachdem, welcher Wert zuerst erreicht wird, gefolgt von systematischen Lasttests mit steigender Batch-Größe unter realistischer Netzwerkanbindung. Der Durchsatz wird dabei üblicherweise mit steigender Batch-Größe zunächst besser, erreicht ein Plateau und fällt bei zu großen Batches durch Speicherdruck wieder ab, sodass sich das Optimum empirisch statt theoretisch bestimmen lässt.

3. Der Trade-off zwischen Round-Trips und Speicherverbrauch

Jeder zusätzliche Round-Trip kostet Netzwerklatenz und Verarbeitungs-Overhead auf beiden Seiten der Verbindung, unabhängig von der eigentlichen Dokumentgröße. Bei tausend kleinen Batches mit jeweils zehn Dokumenten summiert sich dieser Overhead schnell zu einer erheblichen Gesamtlaufzeit, selbst wenn die reine Indexierungsarbeit auf dem Cluster minimal wäre.

Größere Batches reduzieren die Anzahl der Round-Trips, verlangen aber, dass der gesamte Batch-Inhalt gleichzeitig im Heap sowohl des sendenden Prozesses als auch des empfangenden koordinierenden Knotens vorliegt, bevor die einzelnen Operationen verteilt und verarbeitet werden können. Dieser Speicherbedarf skaliert linear mit der Batch-Größe, weshalb eine unbegrenzte Erhöhung der Batch-Größe irgendwann in Speicherdruck statt in mehr Durchsatz mündet.

4. Refresh-Interval während der Massenindexierung deaktivieren

Standardmäßig macht Elasticsearch neu geschriebene Dokumente etwa jede Sekunde über einen Refresh-Vorgang durchsuchbar, der ein neues, kleines Lucene-Segment erzeugt. Bei einer Massenindexierung mit hoher Schreibrate summieren sich diese häufigen Refreshs zu erheblichem Overhead, da jedes neue Segment später wieder mit anderen Segmenten zusammengeführt werden muss und die Sichtbarkeit einzelner Dokumente während der initialen Ladephase ohnehin meist nicht gebraucht wird.

Für die Dauer einer initialen Katalog-Indexierung empfiehlt es sich deshalb, index.refresh_interval auf -1 zu setzen, was automatische Refreshs vollständig deaktiviert, und nach Abschluss der Massenindexierung wieder auf den gewünschten Produktivwert, meist eine Sekunde, zurückzusetzen. Ein manueller Refresh am Ende macht die Daten dann gezielt auf einmal durchsuchbar, statt kontinuierlich kleine Segmente zu erzeugen.


PUT products/_settings
{ "index": { "refresh_interval": "-1", "number_of_replicas": 0 } }

// nach Abschluss der Massenindexierung wieder zurücksetzen
PUT products/_settings
{ "index": { "refresh_interval": "1s", "number_of_replicas": 1 } }
POST products/_refresh

5. Replica-Anzahl temporär auf null reduzieren

Jede Schreiboperation muss bei aktivierten Replicas nicht nur auf dem primären Shard, sondern auch auf jedem Replica-Shard ausgeführt werden, bevor der Bulk-Request als abgeschlossen gilt. Während einer initialen Massenindexierung verdoppelt oder vervielfacht das den tatsächlichen Schreibaufwand im Cluster, ohne dass die Replicas in dieser Phase bereits einen praktischen Nutzen für Ausfallsicherheit oder Lesekapazität bieten.

Eine verbreitete Praxis ist deshalb, number_of_replicas für die Dauer der initialen Indexierung auf null zu setzen und erst danach schrittweise wieder zu erhöhen. Elasticsearch erzeugt die Replica-Shards dann im Anschluss durch Kopieren der bereits vollständig befüllten primären Shards, was insgesamt oft schneller ist, als jede einzelne Schreiboperation während der gesamten Ladephase auf mehrere Shards gleichzeitig anzuwenden.

6. Thread-Pool und Bulk-Queue im Blick behalten

Der Write-Thread-Pool jedes Knotens hat eine feste Größe, üblicherweise gekoppelt an die Anzahl der verfügbaren Prozessorkerne, sowie eine begrenzte Warteschlange für Requests, die nicht sofort verarbeitet werden können. Sendet eine Anwendung mehr parallele Bulk-Requests, als der Pool zusammen mit seiner Warteschlange aufnehmen kann, liefert Elasticsearch für die überzähligen Requests einen EsRejectedExecutionException-Fehler zurück, statt sie unbegrenzt zu puffern.

Über GET _nodes/stats/thread_pool lässt sich die aktuelle Auslastung, einschließlich abgelehnter Requests, pro Knoten beobachten. Statt die Warteschlangengröße pauschal zu erhöhen, was Rejections nur verzögert statt löst, ist es meist wirksamer, die Anzahl paralleler Client-Verbindungen und deren Batch-Größe so zu dimensionieren, dass sie zur tatsächlichen Verarbeitungskapazität des Clusters passt.

7. Fehlerbehandlung bei partiellen Bulk-Failures

Ein Bulk-Request schlägt selten komplett fehl, viel häufiger scheitern einzelne Operationen innerhalb eines ansonsten erfolgreichen Batches, etwa wegen eines Versionskonflikts, eines Mapping-Fehlers bei einem einzelnen Dokument oder eines ausgelösten Circuit-Breakers. Die Antwort der Bulk-API enthält dafür ein Feld errors, das bei true signalisiert, dass mindestens eine Teiloperation fehlgeschlagen ist, während der Rest des Batches trotzdem verarbeitet wurde.

Eine robuste Client-Implementierung muss deshalb jedes Element im items-Array einzeln auf einen vorhandenen Fehlercode prüfen, statt sich allein auf den HTTP-Statuscode der Gesamtantwort zu verlassen. Fehlgeschlagene Einzeloperationen lassen sich dann gezielt erneut senden, idealerweise mit exponentiellem Backoff bei Fehlern durch temporäre Überlastung wie Circuit-Breaker-Auslösungen.

8. Praktische Tuning-Checkliste für die initiale Katalog-Indexierung

Vor dem Start einer großen initialen Indexierung lohnt sich eine feste Checkliste: Refresh-Interval auf minus eins setzen, Replica-Anzahl auf null reduzieren, Translog-Durability bei akzeptablem Risiko auf asynchron stellen, eine anhand von Lasttests ermittelte Batch-Größe verwenden und die Anzahl paralleler Bulk-Clients an die verfügbaren Thread-Pool-Ressourcen anpassen, statt blind möglichst viele parallele Verbindungen zu öffnen.

Nach Abschluss der Massenindexierung gehört zur Checkliste ebenso, alle temporären Einstellungen wieder auf produktive Werte zurückzusetzen, einen abschließenden manuellen Refresh auszulösen und optional einen Force-Merge anzustoßen, der die während der Ladephase entstandenen vielen kleinen Segmente zu wenigen größeren, effizienteren Segmenten zusammenführt, bevor der Index für den produktiven Lesebetrieb freigegeben wird.


# 1. Vor der Massenindexierung
curl -X PUT "es:9200/products/_settings" -d '{"index":{"refresh_interval":"-1","number_of_replicas":0}}'

# 2. Während der Ladephase: Bulk-Requests mit getesteter Batch-Größe senden

# 3. Nach Abschluss
curl -X PUT "es:9200/products/_settings" -d '{"index":{"refresh_interval":"1s","number_of_replicas":1}}'
curl -X POST "es:9200/products/_refresh"
curl -X POST "es:9200/products/_forcemerge?max_num_segments=1"

9. Den Indexierungsdurchsatz während der Ladephase überwachen

Ohne Monitoring bleibt Tuning reines Raten. Über GET _nodes/stats/indices/indexing lässt sich die aktuelle Indexierungsrate pro Knoten beobachten, während GET _cat/thread_pool/write?v einen schnellen Überblick über aktive und abgelehnte Requests im Write-Pool liefert. Beide Werte gemeinsam zeigen, ob ein Engpass eher an der Verarbeitungskapazität des Clusters oder an der Sendegeschwindigkeit der Client-Anwendung liegt.

Ein plötzlicher Rückgang der Indexierungsrate bei gleichbleibender Bulk-Request-Rate deutet meist auf Speicherdruck, häufige Garbage-Collection-Pausen oder einen ausgelösten Circuit-Breaker hin, erkennbar an den entsprechenden Zählern in GET _nodes/stats/breaker. Regelmäßiges Beobachten dieser Kennzahlen während der gesamten Ladephase erlaubt es, die Batch-Größe oder die Anzahl paralleler Clients noch während des laufenden Imports nachzujustieren, statt erst nach einem fehlgeschlagenen Lauf zu reagieren.

Einstellung Standardwert Wert während Massenindexierung Effekt
refresh_interval 1s -1 (deaktiviert) Weniger kleine Segmente, weniger Merge-Overhead
number_of_replicas 1 oder mehr 0 Kein doppelter Schreibaufwand pro Dokument
translog.durability request async (bei akzeptablem Risiko) Weniger fsync-Overhead pro Bulk-Request
Batch-Größe Anwendungsabhängig 5-15 MB oder wenige tausend Dokumente Ausgewogenes Verhältnis Round-Trips zu Speicher
Parallele Bulk-Clients Oft unkoordiniert An Thread-Pool-Kapazität angepasst Weniger Rejections, stabilerer Durchsatz

Mironsoft

Suchindex-Setup, Relevanz-Tuning und Magento-Suche

Magento-Suche, die die falschen Produkte zuerst zeigt?

Wir richten Elasticsearch oder OpenSearch für Magento sauber ein, tunen Relevanz und Facetten auf das tatsächliche Sortiment und optimieren Indexierungsprozesse für große Kataloge.

Relevanz-Tuning

Suchergebnisse und Facetten auf die tatsächlichen Kundenbedürfnisse abstimmen.

Such-Migration

Umstieg von Solr oder MySQL-Suche auf Elasticsearch/OpenSearch sauber begleiten.

Index-Performance

Indexierungsprozesse für große Kataloge zuverlässig und performant gestalten.

10. Zusammenfassung

Bulk-API-Tuning: Das Wichtigste auf einen Blick

Kernprinzip

Batch-Größe, Refresh-Interval, Replica-Anzahl und Thread-Pool-Kapazität müssen für eine Massenindexierung gemeinsam justiert werden, nicht isoliert voneinander.

Wichtigste Hebel

refresh_interval auf -1 und number_of_replicas auf 0 während der Ladephase bringen meist den größten Effekt, beide danach wieder auf Produktivwerte zurücksetzen.

Batch-Größe finden

Kein fester Wert, sondern empirisch per Lasttest ermitteln, Ausgangspunkt fünf bis fünfzehn Megabyte Payload oder wenige tausend Dokumente pro Request.

Nach der Ladephase

Manueller Refresh und optionaler Force-Merge konsolidieren die während der Massenindexierung entstandenen vielen kleinen Segmente vor dem produktiven Lesebetrieb.

11. FAQ: Bulk-API-Tuning: Das Wichtigste auf einen Blick

1Warum ist die Standardkonfiguration von Elasticsearch für eine Massenindexierung suboptimal?
Die Standardwerte sind auf einen ausgewogenen Dauerbetrieb mit gleichzeitigem Lesen und Schreiben optimiert, nicht auf einen kurzen, sehr intensiven Massenimport ohne gleichzeitige Lesezugriffe.
2Wie findet man die optimale Batch-Größe für die Bulk-API?
Systematisch durch Lasttests mit steigender Batch-Größe unter realistischer Netzwerkanbindung, ausgehend von einem Startwert zwischen fünf und fünfzehn Megabyte Payload, bis der Durchsatz ein Plateau erreicht.
3Warum sollte refresh_interval während einer Massenindexierung deaktiviert werden?
Weil jeder automatische Refresh ein neues, kleines Lucene-Segment erzeugt, das später wieder zusammengeführt werden muss. Deaktivierung reduziert diesen Overhead während der Ladephase erheblich.
4Was bringt es, number_of_replicas während der initialen Indexierung auf null zu setzen?
Jede Replica verdoppelt oder vervielfacht den tatsächlichen Schreibaufwand pro Dokument. Ohne Replicas während der Ladephase sinkt dieser Aufwand deutlich, die Replicas werden danach durch Kopieren der fertigen primären Shards erzeugt.
5Was passiert, wenn mehr parallele Bulk-Requests gesendet werden, als der Write-Thread-Pool verarbeiten kann?
Überzählige Requests landen zunächst in einer begrenzten Warteschlange und werden anschließend mit einem EsRejectedExecutionException-Fehler abgelehnt, statt unbegrenzt gepuffert zu werden.
6Wie erkennt man einen partiellen Fehler in einer Bulk-Antwort?
Am Feld errors, das bei true signalisiert, dass mindestens eine Teiloperation im Batch fehlgeschlagen ist. Jedes Element im items-Array muss einzeln auf einen Fehlercode geprüft werden.
7Welche Rolle spielt translog.durability beim Bulk-Tuning?
Der Wert async reduziert den fsync-Overhead pro Bulk-Request, erhöht aber bei einem Knotenausfall das Risiko, kurz zuvor geschriebene Daten zu verlieren, ein Trade-off, der bewusst abgewogen werden muss.
8Welche Kennzahlen zeigen einen Engpass während der Massenindexierung an?
Die Indexierungsrate aus GET _nodes/stats/indices/indexing, die Thread-Pool-Auslastung aus GET _cat/thread_pool/write und Circuit-Breaker-Zähler aus GET _nodes/stats/breaker.
9Was sollte nach Abschluss der Massenindexierung passieren?
Alle temporären Einstellungen wie refresh_interval und number_of_replicas werden auf produktive Werte zurückgesetzt, ein manueller Refresh wird ausgelöst und optional ein Force-Merge, der viele kleine Segmente konsolidiert.
10Warum ist eine zu große Batch-Größe ebenfalls problematisch?
Sie belastet den Heap des koordinierenden Knotens stärker, erhöht das Risiko eines ausgelösten Circuit-Breakers und kann durch längere Garbage-Collection-Pausen die Latenz einzelner Requests unvorhersehbar erhöhen.