1.背景
2020年以来内容标注后果搜寻就是社区中后盾业务的外围高频应用场景之一,为了撑持简单的后盾搜寻,咱们将社区内容的要害信息额定存了一份到Elasticsearch中作为二级索引应用。随着标注业务的细分、迭代和工夫的推移,这个索引的文档数和搜寻的RT开始逐渐回升。上面是这个索引以后的监控状况。
本文介绍社区利用IndexSorting,将亿级文档搜寻性能由最开始2000ms优化到50ms的过程。如果大家遇到类似的问题和场景,置信看完之后肯定可能一行代码成吨收益。
2.摸索过程
2.1 初步优化
最开始需要很简略,只须要取最新公布的动静分页展现。这时候实现也是简略粗犷,满足性能即可。查问语句如下:
{
"track_total_hits": true,
"sort": [
{
"publish_time": {
"order": "desc"
}
}
],
"size": 10
}
因为首页加载时没加任何筛选条件,于是变成了从亿级内容库中找出最新公布的10条内容。
针对这个查问很容易发现问题呈现在大后果集的排序,要解决问题,天然的想到了两条门路:
- 去掉sort
- 放大后果集
通过用户诉求和开发成本的衡量后,过后决定“先扛住,再优化”:在用户关上首页的时候,默认减少“公布工夫在最近一周内”的筛选条件,这时语句变成了:
{
"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的理解还比拟根底,只能说会用、晓得分片、倒排索引、相关性打分,而后就没有了。总之咱们有了方向,开始奋起直追。
2.2 粗疏打磨
2.2.1 常识积攒
带着之前遗留的问题,咱们开始开始从新登程,从头学习ES。要优化搜寻性能,首先咱们要晓得的是搜寻是怎么做的。上面咱们就以一个最简略的搜寻为例,拆解一下整个搜寻申请的过程。
(1)搜寻申请
{
"track_total_hits":false,
"query": {
"bool": {
"filter": [
{
"term": {
"category_id.keyword": "xxxxxxxx"
}
}
]
}
},
"size": 10
}
准确查问category_id为”xxxxxxxx”的文档,取10条数据,不须要排序,不须要总数
总流程分3步:
- 客户端发动申请到Node1
- Node1作为协调节点,将申请转发到索引的每个主分片或副分片中,每个分片在本地执行查问。
- 每个节点返回各自的数据,协调节点汇总后返回给客户端
如图能够大抵描述这个过程:
咱们晓得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。
Lucene index由许许多多的Segment组成,每一个Segment外面蕴含着文档的Term字典、Term字典的倒排表、文档的列式存储DocValues以及正排索引。它可能独立的间接对外提供搜寻性能,简直是一个放大版的Lucene index。
(3)Term字典和倒排表
上图是Term字典和其倒排表的大抵样子当然这里还有些重要数据结构,比方:
- FST:term索引,在内存中构建。能够疾速实现单Term、Term范畴、Term前缀和通配符查问。
- BKD-Tree:用于数值类型(包含空间点)的疾速查找。
- SkipList:倒排表的数据结构
这外面的细节比拟多,感兴趣的能够独自理解,这里不影响咱们的整体搜寻流程,不过多赘述。有了Term字典和倒排表咱们就能间接拿到搜寻条件匹配的后果集了,接下来只须要通过docID去正排索引中取回整个doc而后返回就完事儿了。这是ES的根本盘实践上不会慢,咱们猜想慢查问产生在排序上。那给申请加一个排序会产生什么呢?比方:
{
"track_total_hits":false,
"query": {
"bool": {
"filter": [
{
"term": {
"category_id.keyword": "xxxxxxxx"
}
}
]
}
},
"sort": [
{
"publish_time": {
"order": "desc"
}
}
],
"size": 10
}
通过倒排表拿到的docId是无序的,当初指定了排序字段,最简略间接的方法是全副取出来,而后排序取前10条。这样诚然能实现成果,然而效率却是可想而知。那么Lucene是怎么解决的呢?
(4)DocValues
倒排索引可能解决从词到文档的疾速映射,但须要对检索后果进行分类、排序、数学计算等聚合操作时须要文档号到值的疾速映射。而正排索引又过于臃肿宏大,怎么办呢?
这时候各位大佬可能就间接想到了列式存储,没有错,Lucene就引入了基于docId的列式存储构造——DocValues
文档号 | 列值 | 列值映射 |
---|---|---|
0 | 2023-01-13 | 2 |
1 | 2023-01-12 | 1 |
2 | 2023-03-13 | 3 |
比方上表中的DocValues=[2023-01-13, 2023-01-12,2023-03-13]
如果列值是字符串,Lucene会把原来的字符串值依照字典排序生成数字ID,这样的预处理能进一步放慢排序速度。于是咱们失去了DocValues=[2, 1, 3]
Docvalues的列式存储模式能够放慢咱们的遍历的速度。到这里一个惯例的搜寻取前N条记录的申请算是真正的拆解实现。这里不探讨词频、相关性打分、聚合等性能的剖析,所以本文对整个过程和数据结构做了大幅简化。如果对这部分感兴趣,欢送一起探讨。
此时排序慢的问题也逐步浮出了水面:只管Docvalues又是列式存储,又是将简单值预处理为简略值防止了查问时的简单比拟,然而仍旧架不住咱们须要排序的数据集过大。
看起来ES尽力了,它如同的确不善于解决咱们这个场景的慢查问问题。
不过有灵性的各位读者必定想到了,如果能把倒排表依照咱们预先指定的顺序存储好,就能省下整个排序的工夫。
2.2.2 IndexSorting
很快ES官网文档《How to tune for search speed》中提到了一个搜寻优化伎俩——索引排序(Index Sorting)呈现在了咱们的视线中。
从文档上的形容咱们能够晓得,索引排序对于搜寻性能的晋升次要在两个方面:
- 对于多条件并列查问(a and b and …),索引排序能够帮忙咱们把不符合条件的文档存在一起,跳过大量的不匹配的文档。然而此技巧仅实用于常常用于筛选的低基数字段。
- 提前中断:当搜寻排序和索引排序指定的程序一样时,只须要比拟每个段的前 N 个文档,其余的文档仅须要用于总数计算。比方:咱们的文档中有一个工夫戳,而咱们常常须要依照工夫戳来搜寻和排序,这时候如果指定的索引排序和搜寻排序统一,通常可能极大的进步搜寻排序的效率。
提前中断!!!几乎是缺什么来什么,于是咱们开始围绕这一点开展调研。
(1)开启索引排序
{
"settings": {
"index": {
"sort.field": "publish_time", // 可指定多个字段
"sort.order": "desc"
}
},
"mappings": {
"properties": {
"content_id": {
"type": "long"
},
"publish_time": {
"type": "long"
},
...
}
}
}
如下面的例子,文档在写入磁盘时会依照 publish_time 字段的递加序进行排序。
在后面的段落中咱们重复提到了docID和正排索引。这里咱们顺带简略介绍下他们的关系,首先Segment中的每个文档,都会被调配一个docID,docID从0开始,程序调配。在没有IndexSorting时,docID是依照文档写入的程序进行调配的,在设置了IndexSorting之后,docID的程序就与IndexSorting的程序统一。
下图形容了docID和正排索引的关系:
那么再次回头来看看咱们最开始的查问:
{
"track_total_hits":true,
"sort": [
{
"publish_time": {
"order": "desc"
}
}
],
"size": 10
}
在Lucene中进行查问时,发现后果集的倒排表程序刚好是publish_time降序排序的,所以查问到前10条数据之后即可返回,这就做到了提前中断,省下了排序开销。那么代价是什么呢?
(2)代价
IndexSorting和查问时排序不一样,实质是在写入时对数据进行预处理。所以排序字段只能在创立时指定且不可更改。并且因为写入时要对数据进行排序,所以也会对写入性能也会有肯定负面影响。
之前咱们提到了Lucene自身对排序也有各种优化,所以如果搜寻后果集自身没有那么多的数据,那么就算不开启这个性能,也能有不错的RT。
另外因为少数时候还是要计算总数,所以开启索引排序之后只能提前中断排序过程,还是要对后果集的总数进行count。如果可能不查总数,或者说通过另外的形式获取总数,那么可能更好的利用这个个性。
小结:
- 针对大后果集的排序取前N条的场景下,索引排序能显著进步搜寻性能。
- 索引排序只能在创立索引时指定,不可更改。如果你有多个指定字段排序的场景,可能须要谨慎抉择排序字段。
- 不获取总数能更好的利用索引排序。
- 开启索引排序会肯定水平升高写性能。 这里贴一条ElaticsearchBenchmarks的数据截图供大家参考。
见:Elasticsearch Benchmarks
2.3 成果
因为咱们的业务远远没有达到ES的写入瓶颈,而且也少有频繁变更排序字段的场景。在通过短暂的衡量之后,确定索引排序正是咱们须要的,于是开始应用线上实在数据对索引排序的成果进行简略的性能测试。
(1)性能测试:首页
(2)性能测试:其余
这里开启索引排序后,随机几个惯例条件和工夫窗口的搜寻组合测试
能够看到成果非常明显,没有以前的那种尖刺,RT也很稳固,于是咱们决定正式上线这个性能。
(3)线上成果
慢查问
!
整体前后比照
和咱们预期的根本一样,搜寻RT大幅升高,慢查问齐全隐没。
2.4 后续优化
在摸索过程中,其实还发现了一些其余的优化伎俩,鉴于开发成本和收益,有些咱们并没有齐全利用于生产环境。这里列出其中几点,心愿能给大家一些启发。
- 不获取总数: 大部分场景下,不查问总数都能缩小开销,进步性能。ES 7.x之后的搜寻接口默认不返回总数了,由此可见一斑。
- 自定义routing规定: 从上文的查问过程咱们能够看到,ES会轮询所有分片以获取想要的数据,如果咱们能控制数据的分片落点,那么也能节俭不少开销。比如说:如果咱们未来如果有大量的场景都是查某个用户的动静,那么能够管制依照用户分片,这样就防止了分片轮询,也能晋升搜寻效率。
- keyword: 不是所有的数字都应该依照数值字段来存,如果你的数字值很少用于范畴查问,然而常常被用作term查问,并且对搜寻rt很敏感。那么keyword才是最适宜的存储形式。
- 数据预处理:就像IndexSoting一样,如果咱们可能在写入时预处理好数据,也能节俭搜寻时的开销。这一点配合
_ingest/pipeline
兴许能施展意想不到的成果。
3.写在最初
置信看到这里的大家都能看出,咱们的优化中也没有波及到非常浅近的技术难点,咱们只是在解决问题的过程中,逐渐从小白转变成了一个初学者。来一个大牛兴许从一开始就能间接绕过咱们的弯路,不过万里之行始于足下,最初这里总结一点教训和感触分享给大家,心愿能给与咱们一样的初学者一些参考。
ES在大后果集指定字段排序的场景下性能不佳,咱们应用时应该尽量避免呈现这种场景。如果无奈防止,适合的IndexSorting设置能大幅晋升排序性能。
优化永无止境,衡量好老本和收益,集中资源解决最优先和重要的问题才是咱们应该做的。
文:海带
本文属得物技术原创,来源于:得物技术官网
未经得物技术许可严禁转载,否则依法追究法律责任!
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。