Elasticsearch 内存里都放了啥 ??凭什么本文能解决你 ES 99.9% 的内存问题 ?

4,309 阅读11分钟

写在前面

注意:本文基于 Elasticsearch 7.12 版本进行总结梳理

Elasticsearch 版本发行要点 这个很重要,想好你要用哪个版本,建议最新版

Elasticsearch 内存主要分为两部分

系统内存 也叫 堆外内存 JVM 内存 也叫 堆内存

系统内存主要给底层 Lucene 使用JVM 内存提供给 ES 查询时各类缓存使用

在这里插入图片描述

本文重点聊聊 堆内存。ES 在查询时到底对我的 堆内存干了点啥 ?!!

一 、ES 与 SQL 的概念映射

Mapping concepts across SQL and Elasticsearchedit 官网地址

SQLESSQL/ES
columnfield列/属性
rowdocument行/文档
tableindex表/索引
schemaimplicit命名空间/(ES 暂无对应)
catalog or databasecluster instance数据库/ ES 运行实例
clustercluster federated多个数据库/ES 集群

如上所示,ES 与 SQL 间概念并非完全一一对应,语义也有所不同,但共同点更多些。 是得益于 SQL 自然设计,很多概念是可以直接透明的移动到 ES 上的。

二、 ES 的堆内存 (JVM 内存区域)

1 . Node query cache 节点查询缓存 default 10% heap size

Node query cache 官方文档地址

这里或者叫 Filter query cache 可能更贴切一些。

Term queries and queries used outside of a filter context are not eligible for caching.

Term 查询 和 filter 上下文之外的查询是不符合该缓存条件的

filter 查询查询缓存,每个节点都有。该缓存使用 LRU 缓存淘汰策略,当缓存满时,会将最近最少使用的查询结果逐出,以腾出空间存放新数据。官网给出这样一句话:You cannot inspect the contents of the query cache 你无法检查查询缓存的内容。

默认的,该缓存将保存 最大 10000 条查询,最多占总堆空间的 10%

如果一个 segment 至少包含 10000 document ,且该 segment 至少具有一个分片 document 总数的 3% ,则按每个 segment 进行缓存,由于缓存是按 segment 划分的,因此 segment合并使缓存的查询无效

该设置是静态的 且 必须在每个节点进行设置

indices.queries.cache.size 默认为 10%

indices.queries.cache.size: 10% //按堆百分比配置 indices.queries.cache.size:512mb //指定固定大小

⚠️ 可提供的情报:heap size used : 10% free : 90% ……ing

2 . Indexing buffer 索引缓冲区 default 10% heap size

Indexing buffer 官方文档地址

Indexing buffer 用于存储 最新 被索引的 documents ,当 indexing buffer 被填满后 ,缓存的 documents 将被写入磁盘的 segment 中。

该设置是静态的,并且必须在集群中每个节点上都进行配置:

indices.memory.index_buffer_size indexing buffer 占用堆大小

indices.memory.index_buffer_size:10% //按堆百分比配置 indices.memory.index_buffer_size:512mb //指定固定大小

如果 indices.memory.index_buffer_size 指定为百分比则以下配置生效:

indices.memory.min_index_buffer_size 指定 index buffer 绝对最小值 默认 48mb indices.memory.max_index_buffer_size 指定 index buffer 绝对最大值 默认为 无界

⚠️ 可提供的情报:heap size used : 20% free : 80% ……ing

3 . Shard request cache 分片请求缓存 default 1% heap size

Shard request cache 官方文档地址

这里或者叫 Aggregations query cache 可能更贴切一些。

分片请求缓存 用于缓存请求结果。

默认的,此请求缓存仅缓存查询请求 size = 0 ,所以它不缓存 hits ,但他将缓存 hits.total ,aggregations 聚合, 以及 suggestions 建议

该缓存是智能的,其 承诺保持 与 未缓存的查询 近乎实时的等效性。人话:该缓存查询保证准实时

每当分片刷新文档更改更新映射时,该缓存自动失效

如果缓存已满,将淘汰掉 最近最少使用的 缓存键

缓存是在每个节点级别管理的 其默认最大大小为 堆的 1% 可在 config/elasticsearch.yml 设置:

indices.requests.cache.size: 2% 指定 查询缓存最大为 堆的 2%

监视缓存情况:

索引统计:

GET /_stats/request_cache?human

节点统计:

GET /_nodes/stats/indices/request_cache?human

⚠️ 可提供的情报:heap size used : 21% free : 79% ……ing

4 . Field data cache 字段数据缓存 default 0%~unlimited heap size

Field data cache 官方文档地址

Doc Values and Fielddata

该缓存相当于 正排索引缓存 在最新版本中默认会使用 doc_value 。当你取消了 doc_value 或者指定使用 field data 时会将查询字段的正排索引加载到内存中。

