Day19 ES内存那点事

“该给ES分配多少内存?” 
“JVM参数如何优化?“
“为何我的Heap占用这么高?”
“为何经常有某个field的数据量超出内存限制的异常?“
“为何感觉上没多少数据,也会经常Out Of Memory?”

以上问题,显然没有一个统一的数学公式能够给出答案。 和数据库类似,ES对于内存的消耗,和很多因素相关,诸如数据总量、mapping设置、查询方式、查询频度等等。默认的设置虽开箱即用,但不能适用每一种使用场景。作为ES的开发、运维人员,如果不了解ES对内存使用的一些基本原理,就很难针对特有的应用场景,有效的测试、规划和管理集群,从而踩到各种坑,被各种问题挫败。

要理解ES如何使用内存,先要尊重下面两个基本事实:
1.  ES是JAVA应用
2.  底层存储引擎是基于Lucene的

看似很普通是吗?但其实没多少人真正理解这意味着什么。 

首先,作为一个JAVA应用,就脱离不开JVM和GC。很多人上手ES的时候,对GC一点概念都没有就去网上抄各种JVM“优化”参数,却仍然被heap不够用,内存溢出这样的问题搞得焦头烂额。了解JVM GC的概念和基本工作机制是很有必要的,本文不在此做过多探讨,读者可以自行Google相关资料进行学习。如何知道ES heap是否真的有压力了? 推荐阅读这篇博客:Understanding Memory Pressure Indicator。 即使对于JVM GC机制不够熟悉,头脑里还是需要有这么一个基本概念: 应用层面生成大量长生命周期的对象,是给heap造成压力的主要原因,例如读取一大片数据在内存中进行排序,或者在heap内部建cache缓存大量数据。如果GC释放的空间有限,而应用层面持续大量申请新对象,GC频度就开始上升,同时会消耗掉很多CPU时间。严重时可能恶性循环,导致整个集群停工。因此在使用ES的过程中,要知道哪些设置和操作容易造成以上问题,有针对性的予以规避。

其次,Lucene的倒排索引(Inverted Index)是先在内存里生成,然后定期以段文件(segment file)的形式刷到磁盘的。每个段实际就是一个完整的倒排索引,并且一旦写到磁盘上就不会做修改。 API层面的文档更新和删除实际上是增量写入的一种特殊文档,会保存在新的段里。不变的段文件易于被操作系统cache,热数据几乎等效于内存访问。 

基于以上2个基本事实,我们不难理解,为何官方建议的heap size不要超过系统可用内存的一半。heap以外的内存并不会被浪费,操作系统会很开心的利用他们来cache被用读取过的段文件。

Heap分配多少合适?遵从官方建议就没错。 不要超过系统可用内存的一半,并且不要超过32GB。JVM参数呢?对于初级用户来说,并不需要做特别调整,仍然遵从官方的建议,将xms和xmx设置成和heap一样大小,避免动态分配heap size就好了。虽然有针对性的调整JVM参数可以带来些许GC效率的提升,当有一些“坏”用例的时候,这些调整并不会有什么魔法效果帮你减轻heap压力,甚至可能让问题更糟糕。

那么,ES的heap是如何被瓜分掉的? 说几个我知道的内存消耗大户并分别做解读:
1.  segment memory
2.  filter cache
3.  field data cache
4.  bulk queue
5.  indexing buffer
6.  state buffer
7.  超大搜索聚合结果集的fetch


Segment Memory
Segment不是file吗?segment memory又是什么?前面提到过,一个segment是一个完备的lucene倒排索引,而倒排索引是通过词典 (Term Dictionary)到文档列表(Postings List)的映射关系,快速做查询的。 由于词典的size会很大,全部装载到heap里不现实,因此Lucene为词典做了一层前缀索引(Term Index),这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。 这种数据结构占用空间很小,Lucene打开索引的时候将其全量装载到内存中,加快磁盘上词典查询速度的同时减少随机磁盘访问次数。

下面是词典索引和词典主存储之间的一个对应关系图:

lucene_index.png


