一.背景
某天用户反馈集群负载很高,CPU资源处于持续被打满的状态,对于elasticsearch集群的业务请求也频繁超时,大量请求失败。
二.排查过程
通过监控,我们发现集群重要任务均为查询任务,通过集群慢日志我们发现集群在大量运行wildcard请求。耗时从数十秒到数分钟不等。在大量请求期间,集群的CPU load,磁盘IO均处于高位运行。
样例语句如下:
代码语言:javascript复制GET index/_search
{
"query": {
"wildcard": {
"name_zh": {
"wildcard": "*xx科技*"
}
}
},
"from": 0,
"size": 10
}
我们对慢查询语句通过profile解析后发现,耗时主要集中于wildcard模糊匹配阶段。
三.问题原因
wildcard通配符模糊匹配查询需要使用正排索引,类似于关系型数据库中的“like”操作。在wildcard查询对数据进行匹配的过程中需要匹配很多类型的数据,所以整体耗时都会很长。通过匹配字符串的方式对数据进行过滤查询。与elasticsearch使用倒排索引加速查询的理念背道而驰。虽然elasticsearch提供了wildcard这种字符串模糊匹配的能力,但是我们不建议使用该方法对elasticsearch进行查询。
1.使用wildcard查询可能造成的潜在问题
- 性能问题:通配符查询需要扫描所有的文档,因此对于大型索引,这可能会导致查询变慢。
- 精度问题:由于通配符查询会匹配所有符合条件的结果,因此可能会返回很多不相关的结果。
- 内存问题:通配符查询需要在内存中维护正则表达式,如果正则表达式太复杂,可能会导致内存不足。
因此,通配符查询应该谨慎使用,尽量避免在大型索引上使用,并且应该使用更精确的查询方式来提高查询性能和结果的准确性。
代码语言:javascript复制public WildcardQueryBuilder(String fieldName, String value) {
if (Strings.isEmpty(fieldName)) {
throw new IllegalArgumentException("field name is null or empty");
}
if (value == null) {
throw new IllegalArgumentException("value cannot be null");
}
this.fieldName = fieldName;
this.value = value;
}
在该构造函数中判断传入wildcard Query中的字段名与搜索关键字是否为空,如果字段名为空,则抛出 IllegalArgumentException
异常。如果值为null
,则抛出IllegalArgumentException
异常。否则,将字段名和值赋值给相应的成员变量。 以便于后续构造QueryBuilder。
public static WildcardQueryBuilder fromXContent(XContentParser parser) throws IOException {
String fieldName = null;
String rewrite = null;
String value = null;
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY;
String queryName = null;
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
throwParsingExceptionOnMultipleFields(NAME, parser.getTokenLocation(), fieldName, currentFieldName);
fieldName = currentFieldName;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else {
if (WILDCARD_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
value = parser.text();
} else if (VALUE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
value = parser.text();
} else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
boost = parser.floatValue();
} else if (REWRITE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
rewrite = parser.textOrNull();
} else if (CASE_INSENSITIVE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
caseInsensitive = parser.booleanValue();
} else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
queryName = parser.text();
} else {
throw new ParsingException(
parser.getTokenLocation(),
"[wildcard] query does not support [" currentFieldName "]"
);
}
}
}
} else {
throwParsingExceptionOnMultipleFields(NAME, parser.getTokenLocation(), fieldName, parser.currentName());
fieldName = parser.currentName();
value = parser.text();
}
}
在构造WildcardQueryBuilder的这段代码中,从XContentParser解析WildcardQueryBuilder的字段名、值和其他参数,并使用这些参数构建一个WildcardQueryBuilder对象。它根据字段名匹配相应的参数,并将其赋值给相应的变量。最后,将构造的WildcardQueryBuilder对象返回。
代码语言:javascript复制public WildcardQueryBuilder caseInsensitive(boolean caseInsensitive) {
this.caseInsensitive = caseInsensitive;
return this;
}
通过该方法来控制匹配的敏感度,如果传入的参数为true
,则表示进行大小写不敏感的匹配;如果传入的参数为false
,则表示进行大小写敏感的匹配。
四.使用建议
1.使用match查询语句
代码语言:javascript复制GET company/_search
{
"query": {
"match": {
"name_zh.word": {
"query": "xx科技"
}
}
},
"from": 0,
"size": 10
}
2.如果需要短语匹配场景建议使用match_phrase短语匹配方式进行查询。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!