如何優雅的全量讀取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的認知。