亿级ES数据搜索性能调优实践

1
背景
2020年以来内容标注结果搜索就是社区中后台业务的核心高频使用场景之一,为了支撑复杂的后台搜索,我们将社区内容的关键信息额外存了一份到Elasticsearch中作为二级索引使用 。随着标注业务的细分、迭代和时间的推移,这个索引的文档数和搜索的RT开始逐步上升 。
下面是这个索引当前的监控情况 。

亿级ES数据搜索性能调优实践

文章插图
本文介绍社区 利用IndexSorting,将亿级文档搜索性能由最开始2000ms优化到50ms的过程 。如果大家遇到相似的问题和场景,相信看完之后一定能够一行代码成吨收益 。
2
探索过程
2.1 初步优化
最开始需求很简单,只需要 取最新发布的动态分页展示 。这时候实现也是简单粗暴,满足功能即可 。查询语句如下:
GET /content-alias/_search { "track_total_hits": true, "sort": [ { "publish_time": { "order": "desc" } } ], "size": 10 }由于首页加载时没加任何筛选条件,于是变成了 从亿级内容库中找出最新发布的10条内容 。
针对这个查询很容易发现问题出现在大结果集的排序,要解决问题,自然的想到了两条路径:
  1. 去掉sort
  2. 缩小结果集
经过用户诉求和开发成本的权衡后,当时决定“先扛住,再优化”:在用户打开首页的时候,默认增加“发布时间在最近一周内”的筛选条件,这时语句变成了:
GET /content-alias/_search { "track_total_hits": true, "query": { "bool": { "filter": [ { "range": { "publish_time": { "gte": 1678550400, "lt": 1679155200 } } } ] } }, "sort": [ { "publish_time": { "order": "desc" } } ], "size": 10 }这个改动上线后,效果可以说是立竿见影, 首页加载速度立马降到了200ms以内,平均RT60ms 。这次改动也为我们减小了来自业务的压力,为后续的优化争取了不少调研的时间 。
虽然搜索首页的加载速度明显快了,但是并没有实际解决根本问题—— ES大结果集指定字段排序还是很慢 。对业务来说,结果页上的一些边界功能的体验依旧不能尽如人意,比如导出、全量动态的搜索等等 。这一点从监控上也能够较明显的看出:慢查询还是存在,并且还伴随着少量的接口超时 。
亿级ES数据搜索性能调优实践

文章插图
老实说这个时期我们对于ES的了解还比较基础,只能说会用、知道分片、倒排索引、相关性打分,然后就没有了 。总之我们有了方向,开始奋起直追 。
2.2 细致打磨2.2.1 知识积累
带着之前遗留的问题,我们开始开始重新出发,从头学习ES 。要优化搜索性能,首先我们要知道的是搜索是怎么做的 。下面我们就以一个最简单的搜索为例,拆解一下整个搜索请求的过程 。
(1)搜索请求
GET /content-alias/_search { "track_total_hits":false, "query": { "bool": { "filter": [ { "term": { "category_id.keyword": "xxxxxxxx" } } ] } }, "size": 10 }
精确查询category_id为"xxxxxxxx"的文档,取10条数据,不需要排序,不需要总数
总流程分3步:
  1. 客户端发起请求到Node1
  2. Node1作为协调节点,将请求转发到索引的每个主分片或副分片中,每个分片在本地执行查询 。
  3. 每个节点返回各自的数据,协调节点汇总后返回给客户端
如图可以大致描绘这个过程:
亿级ES数据搜索性能调优实践

文章插图
我们知道ES是依赖Lucene提供的能力,真正的搜索发生在Lucene中,还需要继续了解Lucene中的搜索过程 。
(2)Lucene
Lucene中包含了四种基本数据类型,分别是:
  • Index:索引,由很多的Document组成 。
  • Document:由很多的Field组成,是Index和Search的最小单位 。
  • Field:由很多的Term组成,包括Field Name和Field Value 。
  • Term:由很多的字节组成 。一般将Text类型的Field Value分词之后的每个最小单元叫做Term 。
在介绍Lucene index的搜索过程之前,这里先说一下组成Lucene index的最小数据存储单元——Segment 。


推荐阅读