Elasticsearch:字段太多, 在 Elasticsearch 中防止映射爆炸的 3 种方法

433 阅读11分钟

当一个系统具有三样东西时,它就被称为“可观察的”:日志、指标和跟踪。 虽然指标和跟踪具有可预测的数据结构,但日志(尤其是应用程序日志)通常是非结构化数据,需要收集和解析才能真正有用。 因此,控制日志可以说是实现可观察性最难的部分。如果你想了解如何把一个数据进行结构化,请参考我之前的文章 “Elasticsearch:Elastic可观测性 - 运用 pipeline 使数据结构化”。你可以在 “Elastic:开发者上手指南” 查找更多的文章。

在本文中,我们将深入探讨开发人员可以用来通过 Elasticsearch 管理日志的三种有效策略。

[相关文章:利用 Elastic 改善云中的数据管理和可观察性]

让 Elasticsearch 为你的数据工作

有时我们无法控制我们在集群中收到的日志类型。 想想一个日志分析提供商,它有一个特定的预算来存储其客户的日志,并且需要保持存储空间(Elastic 在咨询中处理了许多类似的案例)。

通常情况下,我们有客户索引字段 “以防万一” 他们需要用于搜索。 如果你是这种情况,那么以下技术在帮助你降低成本并将集群性能集中在真正重要的事情上应该被证明是有价值的。

让我们首先概述问题。 考虑以下具有三个字段的 JSON 文档:message、transaction.user、transaction.amount:



1.  {
2.   "message": "2023-06-01T01:02:03.000Z|TT|Bob|3.14|hello",
3.   "transaction": {
4.     "user": "bob",
5.     "amount": 3.14
6.   }
7.  }


将保存此类文档的索引的映射可能类似于以下内容:



1.  PUT dynamic-mapping-test
2.  {
3.   "mappings": {
4.     "properties": {
5.       "message": {
6.         "type": "text"
7.       },
8.       "transaction": {
9.         "properties": {
10.           "user": {
11.             "type": "keyword"
12.           },
13.           "amount": {
14.             "type": "long"
15.           }
16.         }
17.       }
18.     }
19.   }
20.  }


但是,Elasticsearch 允许我们为新字段编制索引,而不必事先指定映射,这也是 Elasticsearch 易于使用的部分原因:我们可以轻松载入新数据。 因此,可以对偏离原始映射的内容进行索引,例如:



1.  POST dynamic-mapping-test/_doc
2.  {
3.   "message": "hello",
4.   "transaction": {
5.     "user": "hey",
6.     "amount": 3.14,
7.     "field3": "hey there, new field with arbitrary data"
8.   }
9.  }


GET dynamic-mapping-test/_mapping 将向我们展示生成的索引新映射。 它现在有 transaction.field3 作为文本和关键字——实际上是两个新字段。

GET dynamic-mapping-test/_mapping 


1.  {
2.   "dynamic-mapping-test" : {
3.     "mappings" : {
4.       "properties" : {
5.         "transaction" : {
6.           "properties" : {
7.             "user" : {
8.               "type" : "keyword"
9.             },
10.             "amount" : {
11.               "type" : "long"
12.             },
13.             "field3" : {
14.               "type" : "text",
15.               "fields" : {
16.                 "keyword" : {
17.                   "type" : "keyword",
18.                   "ignore_above" : 256
19.                 }
20.               }
21.             }
22.           }
23.         },
24.         "message" : {
25.           "type" : "text"
26.         }
27.       }
28.     }
29.   }
30.  }


太好了,但这现在是问题的一部分:当我们无法控制发送到 Elasticsearch 的内容时,我们很容易面临称为映射爆炸的问题。 没有什么可以阻止你创建子字段和子子字段,它们将具有相同的两种类型的文本和关键字,例如:



1.  POST dynamic-mapping-test/_doc
2.  {
3.   "message": "hello",
4.   "transaction": {
5.     "user": "hey",
6.     "amount": 3.14,
7.     "field3": "hey there, new field",
8.     "field4": {
9.       "sub_user": "a sub field",
10.       "sub_amount": "another sub field",
11.       "sub_field3": "yet another subfield",
12.       "sub_field4": "yet another subfield",
13.       "sub_field5": "yet another subfield",
14.       "sub_field6": "yet another subfield",
15.       "sub_field7": "yet another subfield",
16.       "sub_field8": "yet another subfield",
17.       "sub_field9": "yet another subfield"
18.     }
19.   }
20.  }


我们将浪费 RAM 和磁盘空间来存储这些字段,因为将创建数据结构以使它们可搜索和可聚合。 这些字段可能从未被使用过——它们 “以防万一” 它们需要用于搜索。

当被要求优化索引时,我们在咨询时采取的第一步是检查索引中每个字段的使用情况,以查看哪些字段真正被搜索,哪些只是浪费资源。

策略一:严格

如果我们想要完全控制我们存储在 Elasticsearch 中的日志结构以及我们如何存储它们,我们可以设置一个清晰的映射定义,这样任何与我们想要的不同的东西都不会被存储。

通过在顶级或某些子字段中使用 dynamic: strict ,我们拒绝与映射定义中的内容不匹配的文档,从而强制发送者遵守预定义的映射:



1.  PUT dynamic-mapping-test
2.  {
3.   "mappings": {
4.     "dynamic": "strict",
5.     "properties": {
6.       "message": {
7.         "type": "text"
8.       },
9.       "transaction": {
10.         "properties": {
11.           "user": {
12.             "type": "keyword"
13.           },
14.           "amount": {
15.             "type": "long"
16.           }
17.         }
18.       }
19.     }
20.   }
21.  }


然后当我们尝试用一个额外的字段索引我们的文档时......



1.  POST dynamic-mapping-test/_doc
2.  {
3.   "message": "hello",
4.   "transaction": {
5.     "user": "hey",
6.     "amount": 3.14,
7.     "field3": "hey there, new field"
8.     }
9.   }
10.  }


……我们得到的回应是这样的:



1.  {
2.   "error" : {
3.     "root_cause" : [
4.       {
5.         "type" : "strict_dynamic_mapping_exception",
6.         "reason" : "mapping set to strict, dynamic introduction of [field3] within [transaction] is not allowed"
7.       }
8.     ],
9.     "type" : "strict_dynamic_mapping_exception",
10.     "reason" : "mapping set to strict, dynamic introduction of [field3] within [transaction] is not allowed"
11.   },
12.   "status" : 400
13.  }


如果你绝对确定只想存储映射中的内容,则此策略会强制发送方遵守预定义的映射。

策略二:不太严格

我们可以更灵活一点,让文档通过,即使它们不是我们期望的那样,通过使用 "strict": "false"。



1.  PUT dynamic-mapping-disabled
2.  {
3.   "mappings": {
4.     "dynamic": "false",
5.     "properties": {
6.       "message": {
7.         "type": "text"
8.       },
9.       "transaction": {
10.         "properties": {
11.           "user": {
12.             "type": "keyword"
13.           },
14.           "amount": {
15.             "type": "long"
16.           }
17.         }
18.       }
19.     }
20.   }
21.  }


使用此策略时,我们接受所有出现的文档,但仅索引映射中指定的字段,使额外的字段根本无法搜索。 换句话说,我们不会在新字段上浪费 RAM,而只会浪费磁盘空间。 这些字段仍然可以在搜索命中中可见,其中包括 top_hits 聚合。 但是,我们无法搜索或聚合它们,因为没有创建数据结构来保存它们的内容。

它不需要是全有或全无——你甚至可以让根是严格的,并有一个子字段来接受新字段而不用索引它们。 我们的 Setting dynamic on internal objects 文档很好地涵盖了它。



1.  PUT dynamic-mapping-disabled
2.  {
3.    "mappings": {
4.      "dynamic": "strict",
5.      "properties": {
6.        "message": {
7.          "type": "text"
8.        },
9.        "transaction": {
10.          "dynamic": "false",
11.          "properties": {
12.            "user": {
13.              "type": "keyword"
14.            },
15.            "amount": {
16.              "type": "long"
17.            }
18.          }
19.        }
20.      }
21.    }
22.  }