Lucene  file的完整数据结构参见Apache Lucene - Index File Formats

说了这么多,要传达的一个意思就是,ES的data node存储数据并非只是耗费磁盘空间的,为了加速数据的访问,每个segment都有会一些索引数据驻留在heap里。因此segment越多,瓜分掉的heap也越多,并且这部分heap是无法被GC掉的! 理解这点对于监控和管理集群容量很重要,当一个node的segment memory占用过多的时候,就需要考虑删除、归档数据,或者扩容了。

怎么知道segment memory占用情况呢?  CAT API可以给出答案。
1.  查看一个索引所有segment的memory占用情况:

seg_mem.png


2.  查看一个node上所有segment占用的memory总和:

seg_mem_node.png



那么有哪些途径减少data node上的segment memory占用呢? 总结起来有三种方法:
1.  删除不用的索引
2.  关闭索引 (文件仍然存在于磁盘,只是释放掉内存)。需要的时候可以重新打开。
3.  定期对不再更新的索引做optimize (ES2.0以后更改为force merge api)。这Optimze的实质是对segment file强制做合并,可以节省大量的segment memory。

Filter Cache
Filter cache是用来缓存使用过的filter的结果集的,需要注意的是这个缓存也是常驻heap,无法GC的。我的经验是默认的10% heap设置工作得够好了,如果实际使用中heap没什么压力的情况下,才考虑加大这个设置。


Field Data cache
在有大量排序、数据聚合的应用场景,可以说field data cache是性能和稳定性的杀手。 对搜索结果做排序或者聚合操作,需要将倒排索引里的数据进行解析,然后进行一次倒排。 这个过程非常耗费时间,因此ES 2.0以前的版本主要依赖这个cache缓存已经计算过的数据,提升性能。但是由于heap空间有限,当遇到用户对海量数据做计算的时候,就很容易导致heap吃紧,集群频繁GC,根本无法完成计算过程。 ES2.0以后,正式默认启用Doc Values特性(1.x需要手动更改mapping开启),将field data在indexing time构建在磁盘上,经过一系列优化,可以达到比之前采用field data cache机制更好的性能。因此需要限制对field data cache的使用,最好是完全不用,可以极大释放heap压力。 需要注意的是,很多同学已经升级到ES2.0,或者1.0里已经设置mapping启用了doc values,在kibana里仍然会遇到问题。 这里一个陷阱就在于kibana的table panel可以对所有字段排序。 设想如果有一个字段是analyzed过的,而用户去点击对应字段的排序表头是什么后果? 一来排序的结果并不是用户想要的,排序的对象实际是词典; 二来analyzed过的字段无法利用doc values,需要装载到field data cache,数据量很大的情况下可能集群就在忙着GC或者根本出不来结果。


Bulk Queue
一般来说,Bulk queue不会消耗很多的heap,但是见过一些用户为了提高bulk的速度,客户端设置了很大的并发量,并且将bulk Queue设置到不可思议的大,比如好几千。 Bulk Queue是做什么用的?当所有的bulk thread都在忙,无法响应新的bulk request的时候,将request在内存里排列起来,然后慢慢清掉。 这在应对短暂的请求爆发的时候有用,但是如果集群本身索引速度一直跟不上,设置的好几千的queue都满了会是什么状况呢? 取决于一个bulk的数据量大小,乘上queue的大小,heap很有可能就不够用,内存溢出了。一般来说官方默认的thread pool设置已经能很好的工作了,建议不要随意去“调优”相关的设置,很多时候都是适得其反的效果。


Indexing Buffer
Indexing Buffer是用来缓存新数据,当其满了或者refresh/flush interval到了,就会以segment file的形式写入到磁盘。 这个参数的默认值是10% heap size。根据经验,这个默认值也能够很好的工作,应对很大的索引吞吐量。 但有些用户认为这个buffer越大吞吐量越高,因此见过有用户将其设置为40%的。到了极端的情况,写入速度很高的时候,40%都被占用,导致OOM。


