Day19 ES内存那点事

作者: kennywu76   发布时间:2015-12-22
【携程旅行网  吴晓刚】
 
注: 本文主要针对ES 2.x。

 “该给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
8. 对高cardinality字段做terms aggregation


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 (5.x里叫做Request cache)
Filter cache是用来缓存使用过的filter的结果集的,需要注意的是这个缓存也是常驻heap,在被evict掉之前,是无法被GC的。我的经验是默认的10% heap设置工作得够好了,如果实际使用中heap没什么压力的情况下,才考虑加大这个设置。


Field Data cache
在有大量排序、数据聚合的应用场景,可以说field data cache是性能和稳定性的杀手。 对搜索结果做排序或者聚合操作,需要将倒排索引里的数据进行解析,按列构造成docid->value的形式才能够做后续快速计算。 对于数据量很大的索引,这个构造过程会非常耗费时间,因此ES 2.0以前的版本会将构造好的数据缓存起来,提升性能。但是由于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这样的方式; 对大结果集做深度分页;一次性拉取全量数据等等。
 
对高cardinality字段做terms aggregation
所谓高cardinality,就是该字段的唯一值比较多。 比如client ip,可能存在上千万甚至上亿的不同值。 对这种类型的字段做terms aggregation时,需要在内存里生成海量的分桶,内存需求会非常高。如果内部再嵌套有其他聚合,情况会更糟糕。  在做日志聚合分析时,一个典型的可以引起性能问题的场景,就是对带有参数的url字段做terms aggregation。 对于访问量大的网站,带有参数的url字段cardinality可能会到数亿,做一次terms aggregation内存开销巨大,然而对带有参数的url字段做聚合通常没有什么意义。 对于这类问题,可以额外索引一个url_stem字段,这个字段索引剥离掉参数部分的url。可以极大降低内存消耗,提高聚合速度。


小结:
  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. 根据监控数据理解内存需求,合理配置各类circuit breaker,将内存溢出风险降低到最低。

51 个评论

赞,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
赞赞赞
optimize最后能减少内存吗? 看上面各位的意见不一
生产集群验证过,可以的。
围观M大于wood叔意见不一,wood叔是否在生产环境中合并时segment中有大量标记为删除的索引?
和有无删除数据无关,合并的确会减少内存消耗,生产实际验证过。
我使用了_forcemerge测试,对于占有内存1.1g的一个index,执行_forcemerge后,内存减小到1g
有个问题:一个segment里是包含多个倒排索引还是一个?因为写入的每个doc都对多个字段建立了倒排索引,这样每个doc的数据会落在不同字段的多个倒排索引上。这样理解的话,每个segment里也应该是包含多个倒排索引吧
每个segment包含所有字段的倒排索引。 其中.tim文件包含所有字段的term dictionary; 另外有一个.tip后缀的文件存放所有字段term dictionary的索引,通过这个文件可以先定位到某个字段的term dictionary索引(FST编码)存放的位置,然后通过这个索引快速查找到该字段的某个term在.tim文件里的存放位置。
生产验证可以_forcemerge后可以有效降低内存消耗,给出一定时间进行扩容准备。
有个问题请教下 我在Docker中运行ES 没有存储任何数据时 容器刚刚启动就消耗宿主机上800M内存 是什么原因呢
一些参数:jvm Heap分配是512M, es5.6
JVM应用除了需要分配heap内存以外,虚拟机本身还需要一些额外的系统内存,也就是off heap memory。 这块内存空间用来存放类,线程堆栈等其他非堆上的对象,并且有些库,比如netty,还会用到Direct Memory来提高性能,这块的内存也不是在heap上的。所以实际内存消耗会比heap分配的多一些。

有一个叫做Native Memory Tracking的工具,可以用来查看内存分配情况,比如给ES分配了512M heap,JVM启动后实际内存消耗可能是这样的:
Native Memory Tracking:

Total: reserved=2094912KB, committed=818416KB
- Java Heap (reserved=524288KB, committed=524288KB)
(mmap: reserved=524288KB, committed=524288KB)