使用此策略时,我们接受所有出现的文档,但仅索引映射中指定的字段,使额外的字段根本无法搜索。 换句话说,我们不会在新字段上浪费 RAM,而只会浪费磁盘空间。 这些字段仍然可以在搜索命中中可见,其中包括 top_hits 聚合。 但是,我们无法搜索或聚合它们,因为没有创建数据结构来保存它们的内容。

dynamicdoc indexed?fields searchablefields indexed?mapping updated?
trueYesYesYesYes
runtimeYesYesNo        No
falseYesNoNoNo
strictNo

它不需要是全有或全无——你甚至可以让根是严格的,并有一个子字段来接受新字段而不用索引它们。 我们的 Setting dynamic on internal objects 文档很好地涵盖了它。

策略三:运行时字段

Elasticsearch 支持读模式和写模式,每个都有它的警告。 使用 dynamic:runtime,新字段将作为运行时字段添加到映射中。 我们索引映射中指定的字段,并使额外字段仅在查询时可搜索/可聚合。 换句话说,我们不会在新字段上预先浪费 RAM,但我们会付出查询响应速度较慢的代价,因为数据结构将在运行时构建。

Elasticsearch 数据类型
JSON data type "dynamic": "true""dynamic": "runtime"
null不添加任何字段不添加任何字段
true 或者 falsebooleanboolean
doublefloatdouble
longlonglong
objectobject不添加任何字段
array依赖于数组里的第一个非 null 值依赖于数组里的第一个非 null 值
通过 date detection 的字符串datedate
通过 numeric detection 的字符串float 或者 longdouble 或者 long
不通过 date detection 或者 numberic detection 的字符串含有 .keyword 子字段的 text 类型keyword


1.  PUT dynamic-mapping-runtime
2.  {
3.   "mappings": {
4.     "dynamic": "runtime",
5.     "properties": {
6.       "message": {
7.         "type": "text"
8.       },
9.       "transaction": {
10.         "properties": {
11.           "user": {
12.             "type": "keyword"
13.           },
14.           "amount": {
15.             "type": "long"
16.           }
17.         }
18.       }
19.     }
20.   }
21.  }


让我们索引我们的大文档:



1.  POST dynamic-mapping-runtime/_doc
2.  {
3.   "message": "hello",
4.   "transaction": {
5.     "user": "hey",
6.     "amount": 3.14,
7.     "field3": "hey there, new field",
8.     "field4": {
9.       "sub_user": "a sub field",
10.       "sub_amount": "another sub field",
11.       "sub_field3": "yet another subfield",
12.       "sub_field4": "yet another subfield",
13.       "sub_field5": "yet another subfield",
14.       "sub_field6": "yet another subfield",
15.       "sub_field7": "yet another subfield",
16.       "sub_field8": "yet another subfield",
17.       "sub_field9": "yet another subfield"
18.     }
19.   }
20.  }


GET dynamic-mapping-runtime/_mapping 将显示我们的映射在索引我们的大文档时发生了变化:



1.  GET dynamic-mapping-runtime/_mapping 
2.  {
3.   "dynamic-mapping-runtime" : {
4.     "mappings" : {
5.       "dynamic" : "runtime",
6.       "runtime" : {
7.         "transaction.field3" : {
8.           "type" : "keyword"
9.         },
10.         "transaction.field4.sub_amount" : {
11.           "type" : "keyword"
12.         },
13.         "transaction.field4.sub_field3" : {
14.           "type" : "keyword"
15.         },
16.         "transaction.field4.sub_field4" : {
17.           "type" : "keyword"
18.         },
19.         "transaction.field4.sub_field5" : {
20.           "type" : "keyword"
21.         },
22.         "transaction.field4.sub_field6" : {
23.           "type" : "keyword"
24.         },
25.         "transaction.field4.sub_field7" : {
26.           "type" : "keyword"
27.         },
28.         "transaction.field4.sub_field8" : {
29.           "type" : "keyword"
30.         },
31.         "transaction.field4.sub_field9" : {
32.           "type" : "keyword"
33.         }
34.       },
35.       "properties" : {
36.         "transaction" : {
37.           "properties" : {
38.             "user" : {
39.               "type" : "keyword"
40.             },
41.             "amount" : {
42.               "type" : "long"
43.             }
44.           }
45.         },
46.         "message" : {
47.           "type" : "text"
48.         }
49.       }
50.     }
51.   }
52.  }


