测试环境html
本文简单对比下Solr与MySQL的查询性能速度。ios
测试数据量:10407608 Num Docs: 10407608apache
普通查询json
这里对MySQL的查询时间都包含了从MySQL Server获取数据的时间。缓存
在项目中一个最经常使用的查询,查询某段时间内的数据,SQL查询获取数据,30s左右网络
SELECT * FROM `tf_hotspotdata_copy_test` WHERE collectTime BETWEEN '2014-12-06 00:00:00' AND '2014-12-10 21:31:55';
对collectTime创建索引后,一样的查询,2s,快了不少。app
Solr索引数据:性能
<!--Index Field for HotSpot--> <field name="CollectTime" type="tdate" indexed="true" stored="true"/> <field name="IMSI" type="string" indexed="true" stored="true"/> <field name="IMEI" type="string" indexed="true" stored="true"/> <field name="DeviceID" type="string" indexed="true" stored="true"/>
Solr查询,一样的条件,72ms测试
"status": 0, "QTime": 72, "params": { "indent": "true", "q": "CollectTime:[2014-12-06T00:00:00.000Z TO 2014-12-10T21:31:55.000Z]", "_": "1434617215202", "wt": "json" }
好吧,查询性能提升的不是一点点,用Solrj代码试试:优化
SolrQuery params = new SolrQuery(); params.set("q", timeQueryString); params.set("fq", queryString); params.set("start", 0); params.set("rows", Integer.MAX_VALUE); params.setFields(retKeys); QueryResponse response = server.query(params);
Solrj查询并获取结果集,结果集大小为220296,返回5个字段,时间为12s左右。
为何须要这么长时间?上面的"QTime"只是根据索引查询的时间,若是要从solr服务端获取查询到的结果集,solr须要读取stored的字段(磁盘IO),再通过Http传输到本地(网络IO),这二者比较耗时,特别是磁盘IO。
时间对比:
查询条件 |
时间 |
MySQL(无索引) |
30s |
MySQL(有索引) |
2s |
Solrj(select查询) |
12s |
如何优化?看看只获取ID须要的时间:
SQL查询只返回id,没有对collectTime建索引,10s左右:
SELECT id FROM `tf_hotspotdata_copy_test` WHERE collectTime BETWEEN '2014-12-06 00:00:00' AND '2014-12-10 21:31:55';
SQL查询只返回id,一样的查询条件,对collectTime建索引,0.337s,很快。
Solrj查询只返回id,7s左右,快了一点。
id Size: 220296
Time: 7340
时间对比:
查询条件(只获取ID) |
时间 |
MySQL(无索引) |
10s |
MySQL(有索引) |
0.337s |
Solrj(select查询) |
7s |
继续优化。。
关于Solrj获取大量结果集速度慢的一些相似问题:
http://stackoverflow.com/questions/28181821/solr-performance#
http://grokbase.com/t/lucene/solr-user/11aysnde25/query-time-help
http://lucene.472066.n3.nabble.com/Solrj-performance-bottleneck-td2682797.html
这个问题没有好的解决方式,基本的建议都是作分页,可是咱们须要拿到大量数据作一些比对分析,作分页没有意义。
偶然看到一个回答,solr默认的查询使用的是"/select" request handler,能够用"/export" request handler来export结果集,看看solr对它的说明:
It's possible to export fully sorted result sets using a special rank query parser and response writer specifically designed to work together to handle scenarios that involve sorting and exporting millions of records. This uses a stream sorting techniquethat begins to send records within milliseconds and continues to stream results until the entire result set has been sorted and exported.
Solr中已经定义了这个requestHandler:
<requestHandler name="/export" class="solr.SearchHandler"> <lst name="invariants"> <str name="rq">{!xport}</str> <str name="wt">xsort</str> <str name="distrib">false</str> </lst> <arr name="components"> <str>query</str> </arr> </requestHandler>
使用/export须要字段使用docValues创建索引:
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" docValues="true"/> <field name="CollectTime" type="tdate" indexed="true" stored="true" docValues="true"/> <field name="IMSI" type="string" indexed="true" stored="true" docValues="true"/> <field name="IMEI" type="string" indexed="true" stored="true" docValues="true"/> <field name="DeviceID" type="string" indexed="true" stored="true" docValues="true"/>
使用docValues必需要有一个用来Sort的字段,且只支持下列类型:
Sort fields must be one of the following types: int,float,long,double,string
docValues支持的返回字段:
Export fields must either be one of the following types: int,float,long,double,string
使用Solrj来查询并获取数据:
SolrQuery params = new SolrQuery(); params.set("q", timeQueryString); params.set("fq", queryString); params.set("start", 0); params.set("rows", Integer.MAX_VALUE); params.set("sort", "id asc"); params.setHighlight(false); params.set("qt", "/export"); params.setFields(retKeys); QueryResponse response = server.query(params);
一个Bug:
org.apache.solr.client.solrj.impl.HttpSolrClient$RemoteSolrException: Error from server at http://192.8.125.30:8985/solr/hotspot: Expected mime type application/octet-stream but got application/json.
Solrj无法正确解析出结果集,看了下源码,缘由是Solr server返回的ContentType和Solrj解析时检查时不一致,Solrj的BinaryResponseParser这个CONTENT_TYPE是定死的:
public class BinaryResponseParser extends ResponseParser { public static final String BINARY_CONTENT_TYPE = "application/octet-stream";
一时半会也不知道怎么解决这个Bug,仍是本身写个Http请求并获取结果吧,用HttpClient写了个简单的客户端请求并解析json获取数据,测试速度:
String url = "http://192.8.125.30:8985/solr/hotspot/export?q=CollectTime%3A[2014-12-06T00%3A00%3A00.000Z+TO+2014-12-10T21%3A31%3A55.000Z]&sort=id+asc&fl=id&wt=json&indent=true"; long s = System.currentTimeMillis(); SolrHttpJsonClient client = new SolrHttpJsonClient(); SolrQueryResult result = client.getQueryResultByGet(url); System.out.println("Size: "+result.getResponse().getNumFound()); long e = System.currentTimeMillis(); System.out.println("Time: "+(e-s));
一样的查询条件获取220296个结果集,时间为2s左右,这样的查询获取数据的效率和MySQL创建索引后的效果差很少,暂时能够接受。
为何使用docValues的方式获取数据速度快?
DocValues是一种按列组织的存储格式,这种存储方式下降了随机读的成本。
传统的按行存储是这样的:
1和2表明的是docid。颜色表明的是不一样的字段。
改为按列存储是这样的:
按列存储的话会把一个文件分红多个文件,每一个列一个。对于每一个文件,都是按照docid排序的。这样一来,只要知道docid,就能够计算出这个docid在这个文件里的偏移量。也就是对于每一个docid须要一次随机读操做。
那么这种排列是如何让随机读更快的呢?秘密在于Lucene底层读取文件的方式是基于memory mapped byte buffer的,也就是mmap。这种文件访问的方式是由操做系统去缓存这个文件到内存里。这样在内存足够的状况下,访问文件就至关于访问内存。那么随机读操做也就再也不是磁盘操做了,而是对内存的随机读。
那么为何按行存储不能用mmap的方式呢?由于按行存储的方式一个文件里包含了不少列的数据,这个文件尺寸每每很大,超过了操做系统的文件缓存的大小。而按列存储的方式把不一样列分红了不少文件,能够只缓存用到的那些列,而不让不多使用的列数据浪费内存。
注意Export fields只支持int,float,long,double,string这几个类型,若是你的查询结果只包含这几个类型的字段,那采用这种方式查询并获取数据,速度要快不少。
下面是Solr使用“/select”和“/export”的速度对比。
时间对比:
查询条件 |
时间 |
MySQL(无索引) |
30s |
MySQL(有索引) |
2s |
Solrj(select查询) |
12s |
Solrj(export查询) |
2s |
项目中若是用分页查询,就用select方式,若是一次性要获取大量查询数据就用export方式,这里没有采用MySQL对查询字段建索引,由于数据量天天还在增长,当达到亿级的数据量的时候,索引也不能很好的解决问题,并且项目中还有其余的查询需求。
分组查询
咱们来看另外一个查询需求,假设要统计每一个设备(deviceID)上数据的分布状况:
用SQL,须要33s:
SELECT deviceID,Count(*) FROM `tf_hotspotdata_copy_test` GROUP BY deviceID;
一样的查询,在对CollectTime创建索引以后,只要14s了。
看看Solr的Facet查询,只要540ms,快的不是一点点。
SolrQuery query = new SolrQuery(); query.set("q", "*:*"); query.setFacet(true); query.addFacetField("DeviceID"); QueryResponse response = server.query(query); FacetField idFacetField = response.getFacetField("DeviceID"); List<Count> idCounts = idFacetField.getValues(); for (Count count : idCounts) { System.out.println(count.getName()+": "+count.getCount()); }
时间对比:
查询条件(统计) |
时间 |
MySQL(无索引) |
33s |
MySQL(有索引) |
14s |
Solrj(Facet查询) |
0.54s |
若是咱们要查询某台设备在某个时间段上按“时”、“周”、“月”、“年”进行数据统计,Solr也是很方便的,好比如下按天统计设备号为1013上的数据:
String startTime = "2014-12-06 00:00:00"; String endTime = "2014-12-16 21:31:55"; SolrQuery query = new SolrQuery(); query.set("q", "DeviceID:1013"); query.setFacet(true); Date start = DateFormatHelper.ToSolrSearchDate(DateFormatHelper.StringToDate(startTime)); Date end = DateFormatHelper.ToSolrSearchDate(DateFormatHelper.StringToDate(endTime)); query.addDateRangeFacet("CollectTime", start, end, "+1DAY"); QueryResponse response = server.query(query); List<RangeFacet> dateFacetFields = response.getFacetRanges(); for (RangeFacet facetField : dateFacetFields{ List<org.apache.solr.client.solrj.response.RangeFacet.Count> dateCounts= facetField.getCounts(); for (org.apache.solr.client.solrj.response.RangeFacet.Count count : dateCounts) { System.out.println(count.getValue()+": "+count.getCount()); } }
这里为何Solr/Lucene的Facet(聚合)查询会这么快呢?
想一想Solr/Lucene的索引数据的方式就清楚了:倒排索引。对于某个索引字段,该字段下有哪几个值,对于每一个值,对应的文档集合是创建索引的时候就清楚的,作聚合操做的时候“统计”下就知道结果了。
若是经过docValues创建索引,对于这类Facet查询会更快,由于这时候索引已经经过字段(列)分割好了,只须要去对应文件中查询统计就好了,如上文所述,经过“内存映射”,将该索引文件映射到内存,只须要在内存里统计下结果就出来了,因此就很是快。
水平拆分表:
因为本系统采集到的大量数据和“时间”有很大关系,一些业务需求根据“时间”来查询也比较多,能够按“时间”字段进行拆分表,好比按每个月一张表来拆分,可是这样作应用层代码就须要作更多的事情,一些跨表的查询也须要更多的工做。综合考虑了表拆分和使用Solr来作索引查询的工做量后,仍是采用了Solr。
总结:在MySQL的基础上,配合Lucene、Solr、ElasticSearch等搜索引擎,能够提升相似全文检索、分类统计等查询性能。
参考:
http://wiki.apache.org/solr/
https://lucidworks.com/blog/2013/04/02/fun-with-docvalues-in-solr-4-2/