加入缓存是一个昂贵的操作,因此默认会将该缓存保持在内存中,默认缓存大小是无限的,field data cache 将会一直增长直到触发断路器

该缓存大小可在配置文件中进行配置:

indices.fielddata.cache.size: 38% //堆节点大小的 38%
indices.fielddata.cache.size: 12GB // 绝对大小 12GB

如果进行设置,它应该小于 field data circuit breaker (字段数据断路器)的大小。

⚠️ 断路器将在后面介绍

⚠️ 可提供的情报:heap size used : 21% free : 79% ……ing

5 . Indexing pressure 索引压力 default 10% heap size

Indexing pressure 官方文档地址

将文档索引至 Elasticsearch 会占用 内存 及 CPU。

外部的索引操作会经历 3 个阶段: coordinating, primary, and replica。协调 ,主索引 和 副本。[Basic write model 基本写模型]

内存限制:默认为堆内存 10%

indexing_pressure.memory.limit: 10%

⚠️ 可提供的情报:heap size used : 31% free : 69% ……ing

6 . Segment Memory unlimited heap size

这里 segment memory 可以理解为 ES 对 Luence segment 的索引缓存,用于加快查询倒排索引的速度

以下引用网上最多的解释:

Segment 不是 file 吗?segment memory 又是什么?一个 segment 是一个完备的 lucene 倒排索引,而倒排索引是通过词典 (Term Dictionary) 到文档列表 (Postings List) 的映射关系,来做快速查询。 由于词典的 size 会很大,全部装载到 heap 里不现实,因此 Lucene 为词典做了一层前缀索引 (Term Index) ,这个索引在 Lucene 4.0 以后采用的数据结构是 FST (Finite State Transducer)。 这种数据结构占用空间很小,Lucene 打开索引的时候将其全量装载到内存中,加快磁盘上词典查询速度的同时减少随机磁盘访问次数。 所以 ES 的 data node 存储数据并非只是耗费磁盘空间,为了加速数据访问,每个 segment 都有会一些索引数据驻留在 heap 里。因此 segment 越多,瓜分掉的 heap 也越多,并且这部分 heap 是无法被GC掉的! 理解这点对于监控和管理集群容量很重要,当一个 node 的 segment memory 占用过多的时候,就需要考虑删除、归档数据,或者扩容了。

使用 cat api 查看每个节点 segment memory 占用大小

GET _cat/nodes?v&h=name,port,sm

7 . 查询聚合 unlimited heap size

最让人头疼的是查询聚合所造成的内存占用。这种占用根据查询的语句以及你的集群分布都有极大的相关性和不确定性。

所以务必在确定一个查询前,考虑好这个查询所带来的影响,以便对你的集群做出相应的调整。

这里就涉及到 索引建立优化查询语句优化。整体思路是尽可能的精简和只做必要的事儿。

bitset

相关优化本文不展开讨论。后续会考虑专门写一篇来聊聊。

优化聚合查询

三、我拿什么来防止 ES OOM ?Circuit breaker 断路器 !

神器!!官网兜底!断路器设置

Elasticsearch 包含多个断路器,这些断路器用于防止操作引起 OutOfMemoryError。每个断路器都限制指定了可以使用多少内存。此外,还有一个父级断路器,用于指定可在所有断路器上使用的内存总量。

1. Parent circuit breaker 总断路器

父断路器考虑实际内存使用量(true);仅考虑子断路器所保留的量(false)。默认为true。

indices.breaker.total.use_real_memory

总断链器限制。

如果 indices.breaker.total.use_real_memory: true 默认占 堆 70% 如果 indices.breaker.total.use_real_memory: false 默认占 堆 95%

indices.breaker.total.limit

2. Field data circuit breaker 字段数据断路器

Field data circuit breaker 将估算将 field 加载到 field data cache 所需的堆内存。如果加载该字段将导致缓存超过预定义的内存限制,则断路器将停止该操作并返回错误。

请求中断限制,默认为 JVM 堆的 60%。

indices.breaker.fielddata.limit

与所有字段数据估计值相乘以确定最终估计值的常数。默认为 1.03

indices.breaker.fielddata.overhead

3. Request circuit breaker 请求数据断路器

Request circuit breaker 将防止每个请求的数据结构超过一定数量的内存。(例如,用于在请求期间计算聚合的内存)

请求中断限制,默认为 JVM 堆的 60%

indices.breaker.request.limit

一个常数,所有请求估计值都将与该常数相乘以确定最终估计值。默认为 1

indices.breaker.request.overhead

4. In flight requests circuit breaker

In flight requests 可以理解为:

客户端已经发出了,但是还没收到服务端响应的请求

