ES(Elastic Search)针对日志场景(读测试)

fdwm 9年前

为了能衡量ES查询日志的成本与性能,我们团队北洲同学做的ES相关实验(ZZ):

目的

对Elastic Search在日志查询场景下的性能有一个比较直观的认识,该场景可以描述为单TermQuery、时间区间为一个小时、按照时间降序排列、查询结果支持翻页、每页两百条。

数据集合:我们构造了两组索引,分别是perf_simple和acc_20k,两组索引分别用于模拟日志流量比较小的用户以及流量比较大的用户,perf_simple包含189887481个document,索引大小为40.64G,acc_20k包含907786934个doc,索引大小为270.02G,

环境配置:四个es服务部署在同一台机器上,16个shard,0个replica,每个es服务自动分配四个shard。服务器配置为:

  • Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GH 24Core
  • 96GB
  • 1W SATA

准备

压测使用JMeter工具,查询语句语义上等价于下面的sql:

select doc from perf_simple
where message=“${term}” and timestamp in [t0, t1]
order by timestamp desc limit 200 offset ${offset};

其中message是索引中的一个field,建索引时,该字段中包含类似200.300.500.600的子串,英文句点被作为分词token,目的主要是为了方便构造TermQuery,term是可变量,offset是命中结果的偏移,”order by timestamp desc;limit 200 offset $offset“含义就是hits按照time降序排列之后取offset之后的200条。

perf_simple和acc_20k对应的索引格式相同,不同的地方在message字段的取值,字段格式举例如下:

"message":"15852.5917.15852.4971 - - [27/Mar/2015:09:39:49 +0800] \"POST /PutData?APIVersion=0.3.0&AccessKeyId=&Date=MonMarxxxx&yabace=deqa&acdea=xxxeeqaGMT\" 15852 4402 7124 3375 \"-\" \"my-logagent\"",
"@version":"1",
"@timestamp":"2015-03-27T02:13:16.567Z",
"type":"perf",
"host":"rs1i07386.et2sqa",
"path":"/home/admin/test/test.log_1427420275"

perf_simple message字段中类似200.300.500.600的子串部分term的取值范围是200到2000,acc_20k取值范围是200到20000。

测试场景:

对perf_simple索引进行测试

在此场景下term为[200,2000]中的随机值,此场景下hits count 在time range为1 hour时数量级为几十万条,所以offset取[0,100000]中的随机数,在time range为10 min时数量级为几万条,此时offset取[0,10000]中的随机数,这样做的目的是为了模拟用户翻页。按照timestamp的range将场景 1分为两部分,1hour和10min。

1.1 (t1-t0 == 1 hour) 读1小时时间段日志

 

Threads samples average latency(ms) qps request size per sec(KB/s)
1 5201 98 10.1 632
7 11001 225 30.5 1905.6

Threads是用于压测的线程数量,samples是样本数量,average latency是请求的平均延迟,单位是毫秒,qps是请求吞吐量,request size per sec是每秒请求的平均大小,单位是KB/s。

1.2 (t1-t0 == 10 min) 读10分钟时间段日志

 

Threads samples average latency(ms) qps request size per sec(KB/s)
1 17501 15 61.8 2172.5
5 23201 39 120.4 4178.6
10 17982 72 130.9 4558.2
15 21001 108 130.2 4574.7

表格中最后一组数据都是把es服务所在机器的cpu打满情况下的数据,jvm heap大小设置为11.85g。

对acc_20k索引进行测试(大用户)

2.1 大用户标准场景测试

变量的取值范围为Offset:[0,10000],Term: [200,20000],Time range: 1 hour。该场景下hits count为几万条。

 

Threads samples average latency(ms) qps request size per sec(KB/s)
1 17088 10 71.4 4503.4
5 27760 21 194.4 12246.8
10 104441 39 229.3 14442.3
15 82601 62 232.9 14679.8

2.2 测试极限场景性能(几乎不复用Cache)

offset从0开始每次递增200,Term固定为PutData,Time range为1hour,该场景下hits count为几亿条

 

Threads samples average latency(ms) qps request size per sec(KB/s) cpu usage jvm heap usage(usage / limit)
1 1891 4389 13.6/min 14.4 50% 6.14g / 11.85g
2 1414 5373 22.3/min 23.6 80% 8g / 11.85g
3 1010 6237 28.7/min 30.1 96% 8.44g / 11.85g

