1. 程式人生 > >如何優雅的全量讀取Elasticsearch索引裡面的資料

如何優雅的全量讀取Elasticsearch索引裡面的資料

(一)scroll的介紹

有時候我們可能想要讀取整個es索引的資料或者其中的大部分資料,來重建索引或者加工資料,相信大多數人都會說這很簡單啊直接用from+size就能搞定,但實際情況是from+size的分頁方法不適合用於這種全量資料的抽取,越到後面這種方法的效能就越低,這也是es裡面為什麼限制了單次查詢結果的資料不能超過1萬條資料的原因。

es裡面提供了scroll的方式來全量讀取索引資料其與資料庫裡面的遊標(cursor)的概念非常類似,使用scroll讀取資料的時候,只需要傳送一次查詢請求,然後es服務端會生成一個當前請求索引的快照資料集,接著我們每次通過scrollId來讀取指定大小的批次資料,直到把整個索引的資料讀取完畢。

這裡面需要注意,當索引快照集生成的時候,其實在es內部維護了一個search context的上下文,這個上下文在指定的時間間隔內是隻讀的和不可變的,也就是隻要它生成,那麼後續你的新增,刪除,更新操作的資料都不會被感知。

(二)scroll的使用

下面看下如何使用:

(1)要使用scroll方式來讀取資料,需要兩步操作,第一步先做一個search context的初始化操作,如下命令:

curl -XGET 'localhost:9200/twitter/tweet/_search?scroll=1m' -d '
{
    "query": {
        "match" : {
            "
title" : "elasticsearch" } } } '

注意上面url裡面的scroll=1m代表,這個search context只保留一分鐘的有效期。

(2)在第一步操作裡面我們能夠獲取一個scrollId,然後後面的每個讀取都會得到一個scrollId,我們在讀取next批次的資料要把這個scrollId回傳,如下:

curl -XGET  'localhost:9200/_search/scroll'  -d'
{
    "scroll" : "1m", 
    "scroll_id" : "c2Nhbjs2OzM0NDg1ODpzRlBLc0FXNlNyNm5JWUc1"
} '

或者通過search lite api的方式:

curl -XGET 'localhost:9200/_search/scroll?scroll=1m' -d 'c2Nhbjs2OzM0NDg1ODpzRlBLc0FXNlNyNm5JWUc1'

這樣依次迴圈讀取直到searchHits陣列為空的情況下就代表資料讀取完畢。

同理聚合的scroll請求,也是如此,但聚合請求的資料體只會在初始化的search裡面存在,這一點需要注意,不過聚合請求的scroll一般沒有這種應用場景,畢竟聚合後的結果一般都是少了好幾個數量級的。

此外scroll請求還可以新增一個或多個排序欄位,如果你讀取的索引資料完全忽略它的順序,那麼我們還可以使用doc欄位排序來提升效能。

curl -XGET 'localhost:9200/_search?scroll=1m' -d '
{
  "sort": [
    "_doc"
  ]
}
'

ok,再補充下再java api裡面如何全量讀取es索引資料的方法:

`           //指定一個index和type    
            SearchRequestBuilder search = client.prepareSearch("active2018").setTypes("active");
            //使用原生排序優化效能
            search.addSort("_doc", SortOrder.ASC);
            //設定每批讀取的資料量
            search.setSize(100);
            //預設是查詢所有
            search.setQuery(QueryBuilders.queryStringQuery("*:*"));
            //設定 search context 維護1分鐘的有效期
            search.setScroll(TimeValue.timeValueMinutes(1));

            //獲得首次的查詢結果
            SearchResponse scrollResp=search.get();
            //列印命中數量
            System.out.println("命中總數量:"+scrollResp.getHits().getTotalHits());
            //列印計數
            int count=1;
            do {
                System.out.println("第"+count+"次列印資料:");
                //讀取結果集資料
                for (SearchHit hit : scrollResp.getHits().getHits()) {
                    System.out.println(hit.getSource())  ;
                }
                count++;
                //將scorllId迴圈傳遞
                scrollResp = client.prepareSearchScroll(scrollResp.getScrollId()).setScroll(TimeValue.timeValueMinutes(1)).execute().actionGet();
                
                //當searchHits的陣列為空的時候結束迴圈,至此資料全部讀取完畢
            } while(scrollResp.getHits().getHits().length != 0);

(三)刪除無用的scroll

上文提到scroll請求時會維護一個search context快照集,這是如何做到的? 通過前面的幾篇文章(點底部選單欄可以看到),我們知道es在寫入資料時,會在記憶體中不斷的生成segment,然後有一個merge執行緒,會不斷的合併小segment到更大的segment裡面,然後再刪除舊的segment,來減少es對系統資源的佔用, 尤其是檔案控制代碼,那麼維護一個時間段內的索引快照,則意味著這段時間內的所有segment不能被合併,否則就破壞了快照的靜態性,這樣以來暫時不能被合併的小segment會佔系統大量的檔案控制代碼和系統資源,所以scroll的方式一定是離線使用的而不是提供給近實時使用的。

我們需要養成一個好習慣,當我們用完之後應該手動清除scroll,雖然search context超時也會自動清除。

es中提供了可以檢視當前系統中有多少個open search context的api命令:

curl -XGET localhost:9200/_nodes/stats/indices/search?pretty

下面看下刪除scrollId的方式

(1)刪除一個scrollId

DELETE /_search/scroll
{
    "scroll_id" : "UQlNsdDcwakFMNjU1QQ=="
}

(2)刪除多個scrollId

DELETE /_search/scroll
{
    "scroll_id" : [
      "aNmRMaUhiQlZkMWFB==",
      "qNmRMaUhiQlZkMWFB=="
    ]
}

(3)刪除所有的scrollId

DELETE /_search/scroll/_all

(4)search lite api的刪除多個scrollId用法

DELETE /_search/scroll/aNmRMaUhiQlZkMWFB==,qNmRMaUhiQlZkMWFB==

上面的所有的功能在es2.3.4的版本中已經驗證過,此外在es5.x之後的版本中,還增加了一個分片讀取索引的功能,通過分片支援並行的讀取方式,來提高匯出效率:

一個例子如下:

GET /twitter/_search?scroll=1m
{
    "slice": {
        "id": 0, 
        "max": 2 
    },
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}
GET /twitter/_search?scroll=1m
{
    "slice": {
        "id": 1,
        "max": 2
    },
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

注意上面的slice引數,裡面id欄位代表當前讀取的按個分片的資料,max引數代表我們將整個索引資料切分成分片的個數,預設的分片演算法:

slice(doc) = floorMod(hashCode(doc._uid), max)

從上面能看到是基於uid欄位的hashCode與分片的最大個數求模得出來的,注意floorMod方法與%求模在都是正整數的情況下結果是一樣的。

slice欄位還可以加入自定義的欄位參與分片,比如基於日期欄位:

"slice": {
        "field": "date",
        "id": 0,
        "max": 10
    }

參與分片的欄位必須是數值欄位並需要開啟doc value,另外設定的max數量最好不要超過shard的個數,否則查詢效能會下降,預設es對每個索引限制的最大分片量是1024,不過在setting裡面通過設定index.max_slices_per_scroll引數改變。

(四)總結

本篇文章介紹瞭如何優雅的全量讀取es的索引資料以及它的一些原理和注意事項,瞭解這些有助於我們在日常工作中更好的使用es,從而提升我們對es的認知。