1.背景
- 小组项目在搭建属于自己的业务定制搜索服务;
- 项目原先接入了平台部门的通用搜索服务,在进行全链路压测时,es搜索成为瓶颈;
- 故统一在业务定制搜索服务中进行整改优化;
2.现象
1.压测条件:并发线程数 10 * 30
(并发线程数 * 节点数)下,出现大量查询超时(>500ms
),基本一压就挂;
2.通过日志查询执行dsl语句,如下,在多查询条件下,包含大量
match
条件的bool
评分查询语句:
3.手动执行dsl,进一步确认dsl语句本身执行就很慢:
4. es在对该请求进行分片缓存后,时延降低:
5.但压测条件下仍然出现大量时延(分片级别缓存失效条件,后面会分析),该dsl本身具有很大的优化空间:
6.而平常生产环境偶发延迟,本身原搜索平台的qps较低(15左右),所以问题没有被放大出来,但风险仍在:
3.问题分析
1.从日志中获取慢dsl语句如下:
(业务字段用field1,field2,field3
表示)
{
"query":{
"bool":{
"filter":[
{
"match":{
"field1":"111"
}
},
{
"match":{
"field2":"0"
}
},
{
"match":{
"field3":"3"
}
}
],
"must":[
{
"bool":{
"should":[
{
"match":{
"field1":"222"
}
},
{
"match":{
"field1":"333"
}
},
{
"match":{
"field1":"444"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field2":"555"
}
},
{
"match":{
"field2":"666"
}
},
{
"match":{
"field2":"777"
}
},
{
"match":{
"field2":"888"
}
},
{
"match":{
"field2":"999"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field2":"111"
}
},
{
"match":{
"field2":"222"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field3":"333"
}
},
{
"match":{
"field3":"a"
}
},
{
"match":{
"field3":"b"
}
},
{
"match":{
"field3":"c"
}
},
{
"match":{
"field3":"d"
}
},
{
"match":{
"field3":"e"
}
},
{
"match":{
"field3":"f"
}
},
{
"match":{
"field3":"g"
}
},
{
"match":{
"field3":"h"
}
},
{
"match":{
"field3":"i"
}
},
{
"match":{
"field3":"j"
}
},
{
"match":{
"field3":"k"
}
},
{
"match":{
"field3":"l"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field2":"111"
}
},
{
"match":{
"field2":"222"
}
},
{
"match":{
"field2":"333"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field2":"111"
}
},
{
"match":{
"field2":"222"
}
},
{
"match":{
"field2":"333"
}
},
{
"match":{
"field2":"444"
}
},
{
"match":{
"field2":"555"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field2":"666"
}
},
{
"match":{
"field2":"777"
}
}
],
"minimum_should_match":"1"
}
},
{
"bool":{
"should":[
{
"match":{
"field3":"a"
}
},
{
"match":{
"field3":"b"
}
},
{
"match":{
"field3":"c"
}
},
{
"match":{
"field3":"d"
}
},
{
"match":{
"field3":"e"
}
},
{
"match":{
"field3":"f"
}
},
{
"match":{
"field3":"g"
}
},
{
"match":{
"field3":"h"
}
},
{
"match":{
"field3":"i"
}
},
{
"match":{
"field3":"j"
}
},
{
"match":{
"field3":"k"
}
},
{
"match":{
"field3":"l"
}
},
{
"match":{
"field3":"m"
}
}
],
"minimum_should_match":"1"
}
}
]
}
},
"_source":[
"uid"
],
"sort":[
{
"sort":{
"order":"desc"
}
}
]
}
2.查询条件基本落在了must的bool子查询,这类查询并不会走Node Query Cache(Filter Cache)查询,而是走Shard Request Cache分片级别缓存,并被单个分片实例使用;
3.该缓存的实现在indicesRequestCache类中,缓存的key是一个复合结构,主要包括cacheEntity, readerVersion, cacheKey信息:
其中,缓存cacheKey:
- 主要是整个客户端请求的请求体(
source
)和请求参数(preference、indexRoutings、requestCache
等)。 - 由于客户端请求信息直接序列化为二进制作为缓存 key 的一部分,所以客户端请求的 json 顺序,聚合名称等变化都会导致
cache
无法命中。 本次dsl嵌套match数目会随着搜索数目条件变化而变化,本身作为缓存key容易导致cache无法命中,呈现压测“缓存不生效”的现象,导致查询时延很高;
4.QueryCache结构:
exp:对chapterIds和knowledgeIds字段的两个子查询,Lucene
在查询过程中遍历每一个segemnt, 检查其是否命中。segment3命中了对chapterIds字段缓存,segment5命中对chapterIds和knowledgeIds;
segment3没有命中knowledgeIds字段,将会执行查询过程:
4.解决
本次优化从两个方面考虑,索引设计,dsl优化;
4.1 索引设计优化
结构设计
- 检索实体扁平化,以使用单索引搜索优势;
- 目前资源服务对于全文搜索能力要求并不高,较多使用于多条件精确查询,将不需要进行全文检索的字符串字段均设置为
keyword
,以支持精确多条件匹配检索; - 业务扩展字段,使用
Object
类型以高效实现对象关系查询;
{
"mappings":{
"_doc":{
"properties":{
"field1":{
"type":"keyword"
},
"field2":{
"type":"keyword"
},
"textField":{
"analyzer":"ik_max_word",
"type":"text",
"fields":{
"keyword":{
"ignore_above":256,
"type":"keyword"
}
}
},
"objectExtend":{
"type":"object"
}
}
}
}
}
4.2 DSL优化
Filter + term + constant_score + range
- 根据逻辑组装精确多条件搜索
terms
精确匹配语句,将先前大量must bool
中的条件提取到filter
中; - 评分查询在dsl查询中是比较耗时的阶段,而只有在使用全文检索条件下,该值对于业务才是有意义的,故在精确多条件搜索下,不计算评分,使用
constant_score+filter
子句跳过评分阶段(全文检索fieldText
字段则需加入bool
评分子查询,但不计算精确匹配部分评分); - 使用range限定搜索范围;
Filter缓存说明:
- Filter查询会使用到Node Query Cache节点级别缓存,缓存key为filter子查询结构(query clause),该缓存很容易被业务使用到;
- 使用位图bitset结构进行匹配结果缓存,与单个分片上下文无关,可以被单个搜索里的所有分片实例所重用;
- 对于一个segment的doc数量需要大于10000,并且占整个分片的3%以上才会走cache策略;
4.3 对比验证
- 压测任务编写;
2.为了对比本次压测环境下
dsl
变更前后的性能差异,未优化前搜索dsl
如下:
{
"query": {
"bool" : {
"must" : [
{
"bool" : {
"should" : [
{
"term" : {
"field1" : {
"value" : "111",
"boost" : 1.0
}
}
},
{
"term" : {
"field1" : {
"value" : "222",
"boost" : 1.0
}
}
}
],
"adjust_pure_negative" : true,
"minimum_should_match" : "1",
"boost" : 1.0
}
},
{
"bool" : {
"should" : [
{
"term" : {
"field2" : {
"value" : "222",
"boost" : 1.0
}
}
},
{
"term" : {
"field2" : {
"value" : "222",
"boost" : 1.0
}
}
}
],
"adjust_pure_negative" : true,
"minimum_should_match" : "1",
"boost" : 1.0
}
},
{
"bool" : {
"should" : [
{
"term" : {
"appCode" : {
"value" : "a",
"boost" : 1.0
}
}
}
],
"adjust_pure_negative" : true,
"minimum_should_match" : "1",
"boost" : 1.0
}
}
]}
}
}
3.并发线程数:1000x4(并发线程数x节点数)
,效果与原线上类似,复现类似缓存失效的效果,出现大量查询超时:
4.进行dsl优化:
{
"from":0,
"size":5,
"query":{
"bool":{
"filter":[
{
"terms":{
"field1":[
"111",
"222"
],
"boost":1
}
},
{
"terms":{
"field2":[
"222",
"333"
],
"boost":1
}
},
{
"terms":{
"appCode":[
"a"
],
"boost":1
}
}
],
"adjust_pure_negative":true,
"boost":1
}
},
"version":true,
"_source":{
"includes":[
"uid"
],
"excludes":[
]
},
"sort":[
{
"sort":{
"order":"desc"
}
}
]
}
同样压测条件下进行验证,平均耗时在6ms左右:(文档数量在几百万左右)
执行计划:命中四个分片,执行非评分constantScore查询后加入结果集返回:
查看监控数据,单节点ES;
查询速率也较为稳定:
CPU负载稳定在61%:
平均时延低:
总结
1.业务静态索引设计需要根据业务场景,定义好字段类型,区分精确和全文检索,结构扁平化等;
2.dsl尽可能使用filter过滤器缓存,根据业务场景去编写,这里使用:Filter + term + constant_score + range