Elasticsearch索引數據大批量刪除接口優化

一、需求

每隔一段時間,刪除N天前的數據,索引只保留最近幾天的數據(索引不是按照日期生成的,不能直接刪除整個索引)。【elasticsearch-version-5.x】

二、索引數據刪除接口

使用接口_delete_by_query,定期向集羣提交批量刪除任務,http請求不用等待刪除任務完成才返回,而是在提交任務之後即時返回任務ID。使用_tasks接口定期檢查刪除任務的運行狀態。這種方式解決了在刪除大批量數據的時候Read timed out問題(_delete_by_query接口設置批量提交對於這個問題無解)。

在實際工程使用中,我們需要把elasticsearch的http接口全部封裝爲JavaWeb工程開發者易於使用和理解的依賴工程的形式。因此在下面的實現中保留此種方式,沒有完全按照腳本的形式實現,而是通過jar+shell的形式實現這個功能,並且在封裝的es接口包裏面保留了這個刪除接口。

2.1使用到的elasticsearch核心接口

# _delete_by_query接口
http://localhost:9210/indexName/indexType/_delete_by_query?refresh=true&scroll_size=1000&conflicts=proceed&wait_for_completion=false
# _tasks接口
http://localhost:9210/_tasks/EXlbuEGgRZK-IYKoOHmqWQ:990296121

2.2封裝刪除腳本

#!/usr/bin/env bash

myJarPath=./lib/xxx.jar

# ---------------------------啓動索引數據刪除進程---------------------------

# 索引類型
indexType="indexType"

# 索引名稱-多個索引名稱使用逗號分隔
indexName="indexName"

# IP和端口-使用冒號分隔
ipPort="localhost:9200"

# 索引mapping中的時間字段
timeField="pubtime"

# 每隔delayTime執行一次刪除數據操作 - 延時執行-支持按天/小時/分鐘(格式數字加d/h/m:1d/24h/60m/60s)
delayTime="2s"

# 刪除beforeDataTime以前的數據 - 行一次時刪除多久以前的數據-支持按天/小時/分鐘(格式數字加d/h/m:1d/24h/60m/60s)
beforeDataTime="2d"

# 是否啓動DEBUG模式
debug="true"

#*****************************************************************
# 是否啓用force merge(釋放磁盤空間 - cpu/io消耗增加,緩存失效)
# 1、對於不再生成新分段的索引,建議打開此配置;2、如果索引在不斷的產生新分段建議關閉此配置-通過修改集羣段合併策略優化
#*****************************************************************
isForceMerge="false"

nohup java -Xmx512m -cp ${myJarPath} casia.isi.delete.DeleteIndexData ${indexType} ${indexName} ${ipPort} ${timeField} ${delayTime} ${beforeDataTime} ${debug} ${isForceMerge} >>logs/delete.DeleteIndexData.log 2>&1 &

2.3封裝接口實現

package casia.isi.elasticsearch.operation.delete.shell;
/**
 *         ┏┓       ┏┓+ +
 *        ┏┛┻━━━━━━━┛┻┓ + +
 *        ┃       ┃
 *        ┃   ━   ┃ ++ + + +
 *        █████━█████  ┃+
 *        ┃       ┃ +
 *        ┃   ┻   ┃
 *        ┃       ┃ + +
 *        ┗━━┓    ┏━┛
 * ┃    ┃
 *          ┃    ┃ + + + +
 *          ┃   ┃ Code is far away from     bug with the animal protecting
 *          ┃   ┃ +
 *          ┃   ┃
 *          ┃   ┃  +
 *          ┃    ┗━━━┓ + +
 *          ┃      ┣┓
 *          ┃      ┏┛
 *          ┗┓┓┏━━━┳┓┏┛ + + + +
 *           ┃┫┫  ┃┫┫
 *           ┗┻┛  ┗┻┛+ + + +
 */

import casia.isi.elasticsearch.common.FieldOccurs;
import casia.isi.elasticsearch.common.RangeOccurs;
import casia.isi.elasticsearch.operation.delete.EsIndexDelete;
import casia.isi.elasticsearch.util.DateUtil;
import casia.isi.elasticsearch.util.StringUtil;
import com.alibaba.fastjson.JSONObject;