- Class (reserved=1101393KB, committed=59745KB)
(classes #11082)
(malloc=1617KB #19498)
(mmap: reserved=1099776KB, committed=58128KB)

- Thread (reserved=89796KB, committed=89796KB)
(thread #88)
(stack: reserved=89088KB, committed=89088KB)
(malloc=273KB #448)
(arena=434KB #177)

- Code (reserved=252392KB, committed=17548KB)
(malloc=2792KB #5312)
(mmap: reserved=249600KB, committed=14756KB)

- GC (reserved=19272KB, committed=19272KB)
(malloc=17560KB #240)
(mmap: reserved=1712KB, committed=1712KB)

- Compiler (reserved=595KB, committed=595KB)
(malloc=78KB #218)
(arena=517KB #8)

- Internal (reserved=52356KB, committed=52352KB)
(malloc=52320KB #18499)
(mmap: reserved=36KB, committed=32KB)

- Symbol (reserved=13813KB, committed=13813KB)
(malloc=12079KB #110545)
(arena=1734KB #1)

- Native Memory Tracking (reserved=2598KB, committed=2598KB)
(malloc=142KB #2206)
(tracking overhead=2457KB)

- Arena Chunk (reserved=90KB, committed=90KB)
(malloc=90KB)

- Unknown (reserved=38320KB, committed=38320KB)
(mmap: reserved=38320KB, committed=38320KB)
今天ES 索引遇到OOM问题,发现wood大佬写过这么好的干货。赞
刚接触elastic,早上遇到OOM,看了之后收获很多,非常感谢wood大佬的干货
现在正在研究ES内存的组成,看到这个清晰了很多,谢谢 wood大叔
有个问题想请教下:
文中说”每个segment都有会一些索引数据驻留在heap里“,如果一个索引中经常有新数据加入,意味着不断有新的segment生成,这些segment的索引数据什么时候进入heap?如果ES重启了,ES中所有的segment的索引数据何时进入heap?
这些segment经过内部的refresh,能够被检索的时候,就已经进入了heap。 如果ES重启了,shard会经过一个recovery的过程,所有被recover过后状态为STARTED的shard,其包含的segments的索引数据就已经进入了heap。
是被检索后进入heap,还是refresh后就进入heap了?我现在经常碰到一个问题,第一次搜索一个词时,比较慢,后面再搜就比较快了,过段时间(比如一两天)再搜这个词,又慢了。
另外 ”每个segment都有会一些索引数据驻留在heap里“---如果一个segment没有跟其他任何segment合并,而且ES也不重启,是不是它的索引数据就一直常驻heap?
refresh以后就进入heap了。 你遇到的搜索开始比较慢的问题,主要受两个因素影响。 一个是如果索引有持续的数据写入,会产生新的segment并触发后台的segment合并。 当segment被合并以后,之前读写的文件从os page cache里evict出去了。 所以一段时间以后,搜索要从合并后的segment里读取数据,需要重新装载到pag cache里,明显就慢了。 另外一个因素是某些搜索会受益query cache,也就是检索的结果被cache起来了,短时间同一个词的搜索可能直接从query cache里拿结果,不需要再去访问segment 文件。 但是query cache大小是有限的,一段时间后,可能因为其他搜索条件用的更频繁被cache起来,导致不怎么用的条件被evcit,这样搜索就需要访问segment file。

还要澄清一下,我文章里说的常驻heap的,是用于快速访问segment文件的一种索引结构。 segment本身不会常驻heap,只有被搜索访问到的文件块会被os page cache缓存。
楼主遇到过2.x版本内存泄漏的问题吗
是cache导致的内存泄漏
Indexing Buffer 这块的描述有个疑问,应该是 refresh interval 到达后,数据写到文件缓冲区 而不是写到磁盘吧。写磁盘是flush的时候通过FileChannel.force触发的
应用层面看是“写磁盘”操作,但是系统层面是先写到文件缓冲区,在translog flush,或者os层面的dirty page过多触发写回磁盘的时候,才真实的持久到磁盘上。
没错 我是这么理解的。

话说这个脏页过多触发写磁盘,脏页的阈值是系统设置的么 还是应用设置的呢?
这个是linux内核的设置,比如我们的服务器上相关设置如下:
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
感谢解答。我之前做过测试,发现使用FileChannel 写数据 但是没有执行 fource方法,tail 文件中还是能够看到数据。系统是Windows 和 mac osx 都试过。看样子这个是因为当前系统脏页占比很低的原因吧
因此Lucene为词典做了一层前缀索引(Term Index), 这段话容易引起歧义,我一开始以为是先查fst, 然后再查字典,再去硬盘里面找segment对应的文件。我觉得这句话应该写成 ”因此Lucene为词典找到一个更好的数据结构fst来实现字典功能,以减少内存的使用“
多谢指正,你说的没错。
高cardinality字段,这种数据在读写的时候有什么要注意的吗,我在存储url及其点击信息的时候,发现当数据达到6亿多的时候,读写就变得很慢。默认的5 shards,645918492条,365.1gb,url试过keyword和text,效果一样,6亿多就慢到无法忍受
写入不应该很慢,读方面,做terms aggregation会很慢。
测过,写真的很慢,term查询也很慢,这种有没有办法解决让它变快
bulk 100条大概需要8秒
应该是有其他问题,即使cardinaility很高,也不会有这么慢的写入速度。 需要观察监控数据,看写入期间,系统各项指标哪里存在瓶颈。
请问需要监控哪些指标,一个index的数据量大概多少比较合适,aggregation这个受影响很好理解,为什么terms查询会受影响
建议至少先安装个官方的x-pack,用里面带的监控工具看下写入期间的各项指标。 cpu利用率,load average, GC频率,GC耗时等等,信息太少无从 判断。
请问,我装了最新的es,自带了x-pack,其中有一个节点经常挂,不知道是不是机器的问题,都是新机器,日志如下,该怎么解决呢
Internal exceptions (10 events):
Event: 89191.658 Thread 0x00007f1d94008000 Exception <a 'sun/nio/fs/UnixException'> (0x00007f1f1edea778) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src/share/vm/prims/jni.cpp, line 709]
Event: 89196.855 Thread 0x00007f1d94006800 Exception <a 'sun/nio/fs/UnixException'> (0x00007f1efcad3670) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src/share/vm/prims/jni.cpp, line 709]
Event: 89197.247 Thread 0x00007f1d94003000 Exception <a 'java/lang/ClassCastException': org.elasticsearch.tasks.Task cannot be cast to org.elasticsearch.xpack.ml.action.TransportOpenJobAction$JobTask> (0x00007f1ef1c1e410) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src
Event: 89202.484 Thread 0x00007f1d94003000 Exception <a 'sun/nio/fs/UnixException'> (0x00007f1f27f14410) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src/share/vm/prims/jni.cpp, line 709]
Event: 89206.855 Thread 0x00007f1d94004800 Exception <a 'sun/nio/fs/UnixException'> (0x00007f1f5069cab0) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src/share/vm/prims/jni.cpp, line 709]
Event: 89207.950 Thread 0x00007f1d94006800 Exception <a 'java/lang/ClassCastException': org.elasticsearch.tasks.Task cannot be cast to org.elasticsearch.xpack.ml.action.TransportOpenJobAction$JobTask> (0x00007f1f0d9723d8) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src
Event: 89207.950 Thread 0x00007f1d94006800 Exception <a 'java/lang/ClassCastException': org.elasticsearch.action.support.replication.ReplicationTask cannot be cast to org.elasticsearch.xpack.ml.action.TransportOpenJobAction$JobTask> (0x00007f1f0d972710) thrown at [/RE-WORK/workspace/8-2-build-linu
Event: 89207.950 Thread 0x00007f1d94006800 Exception <a 'java/lang/ClassCastException': org.elasticsearch.action.support.replication.ReplicationTask cannot be cast to org.elasticsearch.xpack.ml.action.TransportOpenJobAction$JobTask> (0x00007f1f0d972a48) thrown at [/RE-WORK/workspace/8-2-build-linu
Event: 89207.950 Thread 0x00007f1d94006800 Exception <a 'java/lang/ClassCastException': org.elasticsearch.action.support.replication.ReplicationTask cannot be cast to org.elasticsearch.xpack.ml.action.TransportOpenJobAction$JobTask> (0x00007f1f0d972d80) thrown at [/RE-WORK/workspace/8-2-build-linu
Event: 89220.904 Thread 0x00007f1d94008000 Exception <a 'sun/nio/fs/UnixException'> (0x00007f1f15cc06b0) thrown at [/RE-WORK/workspace/8-2-build-linux-amd64/jdk8u45/3457/hotspot/src/share/vm/prims/jni.cpp, line 709]
一个索引有5亿的数据量,且这个索引有更新操作,请问多少shards合适呢?data节点有50台(16C/32G),25shards+25replica这样合理吗?
如果单次搜索的时延可以满足业务上的要求,可以这么划分。 如果时延过高,可以增加shard数量,代价是每次搜索的并发两增大,带来的额外开销更大,因而集群能支撑的峰值QPS可能会降低。 原则上,在满足搜索时延的前提下,划分尽量少的shard。

另外有一种场景划分更多的shard是合理的,那就是集群大多数搜索都会用到某个字段做过滤,比如城市id。 这个时候,可以用该字段做为routing_key,将相关联的数据route到某个或某几个(如果用到routing partition)shard。 适当多划分一些shard,可以让单个shard上的数据集较小,搜素速度快,同时因为搜索不会hit所有的shard,规避了划分过多的shard带来的并发过高,以及需要汇总的数据过多引起的性能问题。
你在社区单独开个帖子问吧,我没遇到过这种类型的问题,不清楚什么状况。
好的,谢谢
你好,在节点挂掉那一刻,我看到了如下日志。我看到您有回答过别人类似的问题,说是iowait高,磁盘故障,iowait高是因为timeout吗,请问怎么确认是这个问题,那台机器是新机器
[2018-06-27T16:12:30,865][WARN ][o.e.c.s.ClusterApplierService] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] cluster state applier task [apply cluster state (from master [master {yq01-kg-sz-es2.yq01.baidu.com_master}{YNf5ivy1RLuS_ILb8v1RsQ}{t-mOqV3uSNSwQQX2uhSb-g}{10.156.88.25}{10.156.88.25:8400}{ml.machine_memory=201028677632, ml.max_open_jobs=20, xpack.installed=true, ml.enabled=true} committed version [10907]])] took [48.2s] above the warn threshold of 30s
[2018-06-27T16:13:33,610][WARN ][o.e.c.s.ClusterApplierService] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] cluster state applier task [apply cluster state (from master [master {yq01-kg-sz-es2.yq01.baidu.com_master}{YNf5ivy1RLuS_ILb8v1RsQ}{t-mOqV3uSNSwQQX2uhSb-g}{10.156.88.25}{10.156.88.25:8400}{ml.machine_memory=201028677632, ml.max_open_jobs=20, xpack.installed=true, ml.enabled=true} committed version [10908]])] took [39.3s] above the warn threshold of 30s
[2018-06-27T16:17:47,507][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:17:57,569][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:22:38,108][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:23:30,047][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:27:26,102][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:27:39,391][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:27:56,664][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:28:08,869][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:28:22,629][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:28:46,507][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:29:11,484][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:29:33,366][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:29:58,587][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:31:58,777][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:32:12,644][ERROR][o.e.x.m.c.n.NodeStatsCollector] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] collector [node_stats] timed out when collecting data
[2018-06-27T16:32:13,088][WARN ][o.e.c.s.ClusterApplierService] [yq01-kg-sz-es2.yq01.baidu.com_datanode_1] cluster state applier task [apply cluster state (from master [master {yq01-kg-sz-es2.yq01.baidu.com_master}{YNf5ivy1RLuS_ILb8v1RsQ}{t-mOqV3uSNSwQQX2uhSb-g}{10.156.88.25}{10.156.88.25:8400}{ml.machine_memory=201028677632, ml.max_open_jobs=20, xpack.installed=true, ml.enabled=true} committed version [10944]])] took [32.7s] above the warn threshold of 30s
前后拜读过好几遍,好文章!
有一个问题请教下wood大叔:很多文章都说对segment 做merge,减少segment数量后可以提高查询速度;这里,如果segment中存在标记为delete的文档,我觉得可以理解,因为merge后会去除那些数据,从而减少了查找的文档数量。但是如果索引从来没有update 或者delete过,那么查找的文档数量并不会改变,这种情况下查询速度会变快吗?如果能,又是为什么呢?
merge不光是无力删除delete过的文档,同时也会合并小的segment文件,搜索的时候可以减少随机磁盘IO,一定程度上可以提升性能。
多谢!
你好,这块装了x-pack后,发现堆内存基本没有变化,但cpu大幅上升,用iostat监控发现io压力很大,这个过程读写延迟也很大,持续了两个小时,io压力并没有下降,这个时候es是不是在从磁盘读数据,然后由于内存不够,这块数据又从文件缓存中被挤掉,然后用到的时候又从磁盘读,磁盘不是ssd
avg-cpu: %user %nice %system %iowait %steal %idle
5.68 1.54 2.05 12.92 0.00 77.82

Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
nvme0n1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 58.50 290.50 27.50 19376.00 8384.00 87.30 51.79 194.25 3.14 100.00
sdc 0.00 10.00 0.00 15.50 0.00 244.00 15.74 0.08 4.87 1.81 2.80
sdd 0.00 17.50 429.50 15.50 18596.00 1676.00 45.56 62.18 153.49 2.25 100.00
sda 0.00 171.00 172.50 83.00 1384.00 2088.00 13.59 1.22 4.92 2.57 65.55
这个问题是之前提到的cardinaility很高(url),当数据达到5亿以上,读写就很慢

要回复文章请先登录注册