Es的字段过多的坑

本文总阅读量

前记

前几天,突然发现线上Es有大量的错误日志,核心日志是Limit of total fields [1000] in index.经过查明发现是同事存api server日志时他是数据带有有很多field,导致报错,解决这个问题很简单,又却很复杂,最好的解决方案是分开存储,让Es只做好Es的职责,但是对于架构又复杂了.

前记

前几天,突然发现线上Es有大量的错误日志,核心日志是Limit of total fields [1000] in index.经过查明发现是同事存api server日志时他是数据带有有很多field,导致报错,解决这个问题很简单,又却很复杂,最好的解决方案是分开存储,让Es只做好Es的职责,但是对于架构又复杂了.

1.发生问题

前几天在准备增加Logstash的机器部署时发现旧Logstash机器上面有一些错误日志,核心的信息是Limit of total fields [1000] in index.很容易理解,就是本次发送的数据字段数量大于1000了.

题外话,之所以会有这样大的数据量是因为同事那边发送的日志数据里有一个动态的json对象,而对于es来说{‘a’: {‘b’: 1, ‘c’: 2}}会认为有a.b,和a.c两个字段.在发现问题后,把含有该动态json对象的字段解析为文本就好了(不过没事还是不要存那么多数据).

2.解决问题

解决这个问题也非常的简单只要针对有问题的索引处理即可,如下是把限制放宽到2000:

1
curl -X PUT  -H "Content-Type: application/json" -d '{"index.mapping.total_fields.limit":2000}' http://127.0.0.1:9200/so1n_index/_settings

如果需要永久解决,则需要给索引增加temple,并执行以下命令:

1
curl -X PUT  -H "Content-Type: application/json" -d '{"template": "so1n-*","settings":{"index.mapping.total_fields.limit":2000}}' http://127.0.0.1:9200/_template/logstash

好了问题就到此解决了,不过如果是简单的解决问题,那我就是在水文章了,接下来当然是迎接一个新的坑

3.坑

在处理完问题后的第二天发现被应用了index.mapping.total_fields.limitgrafana和自建报表平台都查不出数据了,而在Kibana上面使用KQL查询时,则是可以查询数据的.

对问题进行排查与梳理后发现,只要使用query语句查询时,就会无法查询数据,同时会有类似的报错:field expansion matches too many fields, limit: 1024, got: 1889.
一开始看到这个报错的想法是:写入的限制通过改索引的配置就可以解决了,那读取的限制也可以通过改索引来解决.然而事实上并没有想象中的容易,找了一圈都找不到可以通过改索引的配置来解决查询数据字段过多的限制.

最后发现有一个改法,需要更改elasticsearch.yml的一个配置indices.query.bool.max_clause_count,然后滚动重启集群.好处是可以马上解决,坏处是解决成本太高了,所以放弃该方法,换成另一个方法:删除字段过长的数据.

想着那些过长的数据都没啥用,只是为了出问题时能排查问题,那现在直接删掉就可以了.然而在删除数据后,问题并没有解决…

4.避坑

回想一下,目前查询中只有用到es的query字符串查询方法才有问题,而使用KQL查询时,是能正常查询到数据的,那么一定是query字符串的查询机制与字段过多而引发限制.

先看一看出问题的query字符串查询的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"size": 0,
"query": {
"bool": {
"filter": [
{
"range": {
"@timestamp": {
"gte": start_time,
"lte": end_time,
"format": "epoch_millis"
}
}
},
{
"query_string": {
"analyze_wildcard": True,
"query": {query str},
}
}
]
}
},
"aggs": ...
}

看了之后会发现还是找不到问题,那就看看query_string的相关文档吧.在文档中发现有对参数default_field的描述

1
2
3
4
5
6
7
8
9
(Optional, string) Default field you wish to search if no field is provided in the query string.

Defaults to the index.query.default_field index setting, which has a default value of *. The * value extracts all fields that are eligible for term queries and filters the metadata fields. All extracted fields are then combined to build a query if no prefix is specified.

Searching across all eligible fields does not include nested documents. Use a nested query to search those documents.

For mappings with a large number of fields, searching across all eligible fields could be expensive.

There is a limit on the number of fields that can be queried at once. It is defined by the indices.query.bool.max_clause_count search setting, which defaults to 1024.

大概就是说如果没填default_field时,会默认按照*多所有字段进行匹配,如果字段数量超过indices.query.bool.max_clause_count的限制则会抛错.

由于自己的项目中有一个公有字段log_level,所以只要把参数body按下面更改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"size": 0,
"query": {
"bool": {
"filter": [
{
"range": {
"@timestamp": {
"gte": start_time,
"lte": end_time,
"format": "epoch_millis"
}
}
},
{
"query_string": {
"analyze_wildcard": True,
"query": {query str},
"default_field": "log_level" <-- changed
}
}
]
}
},
"aggs": ...
}

如果由于设计的问题,导致每个查询的字段相差比较大,没有公有字段,那么还是更改限制并重启吧.

5.解决方案

上面只是从写入和读取层解决问题的,但需要注意的是,我们不应该把大量非正常使用的数据全塞入Es.首先从ES的查询原理来看看为什么不应该这样处理?.

Es在查询数据时,会把命中查询的数据全部都拉到内存里面,并根据语句进行计算与统计.当数据的字段变多时,对应的数据量就更多了,Es就需要更多的内存来存放数据,如果此时机器不够(一般不能配置机器内存大于32g),旧的数据就会被清掉,下次查询时又得重新从硬盘里拉取符合条件的数据.
简单来说Es读取热数据(内存数据)的速度远远大于读取冷数据(硬盘数据)的速度,如果单个数据量越大或者内存不够大,那么会影响到Es的查询速度和效率,除此之外,Es数据的每个字段都是索引,而这些会占用了大量的空间.

了解完上面那些,我们就可以知道我们不应该把所有数据都存在于Es,特别是数据量比较多的时候.我们应该对数据进行拆分,把每个数据存到他应该呆的地方.我们可以按照上面的说法来拆分两大类数据,一类是索引型数据,也就是存在Es中的可以被快速查询到的数据;一类型是详情型数据,这类数据类似于商品信息数据,数据量很大,记录了商品的说明等等,可以不用存入Es中被索引,可以存在Mysql或者Hbase之中.除此之外,还有一种介于索引性数据详情型数据辅助型数据,他可以被Es做聚合运算或者充当辅助查询,这类型数据必须存在与Es之中,也可以存在于数据库之中,如果没办法确定就先放Es里吧.

做好数据类型拆分后,我们就需要给架构添加复杂性了…
如图,我们需要在后端服务与Es之间添加一个中间件,这里中间件负责的是在写数据时把数据进行拆分,把索引性数据辅助型数据发送到Es,并拿到写入成功的id与详情型数据一起写入数据库.而在查的时候需要把查询语句转发到Es进行查询,并拿到查询成功的id从数据库拉取数据并返回给用户.
![Untitled Diagram](/home/so1n/Downloads/Untitled Diagram.png)

虽然描述起来简单,但是实现还是比较复杂的,比如发给Es的数据需要内部保存,每隔一段时间bluk一次,同时要防止数据保存期间丢失.还有中间件接受到数据时该如何拆分数据,怎么制订方案等都是比较复杂同时又得结合自己本身项目来制订.

查看评论