当前位置: 首页 > 科技观察

面对千万级别的数据查询,CK、ES、RediSearch谁才是王者?

时间:2023-03-20 16:34:06 科技观察

前言在开发中遇到一个业务需求,需要从千万锅数据中筛选出不超过10W的数据,按照配置的权重规则(如商品数据不能连续出现3次)。下面介绍这个业务诉求的实现、设计思路和方案优化,针对“千万级数据中查询10W级数据”设计如下方案:多线程+CK翻页方案ES滚动扫描深度翻页方案ES+Hbase组合方案RediSearch+RedisJSON组合方案一、初步设计方案整体方案设计如下:首先,根据配置的“过滤规则”从pot表中过滤出“目标数据””。根据配置的“排序规则”,对“目标数据”进行排序,得到“结果数据”。技术方案如下:每天运行衍生任务,将已有的千万级pot数据(Hive表)导入Clickhouse,然后使用CK表进行数据筛选。将业务配置的过滤规则和排序规则构建成一个“过滤+排序”的对象SelectionQueryCondition。当从CKpot表中取“目标数据”时,启动多线程,进行页面过滤,将获取到的“目标数据”存入结果列表。//页面大小默认5000intpageSize=this.getPageSize();//页码intpageCnt=totalNum/this.getPageSize()+1;List>result=Lists.newArrayList();List>>>futureList=newArrayList<>(pageCnt);//开启多线程调用for(inti=1;i<=pageCnt;i++){//配置业务筛选规则和顺序规则构造为SelectionQueryCondition对象SelectionQueryConditionselectionQueryCondition=buildSelectionQueryCondition(selectionQueryRuleData);selectionQueryCondition.setPageSize(pageSize);selectionQueryCondition.setPage(i);futureList.add(selectionQueryEventPool.submit(newQuerySelectionDataThread(selectionQueryCondition)));}for(Future>>future:futureList){//RPC调用List>queryRes=future.get(20,TimeUnit.SECONDS);if(CollectionUtils.isNotEmpty(queryRes)){//将目标数据存入resultresult.addAll(queryRes);}}对目标数据结果进行排序,得到最终的“结果数据”。”。这里引入了CK分页查询。封装了queryPoolSkuList方法,负责从CK表中获取目标数据。该方法内部调用了sqlSession.selectList方法。publicList>queryPoolSkuList(Mapparams){List>resultMaps=newArrayList<>();QueryConditionqueryCondition=parseQueryCondition(params);List>mapList=lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);如果(CollectionUtils.isNotEmpty(mapList)){for(Mapdata:mapList){resultMaps.add(camelKey(data));}}returnresultMaps;}//lianNuDao.queryPoolSkuList@Autowired@Qualifier("ckSqlNewSession")privateSqlSessionsqlSession;publicList>queryPoolSkuList(Stringdt,QueryConditionqueryCondition){queryCondition.setDt(dt);queryCondition.checkMultiQueryItems();returnsqlSession.selectList("LianNu.queryPoolSkuList",queryCondition);}sqlSession.selectList方法中调使用了和CK交互的queryPoolSkuList查询方法,部分代码如下。selectsku_pool_id,item_sku_id,skuPoolName,price,......businessTypefromliannu_sku_pool_indicator_allwheredt=#{dt}and${queryItem.field}=#{queryItem.value}......按sku_pool_id,item_sku_id分组按sku_pool_id,item_sku_id,${orderBy}排序${orderBy}${orderAd}limit#{limitStart},#{limitEnd}可以看到,在CK页面查询时,是通过limit#{limitStart},#{limitEnd}实现的分页限制分页方案在“深度翻页”时会出现性能问题。第一版方案上线后,从1000W锅数据中过滤出10W数据最差也需要10s到18s左右。3、使用ESScrollScan优化深度翻页针对CK深度翻页的性能问题进行优化,使用Elasticsearch的scrollscan翻页方案进行优化。1.ES的翻页方案ES的翻页方案有以下几种:from+size翻页scroll翻页scrollscan翻页翻页后搜索对于上述翻页方案,不同数量的数据被查询,耗时数据见下表。2.耗时数据这里使用Elasticsearch的滚动扫描翻页方案和第一版中的CK翻页方案查询数据,比较它们的耗时数据。从上面的测试数据可以看出,以10万、10万、1000万级别的锅为例。锅的量级越大,查询同样数量的数据会越费时。当查询结果在3W以下时,ES性能优秀;当查询结果在5W以上时,CK多线程性能优秀。4.ES+Hbase组合查询方案在《使用ES滚动扫描优化深度翻页》中,使用了Elasticsearch的滚动扫描翻页方案来优化深度翻页问题,但是实现时是单线程调用,所以最终测试的耗时数据不是特别理想,其性能和CK翻页方案差不多。研究阶段发现,当10W的目标数据从锅中取出时,一个产品包含多个字段的信息(CK表中一行有150个字段信息),如价格、会员价、学生价格、库存、优惠率等。对于一行记录,当获取的字段数量减少时,查询耗时会明显减少。例如sku1的商品,从之前获取价格、会员价、学生价、亲友价、库存等100个字段信息,缩减为只有价格和库存两??个字段信息。如下图,使用ES查询方案,对于查询相同条数记录的场景(从千万盆中筛选出70000+条数据),每条记录获取的字段数减少从32个减到17个,再减到1个(其实是两个字段,一个是商品的唯一标识sku_id,一个是ES为每个文档记录的doc_id),查询时间会从9.3s下降到4.2s,然后到2.4s。由此,我们可以得出以下结论:在ES查询中,如果查询字段和信息较多,fetch阶段的耗时要比query阶段长很多。在ES查询中,如果查询字段和信息较多,通过减少不必要的查询字段,可以显着缩短查询时间。下面对结论中涉及的query和fetch查询阶段进行补充说明。1.ES查询的两个阶段:query和fetch在ES中,搜索一般包括两个阶段,query和fetch。在查询阶段,根据查询条件,判断取哪些文档(doc),过滤出文档ID(doc_id)。fetch阶段根据query阶段返回的文档ID(doc_id)提取具体的文档(doc)。2.ES的文件系统缓存1)ES会自动将磁盘中的数据缓存到文件系统缓存中,并在内存中查找,提高了速度。2)如果文件系统缓存无法容纳索引数据文件,则会基于磁盘搜索,此时查询速度会明显变慢。3)如果数量过多,基于“ES查询的query和fetch两个阶段”,可以采用ES+HBase的架构,保证ES的数据量小于文件系统缓存,查询速度有保证。3、结合使用Hbase通过以上研究发现,“减少不必要的查询显示字段”可以显着缩短查询时间。遵循这种优化思路,参考相关资料设计了一种新的查询方案。ES只用于条件过滤,ES的查询结果只包含记录的唯一标识sku_id(实际上还包含ES记录的每篇文档的doc_id)。Hbase是一个列存储数据库,每一列数据都有一个rowKey。使用rowKey过滤一条记录时,复杂度为O(1)。(类似于根据key从HashMap中取值)。根据ES查询返回的唯一标识sku_id,在Hbase查询中作为rowKey获取其他信息字段,如价格、库存等,复杂度为O(1)。使用ES+Hbase组合查询方案,在线进行了小规模的灰度测试。从1000W锅数据中筛选出10W数据,对比CK翻转方案,最差耗时从10~18s优化到3~6s左右。还需要注意的是,使用ES+Hbase的组合查询方案会增加系统的复杂度,数据也需要同时存储在ES和Hbase中。5、RediSearch+RedisJSON优化方案RediSearch是一个基于Redis的分布式全文搜索和聚合引擎,可以极快的速度对Redis数据集执行复杂的搜索查询。RedisJSON是一个Redis模块,在Redis中提供JSON支持。RedisJSON可以与RediSearch无缝协作来索引和查询JSON文档。根据一些参考,RediSearch+RedisJSON可以达到极高的性能,可以说是碾压其他NoSQL方案。在后续的版本迭代中,可以考虑对该方案进行进一步优化。RediSearch+RedisJSON的一些性能数据如下。1、RediSearch性能数据在相同服务器配置下索引了560万个文档(5.3GB),RediSearch建索引耗时221秒,Elasticsearch耗时349秒。RediSearch比ES快58%。数据被索引后,使用32个客户端检索两个词。RediSearch的吞吐量达到了12.5Kops/sec,ES的吞吐量达到了3.1Kops/sec。RediSearch比ES快4倍。同时,RediSearch的延迟是8ms,而ES是10ms,RediSearch的延迟稍低。2、RedisJSON性能数据根据官网的性能测试报告,RedisJson+RedisSearch可以说是碾压其他NoSQL。对于隔离写,RedisJSON比MongoDB快5.4倍,比ES快200多倍。对于隔离读取,RedisJSON比MongoDB快12.7倍,比ES快500多倍。在混合工作负载场景下,实时更新不会影响RedisJSON的搜索和读取性能,而ES会。RedisJSON支持的操作数是MongoDB的50倍/秒,是ES的7倍/秒。RedisJSON的延迟比MongoDB低约90倍,比ES低23.7倍。此外,RedisJSON的读取、写入和加载搜索延迟在更高的百分位数上远比ES和MongoDB稳定。RedisJSON也可以在提高写入率时处理越来越高的整体吞吐量。并且当写入比例增加时,ES会降低它可以处理的整体吞吐量。总结本文由一个业务需求引发,针对“从千万级数据中查询10W级数据”,介绍了不同的设计方案。对于“从1000W锅数据中过滤10W数据”的场景,不同方案的耗时如下:多线程+CK翻页方案,最差耗时10s~18s。单线程+ESscrollscan深度翻页的方案相比CK方案没有明显优化。ES+Hbase组合方案,最差耗时优化到3s~6s。RediSearch+RedisJSON组合方案,这个方案的耗时后面会实测。