限制破解:三種方案對(duì)比與實(shí)戰(zhàn))
Elasticsearch 搜索數(shù)量不能超過(guò)10000的解決方案但凡用 Elasticsearch 做過(guò)搜索或者導(dǎo)出功能的同學(xué)大概率都撞上過(guò)這面墻明明索引里有幾十萬(wàn)條數(shù)據(jù)一個(gè)match_all查下去hits.total卻只給你 10000再往后翻就報(bào)Result window is too large的錯(cuò)。第一次遇到的時(shí)候我也挺懵查了一下文檔才發(fā)現(xiàn)是index.max_result_window的默認(rèn)值在作怪。這其實(shí)不是 ES 在故意給你使絆子而是它在用這個(gè)限制保護(hù)集群的內(nèi)存和響應(yīng)速度。但業(yè)務(wù)可不管這些產(chǎn)品經(jīng)理一句“全量導(dǎo)出”你就得老老實(shí)實(shí)想辦法繞過(guò)去。這篇文章我就把這幾年來(lái)用過(guò)的、踩過(guò)的坑整理成一套完整的方案。不是單純告訴你“調(diào)參數(shù)就行”而是把為什么會(huì)有 10000 這個(gè)數(shù)、各種方案分別適合什么場(chǎng)景、參數(shù)怎么算、代碼怎么寫一次講清楚。不管你是剛接觸 ES 的新手還是已經(jīng)被深分頁(yè)折磨過(guò)的老手這篇應(yīng)該都能幫你省下不少試錯(cuò)的時(shí)間。1. 為什么搜索數(shù)量被鎖死在100001.1 從深分頁(yè)的代價(jià)說(shuō)起ES 的搜索本質(zhì)上是一個(gè)“分散收集”的過(guò)程。一個(gè)索引通常有多個(gè)分片你發(fā)一個(gè)查詢請(qǐng)求協(xié)調(diào)節(jié)點(diǎn)會(huì)把請(qǐng)求廣播到所有分片每個(gè)分片各自完成排序然后只返回自己排名靠前的from size條結(jié)果給協(xié)調(diào)節(jié)點(diǎn)協(xié)調(diào)節(jié)點(diǎn)再對(duì)這些結(jié)果做全局排序。問(wèn)題就出在這里。如果from是 100000協(xié)調(diào)節(jié)點(diǎn)就必須先從每個(gè)分片取回 100000 條文檔哪怕最終只需要 10 條。分片數(shù)量越多這個(gè)中間結(jié)果集就越龐大內(nèi)存開銷呈線性甚至超線性增長(zhǎng)。在深分頁(yè)的情況下整個(gè)集群的資源會(huì)被這種無(wú)意義的計(jì)算白白吃掉??梢源炙阋还P賬假設(shè)索引有 5 個(gè)分片查詢from100000, size10那么協(xié)調(diào)節(jié)點(diǎn)需要接收 5 × 100000 500000 條文檔每條文檔就算只保留_id、_score和排序字段也有幾百字節(jié)這一輪下來(lái)就是幾百 MB 的內(nèi)存暫用。如果業(yè)務(wù) QPS 再高一點(diǎn)協(xié)調(diào)節(jié)點(diǎn)的堆內(nèi)存直接被撐爆不是玩笑。所以 ES 提供了max_result_window這個(gè)保護(hù)機(jī)制。默認(rèn) 10000也就是說(shuō)from size的最大值不允許超過(guò) 10000。它的本質(zhì)是默認(rèn)場(chǎng)景下ES 只支持你在前 10000 條結(jié)果里翻頁(yè)再深就不玩了。1.2 實(shí)際場(chǎng)景中誰(shuí)會(huì)被這個(gè)限制卡住很多人以為“搜索”最多就看前幾頁(yè)10000 條完全夠用。但在真實(shí)業(yè)務(wù)中撞墻的場(chǎng)景遠(yuǎn)比想象中多后臺(tái)管理系統(tǒng)的數(shù)據(jù)導(dǎo)出運(yùn)營(yíng)要導(dǎo)出一份半年內(nèi)的訂單明細(xì)總量可能 20 萬(wàn)條但導(dǎo)出接口基本都是一個(gè)scroll或者search_after就能解決的事。定時(shí)任務(wù)的全量同步把 ES 里的數(shù)據(jù)同步到數(shù)倉(cāng)、或者重灌另一個(gè)索引動(dòng)不動(dòng)幾十萬(wàn)上百萬(wàn)條。業(yè)務(wù)上的“下一頁(yè)”遍歷某些檢索產(chǎn)品的翻頁(yè)深度確實(shí)能超過(guò)幾百頁(yè)。聚合分析的前置查詢先查出一批文檔 ID 再做后續(xù)處理為了避免 deep pagination很多團(tuán)隊(duì)直接把size設(shè)成 Integer.MAX_VALUE結(jié)果就是直接報(bào)錯(cuò)。不管是哪種情況解決思路本質(zhì)上只有兩類第一類是把窗口調(diào)大讓 ES 允許你翻得更深第二類是換一種遍歷方式壓根不走深分頁(yè)。下面挨個(gè)說(shuō)。2. 方案橫向?qū)Ρ让總€(gè)思路的優(yōu)缺點(diǎn)和適用場(chǎng)景2.1 直接調(diào)大 index.max_result_window最直觀的辦法。一條命令就能搞定PUT /your_index/_settings { index.max_result_window: 1000000 }也可以用通配符批量設(shè)置PUT /my_logs_*/_settings { index.max_result_window: 1000000 }這個(gè)方案的優(yōu)點(diǎn)是簡(jiǎn)單、立即生效、不需要改代碼適合某些緊急情況下的快速止血。但缺點(diǎn)也非常明顯它只是把“墻”挪遠(yuǎn)了并沒(méi)有消除深度分頁(yè)的內(nèi)存開銷。如果你把窗口調(diào)到 100 萬(wàn)一旦有查詢真的翻到 50 萬(wàn)頁(yè)協(xié)調(diào)節(jié)點(diǎn)的內(nèi)存照樣會(huì)被瞬間打滿整個(gè)集群可能因此進(jìn)入 GC 長(zhǎng)暫停甚至宕機(jī)。我的建議是這個(gè)方案只適合“臨時(shí)應(yīng)急”或者“明確知道數(shù)據(jù)量很小”的場(chǎng)景。比如一個(gè)索引上限就是兩萬(wàn)條那調(diào)到 50000 完全沒(méi)毛病。如果是用來(lái)解決全量導(dǎo)出的問(wèn)題千萬(wàn)不要靠它硬撐——你這是在拿集群的穩(wěn)定性做賭注。2.2 使用 Scroll 游標(biāo)Scroll 是 ES 專門為“遍歷全部數(shù)據(jù)”設(shè)計(jì)的 API核心思路是第一次查詢時(shí)ES 為這個(gè)查詢生成一個(gè)快照游標(biāo)之后通過(guò)游標(biāo) ID 分批拉取數(shù)據(jù)不再重復(fù)排序計(jì)算。# 第一次請(qǐng)求返回 _scroll_id POST /your_index/_search?scroll1m { size: 1000, query: { match_all: {} } } # 后續(xù)每次通過(guò) scroll id 拉下一批 POST /_search/scroll { scroll: 1m, scroll_id: DXF1ZXJ5QW5kRmV0Y2gB... }Scroll 的優(yōu)點(diǎn)很明顯它是官方設(shè)計(jì)的遍歷方案性能穩(wěn)定不會(huì)因?yàn)閿?shù)據(jù)總量大而內(nèi)存失控。但它也有幾個(gè)讓人頭疼的副作用生成快照是有成本的如果索引在 scroll 期間發(fā)生寫入、刪除、更新scroll 查詢到的是快照建立那一刻的數(shù)據(jù)不是“實(shí)時(shí)”的。游標(biāo)會(huì)話需要占用服務(wù)端資源超過(guò)scroll參數(shù)指定的時(shí)間后游標(biāo)會(huì)被自動(dòng)清理。用完還必須顯式刪除游標(biāo)否則資源泄漏。因?yàn)椴恢С痔?yè)你只能從頭拉到尾所以它根本不適合用在“用戶翻頁(yè)瀏覽”的交互場(chǎng)景里。結(jié)論是Scroll 最適合“一次性全量導(dǎo)出”“數(shù)據(jù)同步”“重索引”這類離線任務(wù)。當(dāng)年我做某系統(tǒng)數(shù)據(jù)遷移用 scroll 配合多線程并發(fā)消費(fèi)一次性搬了接近 200 萬(wàn)條數(shù)據(jù)整個(gè)過(guò)程非常穩(wěn)。但如果你是要做“支持跳到任意頁(yè)”的后臺(tái)列表scroll 幫不了你。2.3 使用 Search After 方案Search after 是目前社區(qū)公認(rèn)的“分頁(yè)理想之選”。它的設(shè)計(jì)思路是記錄上一頁(yè)最后一條數(shù)據(jù)的排序值下一頁(yè)查詢時(shí)以這個(gè)排序值為起點(diǎn)向后查找。典型的用法如下GET /your_index/_search { size: 10, query: { match_all: {} }, sort: [ { id: asc }, { _id: asc } ] }拿到最后一條結(jié)果后把 sort 數(shù)組里的值填進(jìn)下一次請(qǐng)求GET /your_index/_search { size: 10, query: { match_all: {} }, search_after: [100024, doc_id_value], sort: [ { id: asc }, { _id: asc } ] }Search after 的原理是不通過(guò)from來(lái)定位偏移量而是拿取上一頁(yè)末尾的排序值作為條件直接讓 ES 從對(duì)應(yīng)位置開始掃描。這樣每次查詢只需獲取下一頁(yè)的數(shù)據(jù)協(xié)調(diào)節(jié)點(diǎn)不需要保留前面所有頁(yè)的中間結(jié)果內(nèi)存開銷和查詢深度沒(méi)有直接關(guān)系。它的優(yōu)勢(shì)非常契合交互式分頁(yè)穩(wěn)定、高效、不占額外資源、能看到實(shí)時(shí)數(shù)據(jù)變化。缺點(diǎn)同樣是無(wú)法自由跳頁(yè)——你想從第 1 頁(yè)直接跳到第 100 頁(yè)是做不到的必須一頁(yè)頁(yè)順著走。那什么時(shí)候用 search_after 最合適數(shù)據(jù)量幾十萬(wàn)到上千萬(wàn)的后臺(tái)列表、帶“加載更多”的 C 端信息流、以及所有需要并行遍歷數(shù)據(jù)的場(chǎng)景。我自己現(xiàn)在寫后臺(tái)列表基本直接默認(rèn) search_after不再用 from size。2.4 順帶說(shuō)一句 Track Total Hits很多同學(xué)還遇到過(guò)另一個(gè)現(xiàn)象hits.total顯示為 10000但數(shù)據(jù)其實(shí)遠(yuǎn)不止這些。這跟max_result_window是兩碼事它是track_total_hits參數(shù)控制的。當(dāng)它為默認(rèn)的false時(shí)ES 只精確統(tǒng)計(jì)前 10000 條命中數(shù)超過(guò)就顯示 10000。如果業(yè)務(wù)上需要知道全量命中數(shù)可以在查詢時(shí)設(shè)置GET /your_index/_search { track_total_hits: true, query: { match_all: {} } }注意開啟后會(huì)產(chǎn)生額外的統(tǒng)計(jì)開銷如果索引特別大、命中數(shù)量特別多這個(gè)計(jì)數(shù)過(guò)程本身會(huì)消耗一定性能。建議只在“必須展示精確總數(shù)”的頁(yè)面上開并且配合size限制使用不要和深分頁(yè)同時(shí)開。3. 深入剖析從分頁(yè)原理到 Search After 的完整實(shí)操3.1 為什么排序字段決定了 Search After 的成敗Search after 依賴排序值的穩(wěn)定性。如果排序字段存在重復(fù)值那基于這個(gè)重復(fù)值的“起點(diǎn)定位”就會(huì)出問(wèn)題同一頁(yè)數(shù)據(jù)可能出現(xiàn)重復(fù)或丟失。規(guī)避的方法是保證排序組合“全局唯一”。最穩(wěn)的組合是一個(gè)業(yè)務(wù)主鍵如自增 id 或時(shí)間戳_id兜底。_id是文檔的唯一標(biāo)識(shí)天然不可能重復(fù)。只要業(yè)務(wù)主鍵相同就靠_id來(lái)打破平局保證每一頁(yè)結(jié)果的順序是嚴(yán)格確定的。這里有個(gè)細(xì)節(jié)如果你用_id做排序字段ES 內(nèi)部對(duì)_id的排序走的是keyword字典序不是你以為的“數(shù)字序”。如果你的主鍵是純數(shù)字的字符串排序結(jié)果可能是 “1002”“1003”“999” 這種和你預(yù)期的數(shù)字順序不一致需要留意。解決辦法有兩種要么在 mapping 里新增一個(gè)long類型的字段存自增數(shù)字主鍵要么設(shè)計(jì)排序時(shí)用日期 自增數(shù)字兩個(gè)字段組合。3.2 實(shí)操用 Python 封裝一個(gè)通用 Scroll 導(dǎo)出工具數(shù)據(jù)導(dǎo)出的場(chǎng)景我推薦直接用 scroll 來(lái)寫。下面是一個(gè)封裝好的通用導(dǎo)出函數(shù)實(shí)測(cè)在數(shù)據(jù)量 50 萬(wàn)級(jí)別的索引上跑得很穩(wěn)from elasticsearch import Elasticsearch, helpers import time class ESExporter: def __init__(self, hosts, index, queryNone, batch_size5000): self.es Elasticsearch(hosts) self.index index self.query query or {query: {match_all: {}}} self.batch_size batch_size def export_all(self): total 0 start time.time() resp self.es.search( indexself.index, bodyself.query, scroll5m, sizeself.batch_size, _sourceTrue ) scroll_id resp.get(_scroll_id) try: while resp[hits][hits]: batch_docs resp[hits][hits] total len(batch_docs) # 在這里把 batch_docs 寫進(jìn)文件、數(shù)據(jù)庫(kù)或消息隊(duì)列 self._process_batch(batch_docs) resp self.es.scroll(scroll_idscroll_id, scroll5m) finally: self.es.clear_scroll(scroll_idscroll_id) print(f導(dǎo)出完成共 {total} 條耗時(shí) {time.time() - start:.2f}s) def _process_batch(self, docs): # 示例打印前 3 條數(shù)據(jù)的 id for doc in docs[:3]: print(doc[_id])幾個(gè)容易踩的注意點(diǎn)scroll 參數(shù)的時(shí)長(zhǎng)設(shè)置要合理。官方推薦“能覆蓋兩個(gè)批次處理間隔”即可比如 5 分鐘處理 5000 條那設(shè)置5m就夠。設(shè)太長(zhǎng)會(huì)占著服務(wù)端資源不釋放。每一批處理完一定要記得用clear_scroll清理游標(biāo)。雖然游標(biāo)超時(shí)會(huì)自動(dòng)失效但在高并發(fā)場(chǎng)景下不主動(dòng)清理會(huì)積累大量空閑游標(biāo)導(dǎo)致內(nèi)存碎片增加。如果是要并發(fā)消費(fèi)不要在一個(gè)線程里串行 scroll可以開多個(gè) worker 線程每個(gè)線程獨(dú)立使用一個(gè) scroll_id這樣能顯著提升吞吐。我在搬數(shù)據(jù)時(shí)開了 8 個(gè)線程速度大概提升了 4 倍左右。3.3 實(shí)操用 Java 實(shí)現(xiàn) Search After 的深分頁(yè)列表查詢國(guó)內(nèi)用 Java 寫 ES 查詢的團(tuán)隊(duì)還是最多的我直接給一套完整的 Java 實(shí)現(xiàn)public ListMapString, Object searchAfterQuery( RestHighLevelClient client, String index, long lastId, String lastDocId, int size) throws IOException { SearchRequest request new SearchRequest(index); SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.matchAllQuery()); sourceBuilder.size(size); sourceBuilder.fetchSource(true); // 排序組合用數(shù)字 id 做主排序_id 做唯一性兜底 sourceBuilder.sort(new FieldSortBuilder(data_id).order(SortOrder.ASC)); sourceBuilder.sort(_doc); if (lastId 0) { sourceBuilder.searchAfter(new Object[]{lastId, lastDocId}); } request.source(sourceBuilder); SearchResponse response client.search(request, RequestOptions.DEFAULT); ListMapString, Object list new ArrayList(); for (SearchHit hit : response.getHits().getHits()) { MapString, Object map hit.getSourceAsMap(); map.put(_doc_id, hit.getId()); list.add(map); } return list; }注意我用的是sort(_doc)而不是sort(_id)。_doc是 ES 內(nèi)置的“輸入順序”排序它不額外計(jì)算字段值性能極高而_id排序走字段反轉(zhuǎn)索引需要額外內(nèi)存。兩者選誰(shuí)取決于你的場(chǎng)景如果數(shù)據(jù)幾乎只追加不更新_doc完全夠用如果會(huì)有排序波動(dòng)風(fēng)險(xiǎn)就回到_id兜底方案。調(diào)用示例ListMapString, Object page searchAfterQuery(client, order_index, 0, , 20); String lastDocId (String) page.get(page.size() - 1).get(_doc_id); Long lastDataId (Long) page.get(page.size() - 1).get(data_id); // 下一次查詢 page searchAfterQuery(client, order_index, lastDataId, lastDocId, 20);3.4 核心參數(shù)的計(jì)算與選擇邏輯很多同學(xué)會(huì)問(wèn)size到底設(shè)多少合適scroll 的batch_size呢這個(gè)沒(méi)有什么銀彈但可以給一套參考邏輯search_after 場(chǎng)景size 設(shè) 10~50 都行關(guān)鍵是排序字段穩(wěn)定。如果信息流類產(chǎn)品一次加載 20 條用戶體驗(yàn)最好。scroll 全量導(dǎo)出場(chǎng)景批量大小推薦在 1000~5000 之間。批量太小網(wǎng)絡(luò)往返次數(shù)多吞吐上不去批量太大單批次響應(yīng)體過(guò)大遇到臟數(shù)據(jù)可能導(dǎo)致 JSON 解析異常重試代價(jià)高。我在生產(chǎn)環(huán)境壓測(cè)過(guò)一個(gè)索引批量 5000 條時(shí)吞吐最高、垃圾回收最平緩批量調(diào)到 20000 時(shí)反而觸發(fā)了好幾次老年代 GC整體耗時(shí)反而上升。內(nèi)存預(yù)估一個(gè)粗略但實(shí)用的公式是size × 單條文檔平均大小 × 分片數(shù)。如果單條文檔 2KB、分片 5 個(gè)、size 5000那么一個(gè)批次協(xié)調(diào)節(jié)點(diǎn)大約要暫用 50MB 內(nèi)存。多線程并發(fā)時(shí)要按照線程數(shù)翻倍估算。3.5 生產(chǎn)級(jí)搜索封裝思路說(shuō)實(shí)話把從 fromsize 切換到 search_after 的過(guò)程做成“透明無(wú)感”才是工程上最優(yōu)雅的做法。我在一個(gè)通用查詢服務(wù)里封裝了一個(gè)分頁(yè)工具對(duì)外暴露PageRequest對(duì)象public class PageRequest { private int pageSize; private Object[] searchAfter; private boolean useSearchAfter; // 第一頁(yè)useSearchAfterfalsepageSize20 // 下一頁(yè)useSearchAftertruesearchAfter上一頁(yè)最后一個(gè)排序值 }代碼層面從第一頁(yè)翻到第一百頁(yè)的時(shí)候前端傳回來(lái)的是一個(gè)“游標(biāo)”字符串后端把它反序列化成排序數(shù)組塞進(jìn) search_after 里。這樣對(duì)前端而言和普通分頁(yè)沒(méi)區(qū)別但后端從底層規(guī)避了深分頁(yè)的問(wèn)題。具體做法是把上一頁(yè)的sort數(shù)組用 Base64 編碼后作為下一頁(yè)的請(qǐng)求參數(shù)這樣既不需要用戶記住頁(yè)碼也天然支持無(wú)限深度翻頁(yè)。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 調(diào)大 max_result_window 后查詢?nèi)匀粓?bào)錯(cuò)這種情況大概率是目標(biāo)索引的設(shè)置沒(méi)生效。一個(gè)容易忽略的點(diǎn)是ES 7.x 之后索引名支持通配符但通配符只能匹配完整的索引名不能匹配別名。如果你通過(guò)別名查詢?cè)O(shè)置卻打在物理索引上那查詢依然會(huì)報(bào)錯(cuò)。排查步驟# 查看當(dāng)前索引的實(shí)際設(shè)置 GET /your_index/_settings/index.max_result_window # 確認(rèn)別名指向哪些物理索引 GET /_alias/your_alias還有一種可能是你改了模板但只對(duì)新索引生效。老索引的 setting 是創(chuàng)索引時(shí)寫死的必須單獨(dú)更新。4.2 Scroll 明明設(shè)置了超時(shí)時(shí)間連接卻一直報(bào)錯(cuò)最常見(jiàn)的原因是 scroll 游標(biāo)在服務(wù)端是獨(dú)立于 HTTP 連接的你設(shè)置scroll1m后如果兩個(gè)批次之間處理耗時(shí)超過(guò)了 60 秒比如批處理器里調(diào)用了外部接口很慢游標(biāo)已經(jīng)過(guò)期你再拿著舊的 scroll_id 去請(qǐng)求就報(bào)No search context found。排查思路把 scroll 時(shí)間放寬到一個(gè)有余量的值比如10m或者你可以在批處理邏輯里做多線程并行處理縮短單批耗時(shí)。還有一個(gè)低級(jí)錯(cuò)誤同一個(gè) scroll_id 被兩個(gè)線程并發(fā)消費(fèi)。ES 的 scroll 游標(biāo)不是線程安全的并發(fā)消費(fèi)同一個(gè)游標(biāo)會(huì)導(dǎo)致結(jié)果錯(cuò)亂或者拋異常。正確做法是每個(gè)線程初始化獨(dú)立的 scroll 查詢。4.3 為什么 search_after 有時(shí)會(huì)漏數(shù)據(jù)或者重復(fù)這個(gè)坑我?guī)屯屡挪檫^(guò)最后定位到原因業(yè)務(wù)方用了一個(gè)時(shí)間戳字段做排序但這個(gè)字段在寫入后會(huì)被更新導(dǎo)致排序值發(fā)生“跨頁(yè)”移動(dòng)。第一條記錄本來(lái)排在第 5 頁(yè)末尾下一秒被更新到第 1 頁(yè)前部search_after 基于舊排序值往后查就會(huì)漏掉這種“排序值變小”的記錄。解決方案很明確排序字段選擇寫入后不變的字段比如create_time、自增主鍵。如果業(yè)務(wù)無(wú)法保證不可變就要接受 search_after 的“弱一致性”同時(shí)結(jié)合業(yè)務(wù)規(guī)則去規(guī)避比如只查三天前的數(shù)據(jù)三天內(nèi)不允許變動(dòng)。兜底方案是加一個(gè)_id參與排序至少保證文檔不會(huì)因?yàn)槠骄侄貜?fù)/丟失。4.4 深度分頁(yè)時(shí)內(nèi)存飆升要不要換方案如果你還在用 from size并且翻到了四五萬(wàn)頁(yè)發(fā)現(xiàn)協(xié)調(diào)節(jié)點(diǎn)內(nèi)存異常飆升你第一步要做的不是加內(nèi)存而是立刻把查詢改成 search_after。同時(shí)通過(guò)慢日志查看是不是有高成本查詢PUT /your_index/_settings { index.search.slowlog.threshold.query.warn: 5s, index.search.slowlog.threshold.fetch.warn: 3s }開啟慢日志后觀察一段時(shí)間往往能發(fā)現(xiàn)不只是分頁(yè)方式的問(wèn)題還有可能是 sort 字段沒(méi)建 doc_values、或者查詢里帶了腳本排序——這些都會(huì)放大內(nèi)存開銷。4.5 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象可能原因推薦處理方式翻頁(yè)超過(guò) 10000 直接報(bào)錯(cuò)max_result_window 默認(rèn)限制交互式分頁(yè)用 search_after全量導(dǎo)出用 scrolltotal 一直顯示 10000track_total_hits 未開啟需要精確總數(shù)時(shí)開啟但要評(píng)估性能開銷scroll 中途報(bào) context not found游標(biāo)超時(shí)調(diào)大 scroll 時(shí)間或減少單批處理耗時(shí)search_after 結(jié)果有重復(fù)/遺漏排序字段不唯一或會(huì)變動(dòng)增加“業(yè)務(wù)主鍵 _id”雙字段排序fromsize 深分頁(yè)內(nèi)存暴漲協(xié)調(diào)節(jié)點(diǎn)需要合并全量中間結(jié)果立即切換到 search_after并評(píng)估查詢成本調(diào)大 setting 后仍報(bào)錯(cuò)別名/模板未覆蓋到優(yōu)先檢查索引真實(shí) setting 和別名指向5. 我的最終建議與選型路徑如果是新項(xiàng)目我的默認(rèn)路徑是這樣的普通業(yè)務(wù)列表、后臺(tái)管理系統(tǒng)、運(yùn)營(yíng)平臺(tái) → 直接用search_after不解釋。全量數(shù)據(jù)導(dǎo)出、索引重建、數(shù)倉(cāng)同步 → 直接用scroll并且每批處理完清游標(biāo)。不能改代碼的臨時(shí)救急 → 調(diào)max_result_window同時(shí)評(píng)估風(fēng)險(xiǎn)并盡快補(bǔ)方案。沒(méi)有深翻頁(yè)需求、確認(rèn)總量小 → 繼續(xù)用 fromsize但建議 size 不超過(guò) 100。我個(gè)人在實(shí)際操作中最深的體會(huì)是不要在分頁(yè)技術(shù)上糾結(jié)“哪種寫法最?!倍认朊靼啄愕膱?chǎng)景是“用戶看數(shù)據(jù)”還是“系統(tǒng)搬數(shù)據(jù)”。這兩個(gè)場(chǎng)景對(duì)分頁(yè)的要求完全不同選型錯(cuò)了再優(yōu)化也都別扭。還有一個(gè)小技巧無(wú)論用 scroll 還是 search_after都要在索引上精心設(shè)計(jì)排序字段最好單獨(dú)加一個(gè)data_id作為穩(wěn)定排序字段別省這一步。后面你會(huì)感謝自己當(dāng)時(shí)加了它。