In flight requests circuit breaker 使 Elasticsearch 可以限制 当前所有活跃的传输请求 或 HTTP请求 在一个节点上的内存使用,以免超出节点上的特定内存量。内存使用情况取决于请求本身的内容长度。该断路器还认为,不仅需要内存来表示原始请求,而且还需要将其作为结构化对象,这由默认开销反映出来。

This circuit breaker also considers that memory is not only needed for representing the raw request but also as a structured object which is reflected by default overhead.

In flight requests circuit breaker 的限制,默认为 JVM 堆的 100%。这意味着它受到为父级断路器配置的限制的限制

network.breaker.inflight_requests.limit

一个常数,所有 in flight 的请求估计值都将与该常数相乘以确定最终估计值。默认为2

network.breaker.inflight_requests.overhead

5. Accounting requests circuit breaker

Accounting requests circuit breaker 允许 Elasticsearch 限制请求完成时未释放的内存。这其中包括Lucene segment memory 。

默认为JVM堆的100%。这意味着它受到为父级断路器配置的限制的限制

indices.breaker.accounting.limit

估计值系数,默认为 1

indices.breaker.accounting.overhead

6. Scrip compilation circuit breaker

Scrip compilation circuit breaker 与上述的基于内存的断路器略有不同,它限制了一段时间内的内联脚本编译的次数。

在给定上下文中允许在一定间隔内唯一动态脚本的数量限制。默认为 75/5m,表示每 5 分钟 75 个

script.context.$CONTEXT.max_compilations_rate

四、配置建议

最后给写配置建议吧,老生常谈了,此类文章网上一搜一大把,我这边就挑重点的提一下。不过多赘述了。

官网重要配置建议:建议看看

堆内存:大小和交换

玩过 G1 或者 CMS 的老玩家们应该对 JVM 的调优都有些经验了我再唠叨两句:

  1. 堆大小不要超过 32 G ,这里给 31 G 比较保险,因为垃圾收集器并不是一定达到 32G 阈值才关闭指针压缩,所以如果想给 就设置成 31 G 吧 (指针压缩这个点很重要一定要注意
  2. 8 G 以上建议用 G1 ES 默认使用 CMS
  3. JVM 内存设置不要超过物理机内存的 50%。为啥? Luene :我不要面子的?
  4. 集群配置和分片先不聊了,东西挺多后续开坑吧~

Tips、加餐 cat api 和 节点监视

官方提供的 cat api 和 节点监视的 api 建议了解下,下面给列出几个常用的:

更多:Nodes Info Api & Cat Apis


## cat api
GET _cat/aliases
GET _cat/indices?v
GET _cat/shards?v
GET _cat/nodes?v
GET _cat/fielddata?v
GET /_cat
GET _cat/nodes?help
GET _cat/nodes?v&h=name,port,hc,hm,rc,rm
GET _cat/nodes?v&h=name,port,cpu,fm,qcm
GET _cat/nodes?v&h=name,port,rcm,sm
GET _cat/nodes?v&h=name,port,siwm,svmm,sfbm
GET _cat/segments/hi_message?human
GET _cat/segments/hi_recent_chat
GET /_cat/ml/anomaly_detectors
GET /_cat/segments

## 节点监视 
GET /_stats/fielddata?human&fields=*
GET /_nodes/stats/indices/fielddata?human&level=indices&fields=*
GET /_nodes/stats/indices/fielddata?human&fields=*
GET /_stats/request_cache?human
GET /_nodes/stats/indices/request_cache?human
GET /_nodes/stats?human
GET /_nodes/stats/jvm?human
GET /_nodes/stats/os?human
GET /_nodes/stats/process?human
GET /_nodes/stats/indices/merge?human
GET /_nodes/stats/breaker?human

总结

总的来说 ES 的内存占用主要的点有:

1 . Node query cache 节点查询缓存 default 10% heap size 2 . Indexing buffer 索引缓冲区 default 10% heap size 3 . Shard request cache 分片请求缓存 default 1% heap size 4 . Field data cache 字段数据缓存 default 0%~unlimited heap size 5 . Indexing pressure 索引压力 default 10% heap size 6 . Segment Memory unlimited heap size 7 . 查询聚合 unlimited heap size

重要的一点,ES 查询时 一定要记得限制你查询文本的大小,避免 分词匹配后 导致大量 倒排索引 load cache 引发无限 full gc 以至集群打挂

同时 ES 官方也提供了 断路器这样的工具来防止 OOM 的发生。 在实际开发中,仍需要我们结合实际的查询场景进行分析,因地制宜。 理论结合实战,希望本篇文章对你的工作和实际生产有所帮助。

我是 dying 搁浅 我看了一场 98 小时的电影也没能等来你的点赞、关注、收藏,我想这不是你不够喜欢我,而是我看的电影不够长……