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.
Inhaltsverzeichnis
- 1. Die Bulk-API im Grundaufbau
- 2. Die optimale Batch-Größe systematisch finden
- 3. Der Trade-off zwischen Round-Trips und Speicherverbrauch
- 4. Refresh-Interval während der Massenindexierung deaktivieren
- 5. Replica-Anzahl temporär auf null reduzieren
- 6. Thread-Pool und Bulk-Queue im Blick behalten
- 7. Fehlerbehandlung bei partiellen Bulk-Failures
- 8. Praktische Tuning-Checkliste für die initiale Katalog-Indexierung
- 9. Den Indexierungsdurchsatz während der Ladephase überwachen
- 10. Zusammenfassung
- 11. FAQ
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.