使用线程池查询ES千万级数据索引遇到的问题

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

使用场景:

        公司接到一个需求,需要查询ES索引A中所有数据,并根据查询到的数据中的某个字段再去查询另外一个索引B,整合并获取最终需要的数据,再生成excel、上传oss等等。其中索引A和索引B中都存储了千万条数据,之前的同事是用单线程写的,查询索引A使用的是limit、from深层分页,最终数据生成大概需要...不知道需要多久,可能一个月也生成不出来,后来这个需求就落在了我这里。

        在做这个需求之前我从未使用过ES,对线程池也是一知半解。我想到了使用线程池会提高处理速度,经过了一番研究,终于将处理速度从4分钟处理一千条提升到了一分钟处理6000条,代码如下:

(最耗时的步骤其实就是查询索引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分钟处理一千条提升到了一分钟处理6000条,本以为大功告成,但是!!!问题来了,这种情况是索引里只有7000条数据,因为我要查的索引有千万条数据,我就试了下当索引中有千万条数据的时候,看下处理时长是不是按比例增长的,我以为是按比例增长的,晚上调了接口让他跑着我就安心睡觉去了,早上去看之前还开开心心的想,看看这下处理了多少条数据,然后!我就发现了一件令人头秃的事情。

        如图所示,越往后查询耗时越长,之前只有7000条数据的时候,查询一千条需要4s左右,可是当索引数据量很多的时候,这个耗时...无法接受。一个晚上才处理了一万多条数据,我百思不得其解。最开始没有定位到是ES查询的问题,以为是处理的时候比较耗时,后来终于发现是查询ES浪费了很多时间,但是我心想,查询不就是这么查么,分页查啊。就去网上搜了一下,发现了问题所在。

(便于理解,以下内容是从这里copy过来的:es深度分页查询_weixin_30872671的博客-CSDN博客

        假设我们的ES有三个节点,当分页查询请求过来时,如果落到node1节点,那么node1节点将会向node2和node3发送同样的查询请求,每个节点将topN的文档返回(这里只返回文档的id以及打分排序的字段,减少数据传输),node1会对三个节点的所有文档(3*N个)进行排序,取topN后再根据文档的id到对应的节点上查询整个文档数据,最后返回客户端。

        而对于分页查询,比如from=10000,szie=10000,其实每个节点需要查询from+size=20000条数据,排序之后截取后10000条数据。当我们进行深度分页,比如查询第十页数据时,每个节点需要查询10*size=10W条数据,这个太恐怖了。而且默认情况下,当from+size大于10000时,查询会抛出一个异常,ES2.0后有一个max_result_window属性的设置,默认值是10000,也就是from+size的最大限度。当然你可以修改这个值作为临时的应对策略,不过治标不治本,产品也只会变本加厉!

        意思就是在使用from、limit分页查询的情况下,当ES索引中数据量越大(超过10000的情况下),查询速度越慢,查询速度几乎是成倍成倍成倍增长,看我上面的图可以感受到了。那怎么办呢,还好ES为我们提供了另外一种查询方式,也就是神奇的scroll查询!

        scroll查询也叫游标查询或滚动查询,具体的介绍可以看一下官方文档: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");

        优化后的代码一分钟大概可以处理3000条数据,无论索引里有多少条数据,处理时间都是等比例增长的,完美结束!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值