• Elasticsearch搜索调优


    最近把搜索后端从AWS cloudsearch迁到了AWS ES和自建ES集群。测试发现search latency高于之前的benchmark,可见模拟数据远不如真实数据来的实在。这次在产线的backup ES上直接进行测试和优化,通过本文记录search调优的主要过程。

    问题1:发现AWS ES shard级别的search latency是非常小的,符合期望,但是最终的查询耗时却非常大(ES response的took), 整体的耗时比预期要高出200ms~300ms。

    troubleshooting过程:开始明显看出问题在coordinator node收集数据排序及fetch阶段。开始怀疑是因为AWS ES没有dedicated coordinator节点,data node的资源不足导致这部分耗时较多,后来给所有data node进行来比较大的升级,排除了CPU,MEM, search thread_pool等瓶颈,并且通过cloud watch排除了EBS IOPS配额不够的可能,但是,发现search latency并没有减少。然后就怀疑是network的延时, 就把集群从3个AV调整到1个AV,发现问题依旧。无奈,联系了AWS的support,AWS ES team拿我们的数据和query语句做了benchmark,发现没有某方面的资源瓶颈。这个开始让我们很疑惑,因为在自建ES集群上search latency明显小于AWS ES,两个集群的版本,规格,数据量都差不多。后来AWS回复说是他们那边的架构问题,比之自建集群,AWS ES为了适应公有云上的security, loadbalance要求,在整个请求链路上加了一些组件,导致了整体延时的增加。

    确定方案:限于ES cluster不受控,我们只能从自身的数据存储和查询语句上去优化。

    存储优化:

    1. index sort。我们的查询结果返回都是按时间(created_time)排序的,所以存储的时候即按created_time进行有序存储,方便单segment内的查询提前中断查询,提升查询效率。

    2. segment merge。索引是按季度存储的,把2019年之前的索引进行了force merge,进行段合并,2019年之前的索引确定都是只读的。

    3. 索引优化。合并了一些小索引,2016,2017年的数据量比较少,把这两年的索引进行合并,减少总shard数。通过创建原索引的别名指向新索引,保证search和index的逻辑不用改动。

    查询优化。

    先通过profile API定位耗时的子查询语句。

    1. 合并查询字段。一个比较耗时子查询查询如下,通常session_id的list size>100,receiver_id和sender_id也会匹配到n多条记录。

    {
        "minimum_should_match": "1"
        "should": [
            {
                "terms": {
                    "session_id": [
                        "ab",
                        "cd"
                    ],
                    "boost": 1
                }
            },
            {
                "term": {
                    "receiver_id": {
                        "value": "efg",
                        "boost": 1
                    }
                }
            },
            {
                "term": {
                    "sender_id": {
                        "value": "hij",
                        "boost": 1
                    }
                }
            }
        ]
    }

    新开一个字段session_receiver_sender_id,通过copy_to把每条记录的session_id,receiver_id, sender_id都放到这个字段上。把query语句改为

    {
        "terms": {
            "session_receiver_sender_id": [
                "ab",
                "cd",
                "efg",
                "hij"
            ],
            "boost": 1
        }
    }

    不过,测试结论显示,合并之后query耗时并没有明显缩短,感觉改动意义不大。推测可能是我们的BoolQuery字段并不多(就3个),但是terms的size很多(100以上),因为不管是多个字段每个对应一个termQuery,还是一个terms query, 都是转成BoolQuery,最终都是多个termQuery做or。

    2. 优化date range查询。另外一个比较耗时的查询是date range。Lucene会rewrite成一个DocValuesFieldExistsQuery。

    "filter": [
        {
            "range": {
                "created_time": {
                    "from": 1560993441118,
                    "to": null,
                    "include_lower": true,
                    "include_upper": true,
                    "boost": 1
                }
            }
        },
        ...
    ]

    这里匹配到的docId的确非常多,date range结果在构造docIdset与别的子查询语句做conjunction耗时较大。

    采用的一个解决方案是尽量对这个子查询进行缓存,把这个date range查询拆成两段,分为3个月前到昨天,昨天到今天两段,一般昨天的数据不再变化,在没有触发segment merge的情况下3个月前到昨天到查询结果应该能缓存较长时间。

    "constant_score": {
      "filter": {
        "bool": {
          "should": [
            {
              "range": {
                "created_time": {
                  "gte": "now-3M/d",
                  "lte": "now-1d/d"
                }
              }
            },
            {
              "range": {
                "created_time": {
                  "gte": "now-1d/d",
                  "lte": "now/d"
                }
              }
            }
          ]
        }
      }
    }

    相应的,在用户可接受的前提下,调大索引的refresh_interval。

    问题2: 在自建ES集群上,发现某个索引500ms以上的搜索耗时占比较多。

    这个索引每日大概30w次查询,落在100ms以内的查询超过90%,但是依旧有1%的查询落在500ms以上。发现同样的query语句模版,但如果某些子查询条件匹配到的数据比较多,查询会变对特别慢。

    troubleshooting过程:同样是通过profile参数分析比较耗时的查询子句。发现一个PointInSetQuery非常耗时,这个子查询是对一个名为user_type的Integer字段做terms查询,子查询内部又耗时在build_score阶段。

    通过查找lucene的代码和相关文章,发现lucene把numeric类型的字段索引成BKD-tree,内部的docId是无序的,与其他查询结果做交集前构造Bitset比较耗时,从而把Integer类型改成keyword,把这个查询转成TermQuery,这样哪怕命中的数据很多,在build_score的时候因为倒排链的docId有序性,利用skiplist,可以更快速的构建一个Bitset。在把这个字段改成keyword后,50th的查询耗时并没有多大差异,但是90th、99th的search latency明显小于之前。

    另一个优化,这个索引里的每条数据都是一个非空的accout_id字段,accout_id在query语句里会用于terms查询。遂把这个accout_id字段作为routing进行存储。同时可以对查询语句进行修改:

    #原query
    "filter": [
        {
            "terms": {
                "account_id": [
                    "abc123"
                ],
                "boost": 1
            }
        }
    ...
    ]
    #改为
    "filter": [
        {
            "terms": {
                "_routing": [
                    "abc123"
                ]
            }
        }
    ...
    ]

    查询改为_routing之后,发现整体的search latency大幅降低。

    经过这两次改动,针对这个索引的search latency基本满足需求。

    另外,还有一个小改动,通过preload docvalue, 可以减少首次查询的耗时。

  • 相关阅读:
    zip 中文文件夹为空问题
    webview长按文本区域不显示文字放大镜等方法
    crash
    精疲力尽先生的造访
    告别忙碌的2017,迎来更加忙碌的2018
    传说中的59分!!
    为什么我一定吵不过女人?
    人挪活!
    低谷时,请读书!
    java小入门的感觉
  • 原文地址:https://www.cnblogs.com/zhq1007/p/11689361.html
Copyright © 2020-2023  润新知