记:Elasticsearch 的 告警机制 和 jvm 内存分配
Elasticsearch 的告警机制导致的更新失败问题
总结:由于测试环境资源占用比例过高导致 elasticsearch 触发保护机制,导致的更新失败的问题
Es 告警日志:
查看服务器资源使用情况:
此时 Es 所在的挂载磁盘已经占用了 95%,Es 默认的保护机制:
属性名 | 属性值(可配置为固定值) | 含义 | 保护措施 |
---|---|---|---|
cluster.routing.allocation.disk.watermark.low | 85% | 低警戒水位线 | 不会将新的分片分配给磁盘使用率达到低警戒水位线的节点,不会影响新创建的索引的主分片,特别是之前从来没有分配过得分片。 |
cluster.routing.allocation.disk.watermark.high | 90% | 高警戒水位线 | Elasticsearch 将尝试对磁盘使用率达到高警戒水位线的节点进行重新分片(将当前节点的数据转移到其他节点),此设置影响所有分片的分配,无论先前是否分配。 |
cluster.routing.allocation.disk.watermark.flood_stage | 95% | 洪泛警戒水位线 | Elasticsearch 将对每个索引强制执行只读索引块(index.blocks.read_only_allow_delete)。这是防止节点耗尽磁盘空间的最后手段。待磁盘空间充裕后,会自动释放只读索引块。 |
从告警日志中看到,此时磁盘空间已经到了 90%,达到了高警戒水位线。此时 Elasticsearch 禁止了写操作,导致系统更新 Elasticsearch 资源失败。
如果想紧急恢复,提供以下措施,可以调高告警水位线:
PUT _cluster/settings
{
"persistent":{
"cluster.routing.allocation.disk.watermark.low":"90%",
"cluster.routing.allocation.disk.watermark.high":"95%",
"cluster.routing.allocation.disk.watermark.flood_stage":"97%"
}
}
如想应急恢复索引功能,可以通过如下请求消除锁:
PUT */_settings?expand_wildcards=all
{
"index.blocks.read_only_allow_delete": null
}
在此扩展一下索引块的五种不同的状态:
状态 | 作用 |
---|---|
index.blocks.read_only | 设置为 true 可以使索引和元数据只读。 |
index.blocks.read_only_allwo_delete | 类似于 index.blocks.read_only ,但是允许删除索引释放磁盘资源。此时删除文档是不被允许的,仅可以删除索引。 |
index.blocks.read | 设置为 true 代表禁止对该索引进行读操作。 |
index.blocks.write | 设置为 true 代表禁止对该索引进行写操作,与read_only 不同,这个设置不影响元数据。 |
index.blocks.metadata | 设置为 true 代表禁用元数据的读写。 |
需要注意,删除索引、删除文档存在的区别:
删除内容 | 过程 |
---|---|
文档 | 在 Elasticsearch 中,删除和更新都是写操作,但是 Elasticsearch 中的文档是不可变的(immutable),因此不能被删除或者更改以展示其变更。 磁盘上的每个段都会有一个对应的 .del 文件。当删除请求发送后,文档并没有真正的被删除,而是在 .del 文件中被标记为删除。当该文档依然能够匹配查询,但是会在结果中被过滤掉。当段合并时,在 .del 文件中被比标记为删除的文档将不会被写入新段。 在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,在执行更新时,旧版本的文档在 .del 文档中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依旧能匹配查询,但是会在结果中被过滤掉。 |
索引 | 删除索引时很快的,基本上就是直接移除了和索引分片相关的文件。 |
可以看到删除文档时,并不是真正删除,只是逻辑删除。因此该文档所占用的空间并不会随着删除操作而马上释放,只有等下一次段合并的时候才会被真正的物理删除,这个时候才会释放磁盘空间。但是,在被查询到的文档标记删除的过程中同样需要占用磁盘空间,这个时候,删除操作并没有带来磁盘的资源释放,反而,磁盘使用率上升了,因此在删除过程中,需要保证集群磁盘有足够的余量,因为标记删除需要占用磁盘空间,如果磁盘空间不够,失败的概率还是很大的,严重时候,可能会造成集群崩溃和数据丢失。
所以 index.blocks.read_only_allwo_delete
禁用了删除文档操作,却可以删除索引。
回到最开始的问题,至此可以给出解决方案:
- 使用 api 暂时上调告警线,恢复正常的索引功能,避免小程序端业务功能瘫痪
- 整理服务器资源,清理日志文件和不需要用到的备份文件
- 回调告警水平线
以下为本次整理服务器资源过程中,使用了的 linux 命令:
命令 | 作用 | 说明 |
---|---|---|
su es | 切换用户 | Es 不允许使用 root 用户启动 |
ps -eo pid,user,args | 查看存活进程信息 | -e 所有存活进程,-o 过滤参数 |
free -m | 查看内存使用情况 | -m 表示使用 M 作为单位来显示内存 |
df 文件夹 | 查看文件挂载内存分区位置 | |
df -lh | 查看系统服务器的磁盘空间 | -l 本地文件,-h 以方便阅读的存储单位标识文件 |
du | 统计文件夹磁盘消耗 | |
rm -rf | 删除文件,不可恢复 | -r 递归 -f 静默删除 |
问题至此已经解决,以下记录本次问题带来的额外收货(问题定位错了,学到的额外知识)。
Elasticsearch 的 Jvm 内存分配为什么建议不大于 31 g(小于 32 g)?
首先我们可以查看一下 es 的启动参数:
[root@funfactory-share-01 ~]# ps -eo pid,user,args | grep "elasticsearch" | grep -v "grep"
29754 es /data/app/jdk/bin/java -Xshare:auto -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.allocator.numDirectArenas=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.locale.providers=SPI,JRE -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Djava.io.tmpdir=/tmp/elasticsearch-7490738416214607905 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m -Dlog4j2.formatMsgNoLookups=true -XX:MaxDirectMemorySize=1073741824 -Des.path.home=/data/app/elasticsearch -Des.path.conf=/data/app/elasticsearch/config -Des.distribution.flavor=default -Des.distribution.type=tar -Des.bundled_jdk=true -cp /data/app/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch -d
29797 es /data/app/elasticsearch/modules/x-pack-ml/platform/linux-x86_64/bin/controller
可以看到存在 jvm 启动参数 -Xms2g -Xmx2g
,我们都知道 es 是基于 java 开发的,不同的 es 版本实际上需要不同的 jdk 版本,这个在官网有版本对应。
项目上 test 环境使用的 es 是 7.10 版本的默认的对内存大小为 2g,早些年的版本为 1g。这个配置在 elasticsearch/config/jvm.options 文件中:
## JVM configuration
################################################################
## IMPORTANT: JVM heap size
################################################################
##
## You should always set the min and max JVM heap
## size to the same value. For example, to set
## the heap to 4 GB, set:
##
## -Xms4g
## -Xmx4g
##
## See https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html
## for more information
##
################################################################
# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space
-Xms2g
-Xmx2g
################################################################
## Expert settings
################################################################
##
## All settings below this section are considered
## expert settings. Don't tamper with them unless
## you understand what you are doing
##
################################################################
## GC configuration
8-13:-XX:+UseConcMarkSweepGC
8-13:-XX:CMSInitiatingOccupancyFraction=75
8-13:-XX:+UseCMSInitiatingOccupancyOnly
## G1GC Configuration
# NOTE: G1 GC is only supported on JDK version 10 or later
# to use G1GC, uncomment the next two lines and update the version on the
# following three lines to your version of the JDK
# 10-13:-XX:-UseConcMarkSweepGC
# 10-13:-XX:-UseCMSInitiatingOccupancyOnly
14-:-XX:+UseG1GC
14-:-XX:G1ReservePercent=25
14-:-XX:InitiatingHeapOccupancyPercent=30
## JVM temporary directory
-Djava.io.tmpdir=${ES_TMPDIR}
## heap dumps
# generate a heap dump when an allocation from the Java heap fails
# heap dumps are created in the working directory of the JVM
-XX:+HeapDumpOnOutOfMemoryError
# specify an alternative path for heap dumps; ensure the directory exists and
# has sufficient space
-XX:HeapDumpPath=data
# specify an alternative path for JVM fatal error logs
-XX:ErrorFile=logs/hs_err_pid%p.log
## JDK 8 GC logging
8:-XX:+PrintGCDetails
8:-XX:+PrintGCDateStamps
8:-XX:+PrintTenuringDistribution
8:-XX:+PrintGCApplicationStoppedTime
8:-Xloggc:logs/gc.log
8:-XX:+UseGCLogFileRotation
8:-XX:NumberOfGCLogFiles=32
8:-XX:GCLogFileSize=64m
# JDK 9+ GC logging
9-:-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m
# BEGIN Log4j
-Dlog4j2.formatMsgNoLookups=true
# END Log4j
这里提供了两种方式修改 elasticsearch 的堆内存。最简单的方法是指定一个环境变量,在服务进程启动的时候指定这个变量的值,设置对应的堆大小,如:
export ES_HEAP_SIZE = 10g
也可以通过命令行参数的形式,在程序启动的时候把内存大小传递给它:
./bin/elasticsearch -Xmx10g -Xms10g
这里需要注意,确保堆内存最大值 Xmx 和最小值 Xms 的大小是相同的,防止程序在运行时改变堆内存的大小,这个是一个很耗资源的过程。
相信大家都听过一个说法:Elasticsearch 的 Jvm 内存分配为什么建议不大于 31 g。那么,为什么?
事实上,JVM 在内存小于 32 GB 的时候会使用一个内存对象指针压缩技术。
在 java 中,所有的对象都分配在堆上,并通过一个指针进行引用。普通对象指针(oop)指向这些对象,通常大小为 CPU 的字长:32 位或 64 位,这取决于处理器,指针引用的就是这个 OOP 值的字节位置。
对于 32 位的系统,意味着堆内存大小最大为 4 GB。对应 64 位的系统,可以使用更大的内存,但是 64 位的指针,意味着很大的浪费,因为你的指针本身就已经更大了。更大的指针在主内存和各级缓存之间移动数据时,会占用更多的带宽。
Java 使用一个叫作 内存指针压缩(compressed oops)的技术来解决这个问题。 它的指针不再表示对象在内存中的精确位置,而是表示 偏移量 。这意味着 32 位的指针可以引用 40 亿个 对象 , 而不是 40 亿个字节。最终, 也就是说堆内存增长到 32 GB 的物理内存,也可以用 32 位的指针表示。
一旦你越过那个神奇的 ~32 GB 的边界,指针就会切回普通对象的指针。 每个对象的指针都变长了,就会使用更多的 CPU 内存带宽,也就是说你实际上失去了更多的内存。事实上,当内存到达 40–50 GB 的时候,有效内存才相当于使用内存对象指针压缩技术时候的 32 GB 内存。
即便你有足够的内存,也尽量不要 超过 32 GB。因为它浪费了内存,降低了 CPU 的性能,还要让 GC 应对大内存。