/**
 * @Description: TODO(監控刪除索引數據)
 * @date 2019/5/30 15:27
 */
public final class DeleteDataByShell {

    private static EsIndexDelete esIndexDataDelete;

    private static String indexType;
    private static String indexName;
    private static String ipPort;

    private static String timeField;
    private static String delayTime;
    private static String beforeDataTime;
    private static boolean isForceMerge = false;

    // DELETE WORK TASK ID
    private static String lastTaskId;

    public static boolean debug = false;

    /**
     * @param indexType:索引類型
     * @param indexName:索引名稱-多個索引名稱使用逗號分隔
     * @param ipPort:IP和端口-使用冒號分隔
     * @param timeField:索引mapping中的時間字段
     * @param delayTime:延時執行-支持按天/小時/分鐘(格式數字加d/h/m:1d/24h/60m/60s)
     * @param beforeDataTime:執行一次時刪除多久以前的數據-支持按天/小時/分鐘(格式數字加d/h/m:1d/24h/60m/60s)
     * @param isForceMerge:true啓用force-merge
     * @return
     * @Description: TODO(爲監控程序創建一個索引數據刪除對象)
     */
    public DeleteDataByShell(String indexType, String indexName, String ipPort, String timeField,
                             String delayTime, String beforeDataTime, boolean isForceMerge) {
        this.esIndexDataDelete = new EsIndexDelete(ipPort, indexName, indexType);

        this.indexType = indexType;
        this.indexName = indexName;
        this.ipPort = ipPort;

        this.timeField = timeField;
        this.delayTime = delayTime;
        this.beforeDataTime = beforeDataTime;

        this.isForceMerge = isForceMerge;
    }

    /**
     * @return
     * @Description: TODO(啓動監控刪除)
     */
    public void run() {

        boolean isExcute = check();
        while (isExcute) {
            try {

                // 執行刪除
                executeDelete();

                // 延時執行
                sleep();

            } catch (Exception e) {
                System.out.println("Delete data exception,please check your parameters!");
                System.out.println("indexType:" + indexType);
                System.out.println("indexName:" + indexName);
                System.out.println("ipPort:" + ipPort);
                System.out.println("timeField:" + timeField);
                System.out.println("delayTime:" + delayTime);
                System.out.println("beforeDataTime:" + beforeDataTime);
                esIndexDataDelete.reset();
            }
        }
    }

    private boolean check() {
        if (this.timeField != null && this.delayTime != null && this.beforeDataTime != null) {
            return true;
        }
        return false;
    }

    private void sleep() throws InterruptedException {
        Thread.sleep(dhmToMill(delayTime));
    }

    private void outputResult() {
        System.out.println("Delay time:" + delayTime);
        System.out.println("Delete data from " + beforeDataTime + " ago.Current system time:" + DateUtil.millToTimeStr(System.currentTimeMillis()));
        if (debug) {
            System.out.println("Query url:" + esIndexDataDelete.getQueryUrl());
            System.out.println("Query json:" + esIndexDataDelete.getQueryString());
            System.out.println("Query result json:" + esIndexDataDelete.getQueryReslut());
        }
        lastTaskId = setTaskId(esIndexDataDelete.getQueryReslut());
    }

    /**
     * @param { "task": "EXlbuEGgRZK-IYKoOHmqWQ:xxxxxxx"
     *          }
     * @return
     * @Description: TODO(設置taskID)
     */
    private String setTaskId(String queryReslut) {
        JSONObject object = JSONObject.parseObject(queryReslut);
        return object.getString("task");
    }