Cluster State Buffer
ES被设计成每个node都可以响应用户的api请求,因此每个node的内存里都包含有一份集群状态的拷贝。这个cluster state包含诸如集群有多少个node,多少个index,每个index的mapping是什么?有少shard,每个shard的分配情况等等 (ES有各类stats api获取这类数据)。 在一个规模很大的集群,这个状态信息可能会非常大的,耗用的内存空间就不可忽视了。并且在ES2.0之前的版本,state的更新是由master node做完以后全量散播到其他结点的。 频繁的状态更新都有可能给heap带来压力。 在超大规模集群的情况下,可以考虑分集群并通过tribe node连接做到对用户api的透明,这样可以保证每个集群里的state信息不会膨胀得过大。


超大搜索聚合结果集的fetch
ES是分布式搜索引擎,搜索和聚合计算除了在各个data node并行计算以外,还需要将结果返回给汇总节点进行汇总和排序后再返回。无论是搜索,还是聚合,如果返回结果的size设置过大,都会给heap造成很大的压力,特别是数据汇聚节点。超大的size多数情况下都是用户用例不对,比如本来是想计算cardinality,却用了terms aggregation + size:0这样的方式; 对大结果集做深度分页;一次性拉取全量数据等等。


小结:
1.  倒排词典的索引需要常驻内存,无法GC,需要监控data node上segment memory增长趋势。
2.  各类缓存,field cache, filter cache, indexing cache, bulk queue等等,要设置合理的大小,并且要应该根据最坏的情况来看heap是否够用,也就是各类缓存全部占满的时候,还有heap空间可以分配给其他任务吗?避免采用clear cache等“自欺欺人”的方式来释放内存。
3.  避免返回大量结果集的搜索与聚合。缺失需要大量拉取数据可以采用scan & scroll api来实现。
4.  cluster stats驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过tribe node连接。
5.  想知道heap够不够,必须结合实际应用场景,并对集群的heap使用情况做持续的监控。

6 个评论

赞,wood大叔出手,都是干货!

这一句再研究下:

"定期对不再更新的索引做optimize (ES2.0以后更改为force merge api)。这Optimze的实质是对segment file强制做合并,可以节省大量的segment memory。"

optimize只合并文件,合并大量小segment文件,可以减轻IO压力,貌似不能节省segment memory。
optimize的确可以减少segment memory占用的。

那我这里一个索引的举例,如果看shard 0下所有segment的内存信息 (第10列),加起来大概是3MB多一点。
$ curl -s localhost:9200/_cat/segments/mobilerestful-2015.12.23 |grep '0 p'
mobilerestful-2015.12.23 0 p 127.0.0.1 _ok 884 170981 0 57.8mb 240778 true true 4.10.4 false
mobilerestful-2015.12.23 0 p 127.0.0.1 _10c 1308 292476 0 98.9mb 387874 true true 4.10.4 false
mobilerestful-2015.12.23 0 p 127.0.0.1 _1cy 1762 100378 0 34mb 139442 true true 4.10.4 false
mobilerestful-2015.12.23 0 p 127.0.0.1 _1my 2122 504068 0 169.6mb 619642 true true 4.10.4 false
mobilerestful-2015.12.23 0 p 127.0.0.1 _25c 2784 504176 0 164.3mb 613970 true true 4.10.4 false
mobilerestful-2015.12.23 0 p 127.0.0.1 _2al 2973 62363 0 20.9mb 88978 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2gp 3193 67960 0 23.3mb 104090 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2or 3483 61453 0 21.3mb 100834 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2u1 3673 56518 0 19.3mb 86314 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2ul 3693 4034 0 1.3mb 15426 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2uv 3703 4077 0 1.4mb 15506 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2v5 3713 4011 0 1.3mb 15418 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2vq 3734 7051 0 2.4mb 20434 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2vz 3743 7999 0 2.7mb 21306 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2wa 3754 4535 0 1.5mb 15458 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2wb 3755 68 0 36.7kb 10330 true true 4.10.4 true
mobilerestful-2015.12.23 0 p 127.0.0.1 _2wc 3756 1 0 8.4kb 9938 true true 4.10.4 true

