Elasticsearch 作为一个检索查询的文档数据库,在查询统计方面有着较为自由且丰富的方式方法。在满足各类查询聚合功能的同时,也会有着资源性能使用的浪费或者误区。在功能和性能使用中寻求最优解,诞生了 ES 慢查询治理这个场景问题。本文主要介绍了 ES 慢查询自动化巡检治理的具体内容和基础逻辑,并对巡检中的判断规则进行解读和相应的解决建议。
慢查询 DSL 的获取主要依赖于慢查询日志的清洗聚合。主要有下面几个操作:
这里通过程序脚本从腾讯云同步日志信息到本地的 ES 集群,并根据日志类型进行拆分要素。
拆分主要利用 ES 的 ingest pipeline。grok表达式:
"\\[%{DATA:classname}\\] \\[%{DATA:node}\\] \\[%{DATA:indexname}\\]\\[%{NUMBER:shard_no}\\] took\\[%{DATA:took}\\], took_millis\\[%{NUMBER:took_millis}\\], total_hits\\[%{DATA:hits}\\], types\\[%{DATA:types}\\], stats\\[%{DATA:stats}\\], search_type\\[%{DATA:search_type}\\], total_shards\\[%{NUMBER:total_shards}\\], source\\[%{DATA:Message}\\], id\\[%{DATA:id}\\], "
可以获得的日志结构如下:
{
"logType": "SearchSlow",
"took": "271.7ms",
"total_shards": "12",
"types": "_doc",
"took_millis": "271",
"Message": "{\"size\":1000,\"query\":{\"bool\":{\"must\":[{\"term\":{\"type1\":{\"value\":\"type1\",\"boost\":1.0}}},{\"term\":{\"id\":{\"value\":123,\"boost\":1.0}}}],\"must_not\":[{\"terms\":{\"tag1\":[\"tag1\",\"tag2\",\"tag2\"],\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"_source\":false}",
"Ip": "9.20.83.243",
"shard_no": "4",
"Cluster": "test-online",
"Time": "2022-06-27T22:34:59.428+08:00",
"search_type": "QUERY_THEN_FETCH",
"hits": "-1",
"node": "node1",
"classname": "i.s.s.fetch ",
"indexname": "test_fetch",
"stats": "",
"Level": "WARN",
"id": ""
}
主要字段:
DSL 聚合的目的是为了将慢日志中的 DSL 去重,保留有效且单一的 DSL。慢日志中每条 DSL 都保留了传入参数,这就造成了简单的聚合并不能获取唯一的 DSL 内容,即实际的 DSL 查询条件。比如下面的查询我们可以认为是一个 DSL 内容,只是传入的参数不一样:
{"query":{"term":{"_id":{"value":"1"}}}}
{"query":{"term":{"_id":{"value":"max"}}}}
因此我们使用脚本进行正则匹配把传入参数消除,并通过生成 MD5 进行唯一性校验。
# 正则匹配去除 dsl 中的传入参数
pattern = r'("max_expansions":)"[0-9]+"'
replacement = r'\1""'
query_dsl = re.sub(pattern, replacement, query_dsl)
# 生成 md5 作为 dsl 的唯一性 id
str_md5 = hashlib.md5(cluster_index_query.encode(encoding='UTF-8')).hexdigest()
最终效果如下:
```
{"size":"","query":{"bool":{"must":[{"term":{"id1":{"value":"",}}},{"terms":{"tag_id":[""],}}],"adjust_pure_negative":true,}},"aggregations":{"by_tag_id":{"terms":{"field":"tag_id","size":"",,,"show_term_doc_count_error":false,"order":[{"order1":"desc"},{"_key":"asc"}]},"aggregations":{"count_id":{"value_count":{"field":"id"}}}}}}
```
在获取实际 DSL 查询条件的同时,我们也保留了DSL 明细与耗时,通过取耗时中位数来保留带入参数的 DSL。接下来,我们利用 ES/lucene 提供的 profile 工具进行解析判断。
Profile API 让用户深入了解搜索请求是如何在低级别执行的,以便用户可以理解为什么某些请求很慢,并采取措施来改进它们。请注意, 除其他外,配置文件 API 不会测量网络延迟、搜索获取阶段花费的时间、请求在队列中花费的时间或在协调节点上合并分片响应时花费的时间。主要的结构如下:
{
"profile": {
"shards": [
{
"id": "[2aE02wS1R8q_QFnYu6vDVQ][my-index-000001][0]",
"searches": [
{
"query": [...], # query 阶段的耗时分析
"rewrite_time": 51443, # 对 dsl 解析重写的耗时
"collector": [...] # collector 阶段的耗时
}
],
"aggregations": [...] # 聚合阶段的耗时
}
]
}
}
在 profile 反馈的结果中,包含整个 search 阶段和各个子阶段。
对于不同版本的 ES, profile API 提供的信息内容也略有不同。这里基于当前主要使用的 7.10 版本,有以下内容是 profile 没有提供的:慢日志记录了 DSL 查询的明细,profile 提供了 DSL 的执行详情,仿佛两者相结合就能方便的判断出 DSL 的不合理之处。
但其实没那么简单,profile的本质是一个lucene collector,所以仅仅统计lucene 耗时,不包含任何网络交互式,队列等待。慢日志实际上是某个分片查询达到数据节点开始计时的。因此两者并不能完整的契合到一起。
每日慢查询巡检结果会根据集群写入同个 excel 文件,每个去除参数后的 DSL 作为一个单独的 sheet。
然后根据每个集群登记的联系人员 ldap,把 excel 邮件发送。ES/lucene 在底层的算法结构上,分别使用了 fst 去实现精确匹配和bkd tree 去实现范围查询。两者在dsl中的查询方式就是 term keyword 和 range long。
相对而已错误的查询方式就是 term long 和 range keyword。ES 中数值类型(代表为 long 类型)存储时并没有采用倒排表索引,而是以value为序,将docid切分到不同的block里面,这样处理可以利用Block k-d tree进行范围查找,速度非常快。但是满足查询条件的 docid 集合在磁盘上并非向 Postlings list 那样按照 docid 顺序存放,也就无法利用 BKD tree 的算法优势了。而此时要实现对 docid 集合的快速 advance 操作,只能将 docid 集合拿出来,然后构造成一个代表 docid 集合的 bitset ,在这个 bitset 上进行 advance 操作。如果命中结果集很大,这个代价会非常大,有可能会造成 cpu 跑满的现象。这里的 term long 查询则被转换为了 PointRangeQuery。
查询子阶段类型为 PointInSetQuery 或者 PointRangeQuery
在 ES 中 keyword 类型是倒排结构的,在精确查询的场景下,fst 的查询算法能达到存储和性能的最优解。但是在范围查询的场景下,ES 的处理方式可以理解为简单的遍历,和 wildcard 查询一样,是极其耗费性能的。
查询子阶段类型为 MultiTermQueryConstantScoreWrapper
本文主要阐述了在大批量 ES 集群下进行慢查询治理的一个方案。在自动化巡检下,发现集群查询中可以优化的内容和方向,降低排查成本。其中慢查询的判断规则和优化手段也随着慢查询治理的深入而进一步完善。
在生产环境下慢查询的产生因素是复杂多样的。除了基本的 profile 分析,火焰图 /heap dump 等方式也是有效手段,在实际排查过程中也避免不了这类操作的介入。
面对不同的慢查询原因,优化手段也是因地制宜,往往也是需要研发运维甚至产品等各方面同学的支持。