结果分析

排除client瓶颈

压测过程中没有发现error的情况,怀疑是瓶颈主要在client端,后来把单机15线程,分配到3台机器,5threads/machine,发现两种方式结果一样,说明瓶颈在服务端。

延迟包含哪些因素

ES读索引初始cpu iowait很高,此时latency比较大,说明disk io占了latency很大一部分,随着读的进行,iowait降低,latency变小,主要是因为索引被cache(es使用了mmap)。上面的测试结果是在cpu iowait较低的情况下获得的,这样也比较贴合用户场景,因此下面的分析将主要考虑计算的latency。

结果分析

  • 1.1和1.2对比time range对latency的影响:time range是1 hour的latency更大,此外随着读压力的增大,latency也相应的升高,但是qps随着读压力增大,刚开始是上升,后来便遇到瓶颈,接着便开始下降了。在实际场景中需要权衡好单机吞吐量,准确获取吞吐量瓶颈以及和请求延迟的关系对用户体验至关重要,除此以外,这些信息对于集群规模大小的考量也很重要。
  • 1.1和1.2对比还可以发现term query的数量对查询性能影响也是很大的,虽然lucene对于类似date range的数值range查询做了一些优化,使得query parser出来的query数量并不会随着range的变大而呈正比变大,但是在我们测试的例子中,range由10min变大到1hour,time range query parser之后子term query变多了,这就意味着需要合并的倒排表数量变多了。
  • 2.1是对acc_20k的测试,索引量放大了将近一个数量级,message中term range也放大了一个数量级,这就意味着term的倒排表长度在同一个数量级,从结果来看2.1和场景1.2中的测试数据基本上处在同一个level,这说明es的词典大小以及索引量对性能影响不大,性能好坏主要影响因素是term的倒排表长度。2.2进一步验证了2.1中的推论,使用 message:PutData作为查询词,hits count数量级为几亿条,latency明显升高很多。
  • es的查询性能还是非常好的,主要体现在延时方面, 从测试的结果来看,查询1亿条数据查询命中几十万次(场景1.1),需要100ms左右, 命中几万次(场景2),只需要40ms左右。

综上所述es的查询性能主要和两方面因素有关系,分别是query parser之后term query的数量和倒排表长度,和被索引的文档数量没有直接的关系,这就指导我们分析的时候从三方面着手,分别是系统的查询串复杂度、term规模、索引文档规模,查询串的复杂度主要通过parser之后term的数量衡量。

一些推测

结合之前对lucene的调研,对测试结论给出一些依据。假设在词典中,distinct字母数量为n,使用树存储词典(不考虑优化),树的孩子遵从字母序,词的平均长度为k,那么找到一个词的最坏时间为k*logn(二分查找)。从这里可以看出查找一个词和词典的大小几乎没有关系,只和词典中词的分布有关系(不同的document集合,词典不一样,从而树的结构也不一样)。上面分析可以得出一个重要的结论:document上一定规模之后,在词典中找词的时间基本上不会随着doc规模变化。


词找到了,接着就是找倒排表,假设词典中每个词都可以携带数据(payload),这样的数据包括倒排表的文件偏移、meta的文件偏移,meta包括词的倒排表长度等,那么找到词的同时也就找到了倒排表的位置,倒排表使用SkipList组织,这就意味着假如要在倒排表中找某一个特定的词复杂度数量级为log(doc count),log的底取决于SkipList的Skip步长,如果是多个term query,and查询,skip list求交集,可以互相之间加速,并且skiplist可以优化,比如在叶子上记录parent偏移,skip list求交集将更快。粗略评估下复杂度,假设m个term,doc数量为n,复杂度为m*n*logn,这个结果没有考虑skip list的优化。对于日期区间查询,query parser会按照类似前缀树重写query,这个之前分享过,这样就会大大减少m。Order by可以通过doc value perfield做,也就是通过空间换时间做一个doc->field value的map,查询结果到这里排序。以上讨论只针对lucene中一个segment的情况,以上过程在每个segment中都会走一遍,不同 seg的结果需要merge。

来自:http://simplelogservice.com/?p=10554