然后执行optimze
$ curl -XPOST 'http://localhost:9200/mobilerestful-2015.12.23/_optimize?max_num_segments=1'
{"_shards":{"total":10,"successful":10,"failed":0}}

成功以后,合并成为一个segment后:
$ curl -s localhost:9200/_cat/segments/mobilerestful-2015.12.23 |grep '0 p'
mobilerestful-2015.12.23 0 p 127.0.0.1 _2wd 3757 1852149 0 623.1mb 2268818 true true 4.10.4 false

segment memory只有2MB多了。


如果看整个索引(5个primary shard + 5个replica) 的memory占用在optimize前后的对比。
optimize以前是 32.8mb
$ curl -s localhost:9200/_cat/indices/mobilerestful-2015.12.23?v\&h=i,tm
i tm
mobilerestful-2015.12.23 32.8mb

optimze以后是23.1mb:
$ curl -s localhost:9200/_cat/indices/mobilerestful-2015.12.23?v\&h=i,tm
i tm
mobilerestful-2015.12.23 23.1mb

并且一个索引越大,optimize后内存缩减效果越显著。 下面两个索引数据量差不多,23号的还没有optimize,22号的已经optmize过。 index memory占用有5倍差别。

$ curl -s localhost:9200/_cat/indices/iislog-ctrip.com-2015.12.23?v\&h=index,docs.count,docs.deleted,store.size,tm
index docs.count docs.deleted store.size tm
iislog-ctrip.com-2015.12.23 1105717050 0 935.7gb 10.5gb

$ curl -s localhost:9200/_cat/indices/iislog-ctrip.com-2015.12.22?v\&h=index,docs.count,docs.deleted,store.size,tm
index docs.count docs.deleted store.size tm
iislog-ctrip.com-2015.12.22 1030387275 0 859.8gb 1.9gb
[op1@VMS06006 ~]$
赞动手实践精神
我也实践一把,稍微完善一下测试的过程,针对同样的一份数据进行合并前后的比较,昨晚在自己的小本上生成1亿条数据,结果如下:

索引过程中的记录:
watch "curl 'localhost:9200/_cat/indices/year_2014?v&h=index,docs.count,docs.deleted,store.size,tm'"

index docs.count docs.deleted store.size tm
year_2014 17883647 0 9.5gb 26.1mb

year_2014 37300148 0 19gb 53.7mb

year_2014 100000000 0 48.4gb 129.1mb

RUN大量的测试查询,对segment.size.memory没有影响

合并前的索引文件:
curl -s localhost:9200/_cat/indices/year_2014?v\&h=i,tm
i tm
year_2014 129.1mb


查看分片0的segment文件
curl -s "localhost:9200/_cat/segments/year_2014" |grep '0 p'
➜ elasticsearch-2.0.0 curl -s "localhost:9200/_cat/segments/year_2014" |grep '0 p'
http://localhost:9200/_cat/segments/year_2014?v
index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound
year_2014 0 p 127.0.0.1 _6ec 8292 5513757 0 2.6gb 7822900 true true 5.2.1 false
year_2014 0 p 127.0.0.1 _m7v 28795 10313943 0 4.9gb 12941828 true true 5.2.1 false
year_2014 0 p 127.0.0.1 _mze 29786 733643 0 365.4mb 974820 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _nvm 30946 503807 0 251.2mb 665724 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _on5 31937 459957 0 229.3mb 617356 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _pai 32778 625226 0 311.5mb 817956 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _pxb 33599 141946 0 70.9mb 274396 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _q8r 34011 303300 0 151.3mb 462676 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _qss 34732 569026 0 283.6mb 733804 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _qzf 34971 91889 0 46mb 157244 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _r8c 35292 95781 0 48mb 166212 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _rf1 35533 66684 0 33.4mb 108076 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _rl4 35752 290064 0 144.7mb 449860 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _rqo 35952 85194 0 42.7mb 141316 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _ruk 36092 32641 0 16.4mb 53476 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _rxd 36193 79675 0 40mb 130492 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s0p 36313 17902 0 8.9mb 36900 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s1k 36344 24449 0 12.2mb 45476 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s2x 36393 56077 0 28.1mb 87380 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s31 36397 352 0 215.5kb 11108 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s33 36399 349 0 213.4kb 11068 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s34 36400 363 0 222.1kb 11068 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s35 36401 353 0 216.4kb 11084 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s37 36403 1971 0 1mb 12972 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s38 36404 4 0 11.1kb 10660 true true 5.2.1 true
year_2014 0 p 127.0.0.1 _s39 36405 5 0 11.8kb 10660 true true 5.2.1 true
其它shard省略
...
shard0的size.memory求和(放excel算一下):26766512

