スレッド プールを使用して ES 数千万のデータ インデックスをクエリするときに問題が発生する

https://www.jylt.cc/#/detail?id=f41997ce9c8828d627a68ca7a9fc2de5 icon-default.png?t=M276https://www.jylt.cc/#/detail?id=f41997ce9c8828d627a68ca7a9fc2de5

使用するシーン:

        同社は、ES インデックス A のすべてのデータをクエリし、クエリされたデータの特定のフィールドに基づいて別のインデックス B をクエリし、最終的に必要なデータを統合して取得し、Excel を生成し、OSS をアップロードするなどのリクエストを受けました。数千万のデータがインデックス A とインデックス B の両方に保存されています。以前の同僚は書き込みに単一スレッドを使用し、クエリ インデックス A は制限とディープ ページングを使用しました。最終的なデータ生成には約...どれくらい時間がかかるかわかりません。生成するのに 1 か月もかからないかもしれませんが、その後、需要が私に降りかかりました。

        この要件を作成するまで、私は ES を使用したことがなく、スレッド プールについての知識もほとんどありませんでした。スレッドプールを使えば処理速度が上がるのではないかと思い、いろいろ調べた結果、1,000件の処理で4分だった処理速度を、6,000件の処理で1分に高速化することができました。

(実際に最も時間がかかるステップは、インデックス A 内の数千万のデータをクエリすることです。このステップのコードをここに投稿します)


int i = 0;
//查询出索引A的数量
int count = esService.queryNum("索引A的名称");
while (true) {
//            如果线程的数量没有超 并且查询出的数据量不够 继续执行(这一步也思考了很久,因为不知道怎么控制是否让新的任务进入线程池,如果不加条件,那么任务就会一股脑的往线程池里送,没一会儿就报错了。MAXIMUMPOOLSIZE是最大线程池数量
    if (threadPool.getActiveCount() < MAXIMUMPOOLSIZE && totalCount < count) {
    //线程池里的任务如果想获取到外部的数据,需要用final定义
        final int n = i;
        i ++;
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
            	int limit = 1000;
                long queryStart = System.currentTimeMillis();
                List<String> dataSetEsQueryList = esService.queryData(yuliaoIndex, n * limit, limit);
                long queryEnd = System.currentTimeMillis();
                logger.info("查询一千条语料成功,耗时:" + (queryEnd - queryStart) / 1000 + "s");
            }
        });
    }
}

        queryData方法:

public List<String> queryData(String dataSetEsIndex, int from, int to) {
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices(dataSetEsIndex);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //根据ID进行排序
        sourceBuilder.sort("_id");
        sourceBuilder.from(from);
        sourceBuilder.size(to);
        //之前查询的是索引的全部字段,但是我只需要一个字段,所以这里做了控制
        sourceBuilder.fetchSource(new String[]{"query"}, null);
        searchRequest.source(sourceBuilder);
        List<String> dataSetQueryEsList = new ArrayList<>();
        try {
            SearchResponse rp = client.search(searchRequest, RequestOptions.DEFAULT);
            if (rp != null) {
                SearchHits hits = rp.getHits();
                if (hits != null) {
                    for (SearchHit hit : hits.getHits()) {
                        String source = hit.getSourceAsString();
                        DataSetEsTwo index = GsonUtil.GSON_FORMAT_DATE.fromJson(source,
                                new TypeToken<DataSetEsTwo>() {
                                }.getType());
                        index.setId(hit.getId());
                        dataSetQueryEsList.add(index.getQuery());
                    }
                }
            }
        } catch (IOException e) {
            logger.error("query ES is error " + e.getMessage(),e);
        }
        return dataSetQueryEsList;
    }

        処理結果:

        前述の通り、処理速度を 4 分で 1,000 件から 1 分で 6,000 件まで高速化することに成功しました。ここで問題ですが、今回の場合、インデックス内のデータは7,000件しかありませんが、調べたいインデックスには数千万件のデータがあるので、それらのデータが存在する場合の処理​​時間は同じかどうかを試してみました。インデックスには数千万のデータが含まれています。比例して増えているのだと思いました。夜インターフェースを調整して実行したので、安らかに眠りました。朝見に行く前に、私は思いました喜んで、どれだけのデータが処理されたかを見てみましょう。衝撃的なものを発見しました。

        図のように、クエリは先に進むにつれて時間がかかり、以前はデータが7,000件しかなかったときは1,000件のクエリに4秒程度かかっていましたが、今回はインデックスデータの量が多くなると、消費することは容認できません。一晩で1万件以上のデータを処理しましたが、戸惑いました。最初は ES クエリの問題点がわからず、処理に時間がかかると思っていましたが、後で ES クエリにかなりの時間を費やしていることが分かりましたが、これは問題ではないかと思いました。クエリ、ページ内のクエリ。そこでネットで検索したところ、問題が見つかりました。

