最近把搜索后端从AWS cloudsearch迁到了AWS ES和自建ES集群。测试发现search latency高于以前的benchmark,可见模拟数据远不如真实数据来的实在。此次在产线的backup ES上直接进行测试和优化,经过本文记录search调优的主要过程。node
问题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不受控,咱们只能从自身的数据存储和查询语句上去优化。session
存储优化:架构
1. index sort。咱们的查询结果返回都是按时间(created_time)排序的,因此存储的时候即按created_time进行有序存储,方便单segment内的查询提早中断查询,提高查询效率。测试
2. segment merge。索引是按季度存储的,把2019年以前的索引进行了force merge,进行段合并,2019年以前的索引肯定都是只读的。fetch
3. 索引优化。合并了一些小索引,2016,2017年的数据量比较少,把这两年的索引进行合并,减小总shard数。经过建立原索引的别名指向新索引,保证search和index的逻辑不用改动。优化
查询优化。ui
先经过profile API定位耗时的子查询语句。spa
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, 能够减小首次查询的耗时。