原文链接:
https://blog.csdn.net/wlei0618/article/details/125835129
https://blog.csdn.net/wlei0618/article/details/120800632
https://blog.csdn.net/m0_50008952/article/details/123532395
Result window is too large, from + size must be less than or equal to [10000] but was [xxxxx].
网上你能搜到的大部分解决方法都是让你把max_result_window参数阈值调大。
PUT /_settings
{
"index.max_result_window":
}
PUT _all/_settings
{
"index.max_result_window":
}
max_result_window
参数的解释没错,这的确能解决眼前问题,但是会带来严重的后果,最常见的就是后期频繁的 OOM,而且很难发现原因。
看一下官方对该参数的解释:
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/8.3/paginate-search-results.html#search-after
正常情况下ES的分页代码如实下面这样的:
GET order_2290w/_search
{
"from": 0,
"size": 5
}
输出结果如下图:
很好理解,即查询第一页的5
条数据。图中数字2即返回的五条文档数据。但是如果我们查询的数据页数特别大,达到什么程度呢?当from + size
大于10000
的时候,就会出现问题,如下图报错信息所示:
报错信息的解释为当前查询的结果超过了10000的最大值。那么疑问就来了,明明只查询了5条数据,为什么它计算最大值要加上我from的数量呢?而且Elasticsearch不是号称PB及数据秒级查询,几十亿的数据都没问题,怎么还限制最大查询前10000条数据呢?这里有一个字很关键:“前”,前10000条意味着什么?意味着数据肯定是按照某种顺序排列的,ES中如果不人工指定排序字段,那么最终结果将按照相关度评分排序。
max_result_window
参数的正确理解max_result_window是分页返回的最大数值,默认值为10000。max_result_window本身是对JVM的一种保护机制,通过设定一个合理的阈值,避免初学者分页查询时由于单页数据过大而导致OOM
在很多业务场景中经常需要查询10000条以后的数据,当遇到不能查询10000条以后的数据的问题之后,网上的很多答案会告诉你可以通过放开这个参数的限制,将其配置为100万,甚至1000万就行。
但是如果仅仅放开这个参数就行,那么这个参数限制的意义有何在呢?如果你不知道这个参数的意义,很可能导致的后果就是频繁的发生OOM而且很难找到原因
max_result_window
参数那么这个参数就完全不能动吗?当然不是,设置一个合理的参数阈值是需要通过你的各项指标参数来衡量确定的,比如你用户量、数据量、物理内存的大小、分片的数量等等。通过监控数据和分析各项指标从而确定一个最佳值,并非越大越好。
建议如果你不是对 ES 有足够的了解和使用经验,不要轻易修改max_result_window参数的阈值!
分布式系统都面临着同一个问题,数据的排序不可能在同一个节点完成。一个简单的需求,比如:
从10万
名高考生中查询成绩为的10001-10100
位的100
名考生的信息。
看似简单的查询其实并不简单,我们来画图解释一下:
假设10万名考生的考试信息被存放在一个exam_info索引中,由于索引数据在写入是并无法判断在执行业务查询时的具体排序规则,因此排序是随机的。而由于ES的分片和数据分配策略为了提高数据在检索时的准确度,会把数据尽可能均匀的分布在不同的分片。假设此时我们有五个分片,每个分片中承载2万条有效数据。按照需求我们需要去除成绩在10001到10100的一百名考生的信息,就要先按照成绩进行倒序排列。然后按照page_size: 100&page_index: 101进行查询。即查询按照成绩排序,第101页的100位学员信息。
单机数据库的查询逻辑很简单,先按照把10万学生成绩排序,然后从前10100
条数据数据中取出第10001-10100
条。即按照100为一页的第101页数据。
但是分布式数据库不同于单机数据库,学员成绩是被分散保存在每个分片中的,你无法保证要查询的这一百位学员的成绩一定都在某一个分片中,结果很有可能是存在于每个分片。换句话说,你从任意一个分片中取出的前10100位学员的成绩,都不一定是总成绩的前10100。更不幸的是,唯一的解决办法是从每个分片中取出当前分片的前10100名学员成绩,然后汇总成50500条数据再次排序,然后从排序后的这50500个成绩中查询前10100的成绩,此时才能保证一定是整个索引中的成绩的前10100名。
如果还不理解,我再举个例子用来类比:从保存了世界所有国家短跑运动员成绩的索引中查询短跑世界前三,每个国家类比为一个分片的数据,每个国家都会从国家内选出成绩最好的前三位参加最后的竞争,从每个国家选出的前三名放在一起再次选出前三名,此时才能保证是世界的前三名。
从上面案例中不难看出,每次有序的查询都会在每个分片中执行单独的查询,然后进行数据的二次排序,而这个二次排序的过程是发生在heap中的,也就是说当你单次查询的数量越大,那么堆内存中汇总的数据也就越多,对内存的压力也就越大。这里的单次查询的数据量取决于你查询的是第几条数据而不是查询了几条数据,比如你希望查询的是第10001-10100这一百条数据,但是ES必须将前10100全部取出进行二次查询。因此,如果查询的数据排序越靠后,就越容易导致OOM(Out Of Memory)情况的发生,频繁的深分页查询会导致频繁的FGC。
ES为了避免用户在不了解其内部原理的情况下而做出错误的操作,设置了一个阈值,即max_result_window,其默认值为10000,其作用是为了保护堆内存不被错误操作导致溢出。因此也就出现了文章一开始所演示的问题。
5.3 max_result_window
参数max_result_window是分页返回的最大数值,默认值为10000。max_result_window本身是对JVM的一种保护机制,通过设定一个合理的阈值,避免初学者分页查询时由于单页数据过大而导致OOM。
在很多业务场景中经常需要查询10000条以后的数据,当遇到不能查询10000条以后的数据的问题之后,网上的很多答案会告诉你可以通过放开这个参数的限制,将其配置为100万,甚至1000万就行。但是如果仅仅放开这个参数就行,那么这个参数限制的意义有何在呢?如果你不知道这个参数的意义,很可能导致的后果就是频繁的发生OOM而且很难找到原因,设置一个合理的大小是需要通过你的各项指标参数来衡量确定的,比如你用户量、数据量、物理内存的大小、分片的数量等等。通过监控数据和分析各项指标从而确定一个最佳值,并非越大约好。
目前人类对抗疾病最有效的手段:打疫苗。没错,能方式发生的问题总比发生之后再治理来的强。同样,解决深度分页问题最好的办法也是预防,也就是能避免最好是避免使用深度分页。我相信不服气的小伙儿伴已经满嘴质疑了,我们怎么能要求用户去做什么、不做什么呢?用户想深度分页检索你凭什么不让呢?技术要服务于业务!不能用妥协用户体验来解决技术问题…
带着这些质疑,我们先来看一看众多大型搜索引擎面对深度分页问题是如何处理的:
首先是以百度和谷歌为代表的全文搜索引擎:
谷歌、百度目前作为全球和国内做大的搜索引擎。不约而同的在分页条中删除了“跳页”功能,其目的就是为了避免用户使用深度分页检索。
这里也许又双叒叕会有人不禁发问:难道删除“跳页”就能阻止用户查询很多页以后的数据了吗?我直接狂点下一页不也是深度分页?好我暂时先不反驳这里的提问,但是我也发出一个反问,至少删除跳页,可以阻挡哪些刻意去尝试深度分页的“恶意用户”,真正想通过搜索引擎来完成自己检索需求的用户,通常来说都会首先查看第一页数据,因为搜索引擎是按照“相关度评分”进行排名的,也就是说,第一页的数据很往往是最符合用户预期结果的(暂时不考虑广告、置顶等商业排序情况)。
下面我们再看一下以中国最大电商平台“淘宝”为代表的垂直搜索引擎是怎么解决的:
我们分别尝试搜索较大较为宽泛的商品种类,以使其召回结果足够多。
虽然这里没有删除“跳页”功能,但这里可以看到一个有趣的现象,不管我们搜索什么内容,只要商品结果足够多,返回的商品列表都是仅展示前100页的数据,我们不难发现,其实召回的商品被“截断”了,不管你有多少,我都只允许你查询前100页,其实这本质和ES中的max_result_window作用是一样的,都是限制你去搜索更深页数的数据。
手机端APP就更不用说了,直接是下拉加载更多,连分页条都没有,相当于你只能点击“下一页”。
那么回到当初的问题,我们牺牲了用户体验了吗?
不仅没有,而且用户体验大大提升了!
首先那些直接输入很大的页码,直接点击跳页的用户,本身就是恶意用户,阻止其行为是理所应当,因此删除“跳页”,功能并无不妥!
其次,真正的通过搜索引擎来检索其意向数据的用户,只关心前几页数据,即便他通过分页条跳了几页,但这种搜索并不涉及深度分页,即便它不停的点下去,我们也有其它方案解决此问题。
类似淘宝这种直接截断前100页数据的做法,看似暴力,其实是在不牺牲用户体验的前提下,极大的提升了搜索的性能,这也变相的为哪些“正常用户”,提升了搜索体验,何乐不为?
from+size查询在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。
scroll,就是为解决深分页问题,而出现,scroll查询每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,直到数据读取完毕或者scroll_id保留时间截止,所以scroll并不适用于有跳页的情景。
注意:请求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用。
根据官方文档的说法,scroll的搜索上下文会在scroll的保留时间截止后自动清除,但是我们知道scroll是非常消耗资源的,所以一个建议就是当不需要了scroll数据的时候,尽可能快的把scroll_id显式删除掉。
清除指定的scroll_id:
DELETE _search/scroll/scroll编号
清除所有的scroll:
DELETE _search/scroll/_all
scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。
search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。