Zu viele Felder! 3 Möglichkeiten, um übermäßiges Mapping in Elasticsearch zu vermeiden

blog-thumb-website-search.png

Ein System benötigt drei Dinge, um als „Observable“ zu gelten: Logs, Metriken und Traces. Metriken und Traces haben vorhersehbare Strukturen, aber Logs (insbesondere Anwendungs-Logs) enthalten normalerweise unstrukturierte Daten, die erfasst und analysiert werden müssen, um nützlich zu sein. Der schwierigste Schritt beim Erreichen von Observability besteht daher zweifellos darin, Ihre Logs unter Kontrolle zu bringen. 

In diesem Artikel sehen wir uns drei effektive Strategien an, mit denen Entwickler ihre Logs in Elasticsearch verwalten können. Eine noch ausführlichere Übersicht finden Sie im unten gezeigten Video.

Video thumbnail

[Weiterführender Artikel: Nutzung von Elastic zur Verbesserung der Datenverwaltung und Observability in der Cloud]

Aufbereiten Ihrer Daten mit Elasticsearch

Manchmal haben wir keinen Einfluss darauf, welche Arten von Logs in unserem Cluster ankommen. Ein mögliches Beispiel ist ein Log-Analyse-Anbieter mit einem festgelegten Budget für die Speicherung von Kunden-Logs, der seinen Speicherbedarf verwalten muss (Elastic erlebt viele solcher Fälle beim Consulting). 

Oft indexieren unsere Kunden Felder „nur zur Sicherheit“, falls sie sie irgendwann einmal durchsuchen möchten. Wenn es Ihnen auch so ergeht, dann können Sie mit den folgenden Techniken möglicherweise Kosten einsparen und Ihre Cluster-Performance für tatsächlich wichtige Bereiche optimieren.

Lassen Sie uns zunächst das Problem genau definieren. Angenommen, Sie haben das folgende JSON-Dokument mit drei Feldern: message, transaction.user, transaction.amount:

{
 "message": "2023-06-01T01:02:03.000Z|TT|Bob|3.14|hello",
 "transaction": {
   "user": "bob",
   "amount": 3.14
 }
}

Das Mapping für einen Index, der diese Art von Dokumenten enthält, könnte in etwa so aussehen:

PUT dynamic-mapping-test
{
 "mappings": {
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

Mit Elasticsearch können wir jedoch neue Felder indexieren, ohne unbedingt zuvor ein Mapping anzugeben. Dies ist ein großer Vorteil von Elasticsearch, da wir neue Daten mühelos integrieren können. Wir können also Daten indexieren, die vom ursprünglichen Mapping abweichen, wie etwa:

POST dynamic-mapping-test/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field with arbitrary data"
 }
}

Mit GET dynamic-mapping-test/_mapping können wir das resultierende neue Mapping für den Index anzeigen. Dort sehen wir transaction.field3 als text und als keyword – wir haben also zwei neue Felder.

{
 "dynamic-mapping-test" : {
   "mappings" : {
     "properties" : {
       "transaction" : {
         "properties" : {
           "user" : {
             "type" : "keyword"
           },
           "amount" : {
             "type" : "long"
           },
           "field3" : {
             "type" : "text",
             "fields" : {
               "keyword" : {
                 "type" : "keyword",
                 "ignore_above" : 256
               }
             }
           }
         }
       },
       "message" : {
         "type" : "text"
       }
     }
   }
 }
}

Das ist großartig, führt aber zu einem neuen Problem: Wenn wir keine Kontrolle darüber haben, was an Elasticsearch gesendet wird, kann es leicht zu übermäßigem Mapping kommen. Nichts hält Sie davon ab, untergeordnete Felder in mehreren Ebenen mit denselben beiden Typen text und keyword zu erstellen:

POST dynamic-mapping-test/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field",
   "field4": {
     "sub_user": "a sub field",
     "sub_amount": "another sub field",
     "sub_field3": "yet another subfield",
     "sub_field4": "yet another subfield",
     "sub_field5": "yet another subfield",
     "sub_field6": "yet another subfield",
     "sub_field7": "yet another subfield",
     "sub_field8": "yet another subfield",
     "sub_field9": "yet another subfield"
   }
 }
}

Wir verschwenden Arbeitsspeicher und Festplattenspeicher für die Speicherung dieser Felder, da Datenstrukturen erstellt werden, um sie durchsuchbar und aggregierbar zu machen. Möglicherweise werden diese Felder niemals verwendet und existieren „nur zur Sicherheit“ für den Fall, dass sie irgendwann einmal für die Suche verwendet werden. 

Wenn wir beim Consulting gebeten werden, einen Index zu optimieren, analysieren wir zunächst die Nutzung aller Felder im Index, um herauszufinden, welche davon tatsächlich für die Suche verwendet werden und welche einfach nur Ressourcen verschwenden.