    private void executeDelete() {

        // 輸出上一個task的信息
        System.out.println("===========================================EXECUTE DELETE TASK===========================================");
        if (lastTaskId != null && !"".equals(lastTaskId)) {
            System.out.println(esIndexDataDelete.outputLastTaskInfo(lastTaskId));
        }

        String currentThreadTime = getCurrentThreadTime();

        esIndexDataDelete.addRangeTerms(timeField, currentThreadTime, FieldOccurs.MUST, RangeOccurs.LTE);
        esIndexDataDelete.setRefresh(true);
        esIndexDataDelete.setScrollSize(1000);
        esIndexDataDelete.conflictsProceed("proceed");
        esIndexDataDelete.setWaitForCompletion(false);
        esIndexDataDelete.execute();

        // 輸出刪除統計結果
        outputResult();

        // 釋放磁盤空間(執行段合併操作)- CPU/IO消耗增加,緩存失效
        if (isForceMerge) {
            System.out.println(esIndexDataDelete.forceMerge());
        }

        esIndexDataDelete.reset();
    }

    private String getCurrentThreadTime() {
        long mill = System.currentTimeMillis() - dhmToMill(beforeDataTime);
        return DateUtil.millToTimeStr(mill);
    }

    private long dhmToMill(String dhmStr) {
        if (dhmStr != null && !"".equals(dhmStr)) {
            int number = Integer.valueOf(StringUtil.cutNumber(dhmStr));
            if (dhmStr.contains("d")) {
                return number * 86400000;
            } else if (dhmStr.contains("h")) {
                return number * 3600000;
            } else if (dhmStr.contains("m")) {
                return number * 60000;
            } else if (dhmStr.contains("s")) {
                return number * 1000;
            }
        }
        return 0;
    }

    /**
     * @param
     * @return
     * @Description: TODO(Delete thread main entrance)
     */
    public static void main(String[] args) {
        String indexType = args[0];
        String indexName = args[1];
        String ipPort = args[2];
        String timeField = args[3];
        String delayTime = args[4];
        String beforeDataTime = args[5];
        DeleteDataByShell.debug = Boolean.valueOf(args[6]);
        String isForceMerge = args[7];
        new DeleteDataByShell(indexType, indexName, ipPort, timeField, delayTime, beforeDataTime, Boolean.valueOf(isForceMerge)).run();
    }

}

三、Lucene分段處理的優化

經過以上操作索引中的數據可以被正確的標記爲刪除,並且及時刷新查詢顯示。但是標記刷新之後,索引分段數據並沒有將磁盤空間及時釋放,還依賴於lucene分段合併的處理。

使用forcemerge可以及時釋放磁盤空間,但是會帶來cpu/io消耗增加,緩存失效等問題。這種問題對查詢性能帶來影響。但是可以按照具體的使用場景來採取措施:1、對於不再生成新分段的索引(不再有數據被索引和更新),可以考慮人工啓動分段merge操作;2、如果索引在不斷的產生新分段(數據被索引),通過修改集羣段合併策略優化。在我們的需求中則必須採用第二種方式,線上系統人工_forcemerge帶來的性能問題是不可接受的。

3.1、refersh

es默認每秒進行自動刷新,這帶來的好處是新索引的數據可以及時對搜索可見。隨之帶來的問題是影響性能:某些緩存將會失效,拖慢搜索請求,而且重新打開索引的過程本身也需要一些處理能力,拖慢了索引的建立。

// 索引級setting
"index.refresh_interval": "5s",

3.2、flush

flush操作是將內存數據沖刷到磁盤。內存緩衝區已滿、事務日誌已滿、時間間隔已到,都會觸發flush操作。具體策略請查閱相關文檔。

// 集羣配置elasticsearch.yml-內存緩衝區大小在elasticsearch.yml配置文件定義-可設置爲JVM堆內存的百分比10%
"indices.memory.index_buffer_size":"3gb"

// 索引級setting-觸動沖刷得規模-可設置爲JVM堆內存得百分比10%(默認512mb)
"index.translog.flush_threshold_size": "3gb"

// 索引級setting-沖刷之間的時間間隔(默認是30m)
"index.translog.flush_threshold_period": "30m"

3.3、合併策略

使用lucene默認的分層合併策略。關於分層合併策略的介紹請移步es官網。

// 索引級setting-每層分段數(segments_per_tier設爲與max_merge_at_once相等可減少合併次數)
"index.merge.policy.segments_per_tier":5

// 索引級setting-每層合併的最大分段數(默認是10)
"index.merge.policy.max_merge_at_once": 5