从上面,我们可以看出额外增加的字段被自动映射为 keyword 字段。这个和我们上面表格的最后的一行是符合的。

新字段现在可以像普通 keyword 字段一样进行搜索。 请注意,在索引第一个文档时会猜测数据类型,但这也可以使用动态模板进行控制。



1.  GET dynamic-mapping-runtime/_search
2.  {
3.   "query": {
4.     "wildcard": {
5.       "transaction.field4.sub_field6": "yet*"
6.     }
7.   }
8.  }


结果:



1.  {
2.3.   "hits" : {
4.     "total" : {
5.       "value" : 1,
6.       "relation" : "eq"
7.     },
8.     "hits" : [
9.       {
10.         "_source" : {
11.           "message" : "hello",
12.           "transaction" : {
13.             "user" : "hey",
14.             "amount" : 3.14,
15.             "field3" : "hey there, new field",
16.             "field4" : {
17.               "sub_user" : "a sub field",
18.               "sub_amount" : "another sub field",
19.               "sub_field3" : "yet another subfield",
20.               "sub_field4" : "yet another subfield",
21.               "sub_field5" : "yet another subfield",
22.               "sub_field6" : "yet another subfield",
23.               "sub_field7" : "yet another subfield",
24.               "sub_field8" : "yet another subfield",
25.               "sub_field9" : "yet another subfield"
26.             }
27.           }
28.         }
29.       }
30.     ]
31.   }
32.  }


太棒了! 当你不知道要摄取哪种类型的文档时,很容易看出这种策略如何有用,因此使用运行时字段听起来像是一种保守的方法,在性能和映射复杂性之间进行了很好的权衡。

关于使用 Kibana 和运行时字段的注意事项

请记住,如果我们在 Kibana 上使用其搜索栏搜索时未指定字段(例如,仅输入 “hello” 而不是 “message: hello”,则该搜索将匹配所有字段,并且包括所有我们声明的运行时字段(runtime fields)。你可能不希望这种行为,因此我们的索引必须使用动态设置 index.query.default_field。将其设置为我们映射的所有或部分字段,并让运行时字段显式查询( 例如,“transaction.field3:hey”)。

我们更新的映射最终将是:



1.  PUT dynamic-mapping-runtime
2.  {
3.    "mappings": {
4.      "dynamic": "runtime",
5.      "properties": {
6.        "message": {
7.          "type": "text"
8.        },
9.        "transaction": {
10.          "properties": {
11.            "user": {
12.              "type": "keyword"
13.            },
14.            "amount": {
15.              "type": "long"
16.            }
17.          }
18.        }
19.      }
20.    },
21.    "settings": {
22.      "index": {
23.        "query": {
24.          "default_field": [
25.            "message",
26.            "transaction.user"
27.          ]
28.        }
29.      }
30.    }
31.  }


选择最佳策略

每种策略都有其优点和缺点,因此最佳策略最终将取决于你的具体用例。 以下是帮助你根据需要做出正确选择的摘要:

策略优点缺点
#1 - strict保证存储的文档符合映射如果文档具有未在映射中声明的字段,则文档将被拒绝
#2 - dynamic: false存储的文档可以有任意数量的字段,但只有映射的字段才会使用资源未映射的字段不能用于搜索或聚合
#3 - Runtime Fields

#2 的所有优点

运行时字段可以像任何其他字段一样在 Kibana 中使用

查询运行时字段时相对较慢的搜索响应时间

可观察性是 Elastic Stack 真正闪耀的地方。 无论是在跟踪受影响系统的同时安全地存储多年的金融交易,还是摄取数 TB 的日常网络指标,我们的客户都在以很少的成本将 Observability 提高十倍。

更多阅读