ES索引设计&查询优化

1,495 阅读5分钟

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 对比验证

  1. 压测任务编写; 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