(理解しやすいように、次の内容はここからコピーしたものです: es deep paging query_weixin_30872671 のブログ - CSDN ブログ)

        ES に 3 つのノードがあると仮定します。ページング クエリ リクエストが来たとき、それがノード 1 ノードに該当する場合、ノード 1 ノードは同じクエリ リクエストをノード 2 とノード 3 に送信し、各ノードは上位 N 個のドキュメント (ここではドキュメントのドキュメントのみ) を返します。 ID とスコアリングおよび並べ替えフィールドを使用してデータ送信を削減します)、node1 は 3 つのノードのすべてのドキュメント (3*N) を並べ替え、上位 N を取得し、ドキュメントの ID に従って対応するノード上のドキュメント データ全体をクエリします。戻ってきたクライアント。

        from=10000、szie=10000 などのページング クエリの場合、各ノードは実際に from+size=20000 個のデータをクエリする必要があり、ソート後に 10000 個のデータがインターセプトされます。ディープ ページングを実行する場合、たとえばデータの 10 ページ目をクエリする場合、各ノードは 10*size=10W 個のデータをクエリする必要がありますが、これはひどいことです。デフォルトでは、from+size が 10000 より大きい場合、クエリは例外をスローします。ES2.0 以降では、max_result_window 属性設定があり、デフォルト値は 10000 で、from+size の最大制限です。もちろん、一時的な対応戦略としてこの値を変更することはできますが、根本原因ではなく症状を治療すると、製品は悪化するだけです。

        これは、from ページング クエリとリミット ページング クエリを使用する場合、ES インデックス内のデータ量が増えるほど (10,000 以上)、クエリ速度が遅くなるということを意味します。クエリ速度はほぼ 2 倍、さらに 2 倍になります。上で述べたことを参照してください。感じられる。では、どうすればよいのでしょうか? 幸いなことに、ES には、魔法のスクロール クエリという別のクエリ メソッドが用意されています。

        スクロール クエリはカーソル クエリまたはスクロール クエリとも呼ばれます。具体的な概要については、公式ドキュメントを参照してください: Search After | Elasticsearch Guide [6.5] | Elastic

        次に、別の変更を加えました。変更されたコードは次のとおりです。

String queryEnd = "false";
long startTime = System.currentTimeMillis();
//        1. 创建查询对象
SearchRequest searchRequest = new SearchRequest("索引名称");//指定索引
searchRequest.scroll(TimeValue.timeValueMinutes(1L));//指定存在内存的时长为1分钟
//    2. 封装查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.sort("id", SortOrder.DESC); //按照哪个字段进行排序
searchSourceBuilder.size(2);    //一次查询多少条
searchSourceBuilder.fetchSource(new String[]{"query"}, null);   //只查询哪些字段或不查询哪些字段
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchRequest.source(searchSourceBuilder);
//        3.执行查询
// client执行
HttpHost httpHost = new HttpHost("ip", "端口号(int类型)", "http");
RestClientBuilder restClientBuilder = RestClient.builder(httpHost);
//也可以多个结点
//RestClientBuilder restClientBuilder = RestClient.builder(
//    new HttpHost("ip", "端口号(int类型)", "http"),
//        new HttpHost("ip", "端口号(int类型)", "http"),
//        new HttpHost("ip", "端口号(int类型)", "http"));
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(restClientBuilder);

SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = searchResponse.getScrollId();

//        4.获取数据
SearchHit[] hits = searchResponse.getHits().getHits();
totalCount = totalCount + hits.length;
for(SearchHit searchHit : hits){
    String source = searchHit.getSourceAsString();
    DataSetEsTwo index2 = GsonUtil.GSON_FORMAT_DATE.fromJson(source,
            new TypeToken<DataSetEsTwo>() {
            }.getType());
    //index2就是我要的数据
    index2.setId(searchHit.getId());
}
//获取全部的下一页
while (true) {
//                当查不出数据后就不再往下执行 这里做判断是因为走到这里的时候可能有的线程还没执行完
//                  所以需要确保所有的线程都执行结束了,这样数据才是对的
    if ("true".equals(queryEnd)) {
        if (threadPool.getActiveCount() == 0) {
            break;
        }
    }
    SearchHit[] hits1 = null;
    try {
        //创建SearchScrollRequest对象
        SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId);
        searchScrollRequest.scroll(TimeValue.timeValueMinutes(3L));
        SearchResponse scroll = restHighLevelClient.scroll(searchScrollRequest, RequestOptions.DEFAULT);
        hits1 = scroll.getHits().getHits();
    } catch (Exception e) {
        logger.error("第一次查询数据失败:" + e.getMessage());
    }

//                线程池处理获取的结果
    //如果当前线程池的数量是满的 那就等待 直到空出一个线程
    //这个是一样的道理 不可以让任务一股脑的进入线程池
    while (threadPool.getActiveCount() >= MAXIMUMPOOLSIZE) {
        try {
            Thread.sleep(100);
        } catch (Exception e) {
            logger.error("休眠失败...");
        }
    }

    if (hits1 != null && hits1.length > 0) {
        //走到下面的肯定是有线程空位的
        final SearchHit[] hits1Fin = hits1;
        threadPool.execute(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //                            线程池处理查询出的结果
                for (SearchHit searchHit : hits1Fin) {
                    try {
                        String source = searchHit.getSourceAsString();
                        DataSetEsTwo index2 = GsonUtil.GSON_FORMAT_DATE.fromJson(source,
                                new TypeToken<DataSetEsTwo>() {
                                }.getType());
                        //index2就是我要的数据
                        index2.setId(searchHit.getId());
                    } catch (Exception e) {
                        logger.error("线程执行错误:" +e.getMessage());
                    }
                }
            }
        });
    } else {
        logger.info("------------语料查询结束--------------");
        queryEnd = "true";
    }
}
//删除ScrollId
try {
    ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
    clearScrollRequest.addScrollId(scrollId);
    ClearScrollResponse clearScrollResponse = restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
    logger.error("ScrollId删除失败:" + e.getMessage());
}
long endTime = System.currentTimeMillis();
logger.info("数据查询运行时间:" + (endTime - startTime) / 1000 / 60 + "min");

        最適化されたコードは 1 分間に約 3,000 個のデータを処理できますが、インデックスにデータがいくつあっても、処理時間は比例して増加します。

おすすめ

転載: blog.csdn.net/wuchenxiwalter/article/details/123909237