在ES中,text
类型的字段使用一种叫做fielddata
的查询时内存数据结构。当字段被排序,聚合或者通过脚本访问时这种数据结构会被创建。它是通过从磁盘读取每个段的整个反向索引来构建的,然后存存储在java的堆内存中。
fileddata
默认是不开启的。Fielddata
可能会消耗大量的堆空间,尤其是在加载高基数文本字段时。一旦fielddata
已加载到堆中,它将在该段的生命周期内保留。此外,加载fielddata
是一个昂贵的过程,可能会导致用户遇到延迟命中。这就是默认情况下禁用fielddata
的原因。如果尝试对文本字段进行排序,聚合或脚本访问,将看到以下异常:
GET /test_index/test_type/_search
{
"aggs": {
"group_by_test_field": {
"terms": {
"field": "test_field"
}
}
}
}
返回:
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "test_index",
"node": "4onsTYVZTjGvIj9_spWz2w",
"reason": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory."
}
}
],
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory."
}
},
"status": 400
}
对分词的field,直接执行聚合操作,会报错,大概意思是说,你必须要打开fielddata,然后将正排索引数据加载到内存中,才可以对分词的field执行聚合操作,而且会消耗很大的内存。
如果要对分词的field执行聚合操作,必须将fielddata
设置为true:
POST /test_index/_mapping/test_type
{
"properties": {
"test_field": {
"type": "text",
"fielddata": true
}
}
}
{
"test_index": {
"mappings": {
"test_type": {
"properties": {
"test_field": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"fielddata": true
}
}
}
}
}
}
keyword:
doc value
--> 不分词的所有field,可以执行聚合操作 --> 如果你的某个field不分词,那么在index-time,就会自动生成doc value
--> 针对这些不分词的field执行聚合操作的时候,自动就会用doc value
来执行。
text:
分词field,是没有doc value
的。在index-time
,如果某个field是分词的,那么是不会给它建立doc value
正排索引的,因为分词后,占用的空间过于大,所以默认是不支持分词field进行聚合的。
如果一定要对分词的field执行聚合,那么必须将fielddata=true
,然后es就会在执行聚合操作的时候,现场将field对应的数据,建立一份fielddata正排索引,fielddata正排索引的结构跟doc value
是类似的,但是只会将fielddata
正排索引加载到内存中来,然后基于内存中的fielddata
正排索引执行分词field的聚合操作。
为什么fielddata必须在内存?因为大家自己思考一下,分词的字符串,需要按照term进行聚合,需要执行更加复杂的算法和操作,如果基于磁盘和os cache,那么性能会很差。
一旦分词字符串被加载到 fielddata ,他们会一直在那里,直到被驱逐(或者节点崩溃)。由于这个原因,留意内存的使用情况,了解它是如何以及何时加载的,怎样限制对集群的影响是很重要的。
Fielddata 是 延迟 加载。如果你从来没有聚合一个分析字符串,就不会加载 fielddata 到内存中。此外,fielddata 是基于字段加载的, 这意味着只有很活跃地使用字段才会增加 fielddata 的负担。
然而,这里有一个令人惊讶的地方。假设你的查询是高度选择性和只返回命中的 100 个结果。大多数人认为 fielddata 只加载 100 个文档。
实际情况是,fielddata 会加载索引中(针对该特定字段的) 所有的文档,而不管查询的特异性。逻辑是这样:如果查询会访问文档 X、Y 和 Z,那很有可能会在下一个查询中访问其他文档。
与 doc values 不同,fielddata 结构不会在索引时创建。相反,它是在查询运行时,动态填充。这可能是一个比较复杂的操作,可能需要一些时间。 将所有的信息一次加载,再将其维持在内存中的方式要比反复只加载一个 fielddata 的部分代价要低。
JVM 堆 是有限资源的,应该被合理利用。 限制 fielddata 对堆使用的影响有多套机制,这些限制方式非常重要,因为堆栈的乱用会导致节点不稳定(感谢缓慢的垃圾回收机制),甚至导致节点宕机(通常伴随 OutOfMemory 异常)。
indices.fielddata.cache.size
控制为 fielddata
分配的堆空间大小。 当你发起一个查询,分析字符串的聚合将会被加载到 fielddata
,如果这些字符串之前没有被加载过。如果结果中 fielddata
大小超过了指定大小,其他的值将会被回收从而获得空间。
默认情况下,这个设置是禁用的,Elasticsearch 永远都不会从 fielddata 中回收数据。
这个默认设置是刻意选择的:fielddata
不是临时缓存。它是驻留内存里的数据结构,必须可以快速执行访问,而且构建它的代价十分高昂。如果每个请求都重载数据,性能会十分糟糕。
一个有界的大小会强制数据结构回收数据。
设想我们正在对日志进行索引,每天使用一个新的索引。通常我们只对过去一两天的数据感兴趣,尽管我们会保留老的索引,但我们很少需要查询它们。不过如果采用默认设置,旧索引的 fielddata
永远不会从缓存中回收! fieldata
会保持增长直到 fielddata
发生断熔,这样我们就无法载入更多的 fielddata
。
这个时候,我们被困在了死胡同。但我们仍然可以访问旧索引中的fielddata
,也无法加载任何新的值。相反,我们应该回收旧的数据,并为新值获得更多空间。
为了防止发生这样的事情,可以通过在 config/elasticsearch.yml
文件中增加配置为 fielddata 设置一个上限:
indices.fielddata.cache.size: 20%
有了这个设置,最久未使用(LRU)的 fielddata
会被回收为新数据腾出空间。
Fielddata 的使用可以被监控:
1).按索引使用 indices-stats API :
GET /_stats/fielddata?fields=*
2).按节点使用 nodes-stats API :
GET /_nodes/stats/indices/fielddata?fields=*
3).按索引节点:
GET /_nodes/stats/indices/fielddata?level=indices&fields=*
fielddata 大小是在数据加载 之后 检查的。 如果一个查询试图加载比可用内存更多的信息到 fielddata
中会发生什么?答案很丑陋:我们会碰到 OutOfMemoryException
。
Elasticsearch 包括一个 fielddata
断熔器 ,这个设计就是为了处理上述情况。 断熔器通过内部检查(字段的类型、基数、大小等等)来估算一个查询需要的内存。它然后检查要求加载的 fielddata
是否会导致 fielddata
的总量超过堆的配置比例。
如果估算查询的大小超出限制,就会 触发 断路器,查询会被中止并返回异常。这都发生在数据加载 之前 ,也就意味着不会引起 OutOfMemoryException
。
Elasticsearch 有一系列的断路器,它们都能保证内存不会超出限制:
indices.breaker.fielddata.limit
indices.breaker.request.limit
indices.breaker.total.limit
断路器的限制可以在文件 config/elasticsearch.yml
中指定,可以动态更新一个正在运行的集群:
PUT /_cluster/settings
{
"persistent" : {
"indices.breaker.fielddata.limit" : "40%"
}
}
关于给 fielddata 的大小加一个限制,从而确保旧的无用 fielddata 被回收的方法。 indices.fielddata.cache.size
和 indices.breaker.fielddata.limit
之间的关系非常重要。 如果断路器的限制低于缓存大小,没有数据会被回收。为了能正常工作,断路器的限制 必须 要比缓存大小要高。