ElasticSearch - 解決 ES 的深分頁問題(游標 scroll)

古古

2018/06/26


  • ES 為了避免深分頁,不允許使用分頁(from & size)查詢 10000 條以後的數據,因此如果要查詢第 10000 條以後的數據,要使用 ES 提供的 scroll 游標 來查詢

    • 原因是因為假設取的頁數較大時(深分頁),如請求第 20 頁,ES 不得不取出所有分片上的第 1 頁到第 20 頁的所有文檔,並做排序,最終再取出 from 後的 size 條結果作爲最終的返回值
    • 假設你有 16 個分片,則需要在 coordinate node 彙總到 shards * (from + size) 條記錄,即需要 16 * (20 + 10) 記錄後做一次全局排序
    • 所以,當索引非常非常大(千萬或億),是無法使用 from + size 做深分頁的,分頁越深則越容易 Out Of Memory,即使你運氣很好沒有發生 Out Of Memory,也會非常消耗 CPU 和內存資源
    • 因此 ES 使用 index.max_result_window:10000 作爲保護措施 ,即默認 from + size 不能超過 10000,雖然這個參數可以動態修改,也可以在配置文件配置,但是最好不要這麼做,應該改用 ES 提供的 scroll 方法來取得數據
  • scroll 游標原理

    • 可以把 scroll 理解爲關係型數據庫裏的 cursor,因此,scroll 並不適合用來做實時搜索,而更適用於後台批處理任務,比如群發
    • scroll 具體分爲初始化和遍歷兩步
      • 初始化時將所有符合搜索條件的搜索結果緩存起來,可以想象成快照
      • 在遍歷時,從這個快照裏取數據
      • 也就是說,在初始化後對索引插入、刪除、更新數據都不會影響遍歷結果
    • 游標可以增加性能的原因,是因為如果做深分頁,每次搜索都必須重新排序,非常浪費,使用 scroll 就是一次把要用的數據都排完了,分批取出,因此比使用 from + size 還好
  • 具體實例

    • 初始化

      • 請求

        • 注意要在URL中的search後加上 scroll=1m,不能寫在 request body 中,其中 1m 表示這個游標要保持開啟 1 分鐘

        • 可以指定 size 大小,就是每次回傳幾筆數據,當回傳到沒有數據時,仍會返回 200 成功,只是 hits 裡的 hits 會是空 list

        • 在初始化時除了回傳 _scroll_id,也會回傳前 100 筆(假設 size = 100)的數據

        • request body 和一般搜索一樣,因此可以說在初始化的過程中,除了加上 scroll 設置游標開啟時間之外,其他的都跟一般的搜尋沒有兩樣(要設置查詢條件,也會回傳前 size 筆的數據)

          GET my_index/_search?scroll=1m
          {
              "query":{
                  "range":{
                      "createTime": {
                          "gte": 1522229999999
                      }
                  }
              },
              "size": 1000
          }
          
      • 返回結果

        {
            "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAfv5-FjNOamF0Mk1aUUhpUnU5ZWNMaHJocWcAAAAAAH7-gBYzTmphdDJNWlFIaVJ1OWVjTGhyaHFnAAAAAAB-_n8WM05qYXQyTVpRSGlSdTllY0xocmhxZwAAAAAAdsJxFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbCcBZlZGUwSWpSVlJqeVJiN1dBWHNpUG1R",
            "took": 2,
            "timed_out": false,
            "_shards": {
                "total": 5,
                "successful": 5,
                "skipped": 0,
                "failed": 0
            },
            "hits": {
                "total": 84,
                "max_score": 1,
                "hits": [
                    {
                        "_index": "video1522821719",
                        "_type": "doc",
                        "_id": "84056",
                        "_score": 1,
                        "_source": {
                            "title": "三个院子",
                            "createTime": 1522239744000
                        }
                    }
                    ....99 data
                ]
            }
        }
        
    • 遍歷數據

      • 請求

        • 使用初始化返回的 _scroll_id 來進行請求,每一次請求都會繼續返回初始化中未讀完數據,並且會返回一個 _scroll_id,這個 _scroll_id 可能會改變,因此每一次請求應該帶上上一次請求返回的 _scroll_id

        • 要注意返回的是 _scroll_id,但是放在請求裡的是 scroll_id,兩者拼寫上有不同

        • 且每次發送 scroll 請求時,都要再重新刷新這個 scroll 的開啟時間,以防不小心超時導致數據取得不完整

          GET _search/scroll?scroll=1m
          {
              "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAdsMqFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbDKRZlZGUwSWpSVlJqeVJiN1dBWHNpUG1RAAAAAABpX2sWclBEekhiRVpSRktHWXFudnVaQ3dIQQAAAAAAaV9qFnJQRHpIYkVaUkZLR1lxbnZ1WkN3SEEAAAAAAGlfaRZyUER6SGJFWlJGS0dZcW52dVpDd0hB"
          }
          
      • 返回結果

        • 如果沒有數據了,就會回傳空的 hits,可以用這個判斷是否遍歷完成了數據

          {
              "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAdsMqFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbDKRZlZGUwSWpSVlJqeVJiN1dBWHNpUG1RAAAAAABpX2sWclBEekhiRVpSRktHWXFudnVaQ3dIQQAAAAAAaV9qFnJQRHpIYkVaUkZLR1lxbnZ1WkN3SEEAAAAAAGlfaRZyUER6SGJFWlJGS0dZcW52dVpDd0hB",
              "took": 2,
              "timed_out": false,
              "_shards": {
                  "total": 5,
                  "successful": 5,
                  "skipped": 0,
                  "failed": 0
              },
              "hits": {
                  "total": 84,
                  "max_score": null,
                  "hits": []
              }
          }
          
  • 優化scroll查詢

    • 在一般場景下,scroll 通常用來取得需要排序過後的大筆數據,但是有時候數據之間的排序性對我們而言是沒有關係的,只要所有數據都能取出來就好,這時能夠對 scroll 進行優化

    • 初始化

      • 使用 _doc 去 sort 得出來的結果,這個執行的效率最快,但是數據就不會有排序,適合用在只想取得所有數據的場景

        GET my_index/_search?scroll=1m
        {
            "query": {
                "match_all" : {}
            },
            "sort": ["_doc"]
        }
        
  • 清除 scroll

    • 雖然我們在設置開啟 scroll 時,設置了一個 scroll 的存活時間,但是如果能夠在使用完順手關閉,可以提早釋放資源,降低 ES 的負擔

      DELETE _search/scroll
      {
          "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAdsMqFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbDKRZlZGUwSWpSVlJqeVJiN1dBWHNpUG1RAAAAAABpX2sWclBEekhiRVpSRktHWXFudnVaQ3dIQQAAAAAAaV9qFnJQRHpIYkVaUkZLR1lxbnZ1WkN3SEEAAAAAAGlfaRZyUER6SGJFWlJGS0dZcW52dVpDd0hB"
      }