// 索引級setting-最大分段規模(默認是5g)
"index.merge.policy.max_merged_segment": "1gb"

// 索引級setting-用於合併的最大線程數(設置爲1可以讓磁盤更好的運轉)
// 要注意的是如果你是用HDD而非SSD的磁盤的話,最好是用單線程爲妙。
"index.merge.scheduler.max_thread_count": 1

3.4、存儲限流

存儲限流和存儲的優化可以有效提升I/O的吞吐量。
存儲限流的原因:過度的合併會拖慢集羣。由於I/O的等待,會導致CPU負載也會很高。

// 集羣配置elasticsearch.yml存儲限流設置默認20mb(SSD-增加到100~200MB)
"indices.store.throttle.max_bytes_per_sec":"20mb"

// 集羣配置elasticsearch.yml使存儲限流的設置應用到所有的es操作
"indices.store.throttle.type":"all"

3.5、存儲

存儲使用默認存儲,主要考慮調整存儲限流的設置。
存儲類型:1、mmapfs-通常用於大型文件。eg.詞條字典;2、niofs-其它類型文件。eg.存儲字段。詳細優化手段請移步es官方參考文檔。

3.6、使用postman設置索引級配置

// URL
PUT http://localhost:9210/indexName/_settings

// PARAMETERS
{
    "index.refresh_interval": "5s",
    "index.translog": {
        "flush_threshold_size": "3gb",
    },
    "index.merge": {
        "policy": {
            "segments_per_tier": 5,
            "max_merge_at_once": 5,
            "max_merged_segment": "1gb"
        },
        "scheduler.max_thread_count": 1
    }
}

// RESPONSBODY
{
    "acknowledged": true
}

// 使用GET接口查看setting
GET http://localhost:9210/indexName/_settings
{
  "indexName": {
    "settings": {
      "index": {
        "refresh_interval": "5s",
        "number_of_shards": "5",
        "translog": {
          "flush_threshold_size": "3gb"
        },
        "provided_name": "indexName",
        "merge": {
          "scheduler": {
            "max_thread_count": "1"
          },
          "policy": {
            "segments_per_tier": "5",
            "max_merge_at_once": "5",
            "max_merged_segment": "1gb"
          }
        },
        "creation_date": "1559195227068",
        "number_of_replicas": "0",
        "uuid": "aDekoukTQL2HeB_aQy_HFA",
        "version": {
          "created": "5060399"
        }
      }
    }
  }
}

postman設置index的setting:
在這裏插入圖片描述
Lucene分段處理優化之後,很明顯可以看到Heap Memory消耗下降了將近一般左右(之前的圖有一個駝峯式的下降效果忘記截圖了:)gg):
在這裏插入圖片描述

四、刪除接口運行效率統計分析

使用_tasks接口,計算平均處理速率。

http://localhost:9210/_tasks/EXlbuEGgRZK-IYKoOHmqWQ:98453352X
{
    "completed": true,
    "task": {
        "node": "EXlbuEGgRZK-IYKoOHmqWQ",
        "id": 984533525,
        "type": "transport",
        "action": "indices:data/write/delete/byquery",
        "status": {
            "total": 10399385,
            "updated": 0,
            "created": 0,
            "deleted": 4784168,
            "batches": 10400,
            "version_conflicts": 5615217,
            "noops": 0,
            "retries": {
                "bulk": 0,
                "search": 0
            },
            "throttled_millis": 0,
            "requests_per_second": -1,
            "throttled_until_millis": 0
        },
        "description": "delete-by-query [indexName]",
        "start_time_in_millis": 1559727929590,
        "running_time_in_nanos": 3237112234217,
        "cancellable": true
    },
    "response": {
        "took": 3237112,
        "timed_out": false,
        "total": 10399385,
        "updated": 0,
        "created": 0,
        "deleted": 4784168,
        "batches": 10400,
        "version_conflicts": 5615217,
        "noops": 0,
        "retries": {
            "bulk": 0,
            "search": 0
        },
        "throttled_millis": 0,
        "requests_per_second": -1,
        "throttled_until_millis": 0,
        "failures": []
    }
}