curl -XPOST "http://localhost:9200/year_2014/_optimize?max_num_segments=1"

重新RUN大量的测试查询,保证流程一致

合并之后的segment信息
http://localhost:9200/_cat/segments/year_2014?v
index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound
year_2014 0 p 127.0.0.1 _s3a 36406 20008358 0 9.6gb 28030260 true true 5.2.1 false
year_2014 1 p 127.0.0.1 _vhu 40818 20003104 0 9.6gb 27843476 true true 5.2.1 false
year_2014 2 p 127.0.0.1 _vbt 40601 19998732 0 9.6gb 27637644 true true 5.2.1 false
year_2014 3 p 127.0.0.1 _vbs 40600 20000830 0 9.6gb 28010876 true true 5.2.1 false
year_2014 4 p 127.0.0.1 _v20 40248 19988976 0 9.6gb 27995540 true true 5.2.1 false

shard0的size.memory(取第一个就行了):28030260

curl 'localhost:9200/_cat/indices/year_2014?v&h=index,docs.count,docs.deleted,store.size,tm'
index docs.count docs.deleted store.size tm
year_2014 100000000 0 48.1gb 133mb

size.memory比较:
合并前: 26766512
合并后: 28030260

tm(memory used per index)
合并前:129.1mb
合并后:133mb

测试结果:
数据随机生成,,数据量1亿条,Elasticsearch版本2.0.0,默认分词,针对同一份数据进行合并前后的比较,segment内存占用没有太大的变化。

数据样本:
<pre>
{
"_id":
"65bd691dd9b14bdeb1a503204cd9fb58",
"_index":
"year_2014",
"_score":
1,
"_source":
{
"field1":
"100000000103",
"field2":
"19",
"field3":
"B",
"field4":
"中CH69L8",
"field5":
"某某株洲路某某fuzzy",
"field6":
"3702020000",
"field7":
"2",
"field8":
"3702020000556201",
"field9":
"2",
"field10":
"01",
"field11":
"02",
"field12":
"ftp://fuzzy.com/100000000101/28/21/5c6ea32fc3294bbe9e41ad0967ddd8b5.jpg",
"field13":
"null",
"field14":
"null",
"field15":
"2",
"field16":
"2014-09-07 4:49:33",
"field17":
"4",
"field18":
"65bd691dd9b14bdeb1a503204cd9fb58",
"field19":
"0",
"field20":
"9-1",
"field21":
"01",
"timestamp":
"1450974781",
"field22":
"931/1703/138/29/1"
},
"_type":
"fuzzy_type"
}
</pre>

查询测试脚本
<pre>
#!/bin/bash

char_table=("A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" "1" "2" "3" "4" "5" "6" "7" "8" "9" "0")

TOTAL=100000

for (( i=0; i<${TOTAL}; i++ )) do
id1=$(($RANDOM % 36))
id2=$(($RANDOM % 36))
id3=$(($RANDOM % 36))
query=${char_table[$id1]}${char_table[$id2]}${char_table[$id3]}"*"
echo ${query}
curl -s "localhost:9200/year_2014/_search?q=field4:${query}" |grep error
done

</pre>
wood 叔叔 写的真好,
赞赞赞,通读一遍,结合官方文档,能看懂marvel图表,略明白该怎么优化了。谢Wood

要回复文章请先登录注册