Strategie Nr. 1: Verwenden von strict

Wenn wir vollständige Kontrolle über die Struktur der in Elasticsearch gespeicherten Logs und deren Speicherung benötigen, können wir eine eindeutige Mapping-Definition festlegen. In diesem Fall wird alles, was von dieser Definition abweicht, einfach nicht gespeichert. 

Mit dynamic: strict auf der obersten Ebene oder in einem untergeordneten Feld können wir Dokumente ablehnen, die nicht dem Inhalt unserer mappings-Definition entsprechen, und den Absender zwingen, das vordefinierte Mapping einzuhalten:

PUT dynamic-mapping-test
{
 "mappings": {
   "dynamic": "strict",
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

Wenn wir anschließend versuchen, unser Dokument mit einem zusätzlichen Feld zu indexieren ...

POST dynamic-mapping-test/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field"
   }
 }
}

... erhalten wir die folgende Antwort:

{
 "error" : {
   "root_cause" : [
     {
       "type" : "strict_dynamic_mapping_exception",
       "reason" : "mapping set to strict, dynamic introduction of [field3] within [transaction] is not allowed"
     }
   ],
   "type" : "strict_dynamic_mapping_exception",
   "reason" : "mapping set to strict, dynamic introduction of [field3] within [transaction] is not allowed"
 },
 "status" : 400
}

Wenn Sie ganz sicher sind, dass Sie nur den Inhalt Ihrer Mappings speichern möchten, können Sie den Absender mit dieser Strategie zwingen, das vordefinierte Mapping einzuhalten.

Strategie Nr. 2: Etwas weniger strict

Mit "dynamic": "false" können wir etwas flexibler sein und Dokumente durchgehen lassen, auch wenn sie nicht genau dem erwarteten Format entsprechen.

PUT dynamic-mapping-disabled
{
 "mappings": {
   "dynamic": "false",
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

Mit dieser Strategie akzeptieren wir alle eingehenden Dokumente, indexieren jedoch nur die im Mapping angegebenen Felder. Alle weiteren Felder sind einfach nicht durchsuchbar. Wir verschwenden also keinen Arbeitsspeicher für die neuen Felder, sondern nur Festplattenspeicher. Die Felder werden weiterhin in den hits einer Suche angezeigt, inklusive einer top_hits-Aggregation. Wir können sie jedoch nicht durchsuchen oder zum Aggregieren verwenden, da keine Datenstrukturen für ihre Inhalte erstellt wurden.

Sie müssen sich nicht komplett für einen der Ansätze verwenden, sondern können beispielsweise strict auf der Stammebene verwenden und in einem untergeordneten Feld neue Felder annehmen, ohne sie zu indexieren. In unserer Dokumentation unter „dynamic“ für untergeordnete Objekte festlegen (Setting dynamic on inner objects) finden Sie dazu sehr ausführliche Informationen.

PUT dynamic-mapping-disabled
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "message": {
        "type": "text"
      },
      "transaction": {
        "dynamic": "false",
        "properties": {
          "user": {
            "type": "keyword"
          },
          "amount": {
            "type": "long"
          }
        }
      }
    }
  }
}

Strategie Nr. 3: Laufzeitfelder

Elasticsearch unterstützt sowohl Schema-on-Read als auch Schema-on-Write, jeweils mit Vor- und Nachteilen. Mit dynamic:runtime werden neue Felder als Laufzeitfelder zum Mapping hinzugefügt. Wir indexieren die im Mapping angegebenen Felder und machen die zusätzlichen Felder erst zur Abfragezeit durchsuchbar/aggregierbar. Wir verschwenden also vorab keinen Arbeitsspeicher für die neuen Felder, nehmen dafür jedoch langsamere Abfragen in Kauf, da die Datenstrukturen zur Abfragezeit erstellt werden.

PUT dynamic-mapping-runtime
{
 "mappings": {
   "dynamic": "runtime",
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

Lassen Sie uns das große Dokument indexieren:

POST dynamic-mapping-runtime/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field",
   "field4": {
     "sub_user": "a sub field",
     "sub_amount": "another sub field",
     "sub_field3": "yet another subfield",
     "sub_field4": "yet another subfield",
     "sub_field5": "yet another subfield",
     "sub_field6": "yet another subfield",
     "sub_field7": "yet another subfield",
     "sub_field8": "yet another subfield",
     "sub_field9": "yet another subfield"
   }
 }
}

Mit GET dynamic-mapping-runtime/_mapping sehen wir, dass unser Mapping beim Indexieren des großen Dokuments verändert wird:

{
 "dynamic-mapping-runtime" : {
   "mappings" : {
     "dynamic" : "runtime",
     "runtime" : {
       "transaction.field3" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_amount" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field3" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field4" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field5" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field6" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field7" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field8" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field9" : {
         "type" : "keyword"
       }
     },
     "properties" : {
       "transaction" : {
         "properties" : {
           "user" : {
             "type" : "keyword"
           },
           "amount" : {
             "type" : "long"
           }
         }
       },
       "message" : {
         "type" : "text"
       }
     }
   }
 }
}

Die neuen Felder sind jetzt wie ein normales keyword-Feld durchsuchbar. Beachten Sie, dass der Datentyp beim Indexieren des ersten Dokuments zu erraten versucht wird. Dies kann jedoch auch mit dynamischen Vorlagen gesteuert werden.

GET dynamic-mapping-runtime/_search
{
 "query": {
   "wildcard": {
     "transaction.field4.sub_field6": "yet*"
   }
 }
}

Ergebnis:

{
…
 "hits" : {
   "total" : {
     "value" : 1,
     "relation" : "eq"
   },
   "hits" : [
     {
       "_source" : {
         "message" : "hello",
         "transaction" : {
           "user" : "hey",
           "amount" : 3.14,
           "field3" : "hey there, new field",
           "field4" : {
             "sub_user" : "a sub field",
             "sub_amount" : "another sub field",
             "sub_field3" : "yet another subfield",
             "sub_field4" : "yet another subfield",
             "sub_field5" : "yet another subfield",
             "sub_field6" : "yet another subfield",
             "sub_field7" : "yet another subfield",
             "sub_field8" : "yet another subfield",
             "sub_field9" : "yet another subfield"
           }
         }
       }
     }
   ]
 }
}

Hervorragend! Diese Strategie ist offensichtlich sehr hilfreich, wenn Sie nicht genau wissen, welche Arten von Dokumenten Sie ingestieren werden. Laufzeitfelder sind daher ein konservativer Ansatz mit einem guten Kompromiss zwischen Leistung und Mapping-Komplexität.

Hinweis zu Kibana und Laufzeitfeldern

Wenn wir bei der Suche in der Kibana-Suchleiste das gewünschte Feld nicht angeben (z. B. "hello" anstelle von "message: hello"), dann gibt die Suche alle Felder zurück, inklusive der deklarierten Laufzeitfelder. Dieses Verhalten ist vermutlich nicht erwünscht, darum muss unser Index die dynamische Einstellung index.query.default_field verwenden. Geben Sie dort alle oder einen Teil der Felder aus dem Mapping an und legen Sie fest, dass die Laufzeitfelder explizit abgefragt werden müssen (z. B. "transaction.field3: hey").

Unser aktualisiertes Mapping sieht also zuletzt so aus:

PUT dynamic-mapping-runtime
{
  "mappings": {
    "dynamic": "runtime",
    "properties": {
      "message": {
        "type": "text"
      },
      "transaction": {
        "properties": {
          "user": {
            "type": "keyword"
          },
          "amount": {
            "type": "long"
          }
        }
      }
    }
  },
  "settings": {
    "index": {
      "query": {
        "default_field": [
          "message",
          "transaction.user"
        ]
      }
    }
  }
}

Die beste Strategie

Jede Strategie hat ihre Vor- und Nachteile, darum hängt die Auswahl der besten Strategie letztendlich von Ihrem Anwendungsfall ab. Die folgende Zusammenfassung hilft Ihnen, die richtige Wahl für Ihre Anforderungen zu treffen:

Strategie

Vorteile

Nachteile

Nr. 1 – strict

Gespeicherte Dokumente stimmen garantiert mit dem Mapping überein.

Dokumente mit Feldern, die nicht im Mapping deklariert sind, werden abgelehnt.

Nr. 2 – dynamic: false

Gespeicherte Dokumente können beliebig viele Felder haben, aber nur im Mapping definierte Felder verbrauchen Ressourcen.

Nicht im Mapping definierte Felder können nicht durchsucht oder zum Aggregieren verwendet werden.

Nr. 3 – Laufzeitfelder

Alle Vorteile von Nr. 2

Laufzeitfelder können in Kibana wie alle anderen Felder verwendet werden.

Etwas langsamere Antwortzeiten, wenn die Laufzeitfelder abgefragt werden

Observability ist eine der wichtigsten Stärken des Elastic Stack. Egal ob Sie Finanztransaktionen über Jahre hinweg sicher speichern und betroffene Systeme überwachen oder jeden Tag mehrere Terabyte an Netzwerkmetriken ingestieren, unsere Kunden nutzen Observability zehnmal schneller zu einem Bruchteil der Kosten. 

Sie möchten Elastic Observability näher kennenlernen? Dazu empfehlen wir Ihnen eine Cloudlösung. Starten Sie Ihren kostenlosen Test der Elastic Cloud noch heute!