類似上述結果,可以根據task的運行情況計算處理效率。使用running_time_in_nanos和deleted字段的數據計算平均處理速率。服務器配置:1、Intel® Xeon® CPU E5-2620 v4 @ 2.10GHz-32核,2、磁盤-HDD1.6T,3、內存-128G。

數據量/總耗時 速率
100萬/792s/13分鐘 1262t/s
219萬/1768s/29分鐘 1238t/s
480萬/3237s/53分鐘 1482t/s

在如上的task統計結果中,可以看到有很多數據是標記爲version_conflicts。在輪詢的刪除過程中需要被刪除的數據最終都會被刪除(每30分鐘運行一次刪除進程)。如果對於數據刪除時效性要求比較高的話,需要解決這個問題。並且繼續優化刪除策略。

// 沒有數據版本衝突的刪除任務,返回的信息是這樣的(version_conflicts=0)
{
    "completed": true,
    "task": {
        "node": "EXlbuEGgRZK-IYKoOHmqWQ",
        "id": 990296121,
        "type": "transport",
        "action": "indices:data/write/delete/byquery",
        "status": {
            "total": 170733,
            "updated": 0,
            "created": 0,
            "deleted": 170733,
            "batches": 171,
            "version_conflicts": 0,
            "noops": 0,
            "retries": {
                "bulk": 0,
                "search": 0
            },
            "throttled_millis": 0,
            "requests_per_second": -1,
            "throttled_until_millis": 0
        },
        "description": "delete-by-query [news_small, blog_small, forum_threads_small, mblog_info_small, video_brief_small, wechat_message_xigua_small, appdata_small, newspaper_info_small][monitor_caiji_small]",
        "start_time_in_millis": 1559731529771,
        "running_time_in_nanos": 71981947551,
        "cancellable": true
    },
    "response": {
        "took": 71981,
        "timed_out": false,
        "total": 170733,
        "updated": 0,
        "created": 0,
        "deleted": 170733,
        "batches": 171,
        "version_conflicts": 0,
        "noops": 0,
        "retries": {
            "bulk": 0,
            "search": 0
        },
        "throttled_millis": 0,
        "requests_per_second": -1,
        "throttled_until_millis": 0,
        "failures": []
    }
}

五、繼續優化

在調用_delete_by_query接口時,設置參數refresh=wait_for。

refresh參數-true表示:立即刷新主分片和副分片;false:表示不刷新,不設置此條件默認不刷新;wait_for:使用集羣自動刷新機制(默認1s,在索引級自定義5s或者其它值,根據業務決定。本次測試使用的5s)。
經過_tasks接口統計,發現優化這個參數之後,每秒的處理能力提升了3~4倍,1262t/s->4115t/s。

數據量/總耗時 速率
100萬/243s/4分鐘 4115t/s
122萬/297s/5分鐘 4107t/s
{
    "completed": true,
    "task": {
        "node": "EXlbuEGgRZK-IYKoOHmqWQ",
        "id": 1111458358,
        "type": "transport",
        "action": "indices:data/write/delete/byquery",
        "status": {
            "total": 1215333,
            "updated": 0,
            "created": 0,
            "deleted": 1215333,
            "batches": 1216,
            "version_conflicts": 0,
            "noops": 0,
            "retries": {
                "bulk": 0,
                "search": 0
            },
            "throttled_millis": 0,
            "requests_per_second": -1,
            "throttled_until_millis": 0
        },
        "description": "delete-by-query [indexName]",
        "start_time_in_millis": 1559802968421,
        "running_time_in_nanos": 297299330904,
        "cancellable": true
    },
    "response": {
        "took": 297299,
        "timed_out": false,
        "total": 1215333,
        "updated": 0,
        "created": 0,
        "deleted": 1215333,
        "batches": 1216,
        "version_conflicts": 0,
        "noops": 0,
        "retries": {
            "bulk": 0,
            "search": 0
        },
        "throttled_millis": 0,
        "requests_per_second": -1,
        "throttled_until_millis": 0,
        "failures": []
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章