一、背景
一套生ES集群,版本为7.12.1,近期频繁告警,频繁出现索引分片异常,索引状态异常,导致应用无法正常写入ES,另外,也经常出现节点掉问题。通过分析相关ES日志,显示和当前JAVA GC有关。当前ES曾经过升级,从ES 6升级到ES 7,在ES应用服务器上除了部署ES本身,还部署运行了其它应用进程。为了方便JAVA管理,ES和其它应用都使用了相同的JDK,版本为1.8.0_202。而ES 7 在启动时会提示建议使用 JDK 11及以后版本。
因为采用JDK 1.8,GC垃圾回收还采用的是CMS方式,如下所示:
[root@localhost ~]# java -XX:+PrintFlagsFinal -version | grep Use.*GC.*=.*true
bool UseAdaptiveSizeDecayMajorGCCost = true {product}
bool UseGCOverheadLimit = true {product}
bool UseMaximumCompactionOnSystemGC = true {product}
bool UseParallelGC := true {product}
bool UseParallelOldGC = true {product}
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
当前JDK 1.8的JAVA_HOME是配置在/etc/profile文件中,作为全局变量使用的,如下所示:
export JAVA_HOME=/usr/java/jdk1.8.0_221
export ES_JAVA_HOME=/usr/java/jdk1.8.0_221
export PATH=$ES_JAVA_HOME/bin:$PATH
export PATH=$JAVA_HOME/bin:$PATH
export JAVA8_HOME=/opt/lib/jdk1.8.0_202
CLASSPATH=/opt/lib/jdk1.8.0_202/lib
JDK_HOME=/opt/lib/jdk1.8.0_202
JAVA_HOME=/opt/lib/jdk1.8.0_202
PATH=/opt/lib/jdk1.8.0_202/bin:/opt/lib/jdk1.8.0_202/jre/bin:$PATH
export CLASSPATH JAVA_HOME JDK_HOME PATH
在和研发等相关人员分析沟通近期问题后,建议将ES采用自带的JDK,并将GC垃圾回收调整为 G1GC,并且对JDK的调整不能影响到当前ES服务器上的其它应用对原JDK 1.8版本的使用。
当前ES启动后信息如下:
二、测试过程
如何调整ES使用指定的JDK版本启动呢,我查询了相关资料,显示可以修改/bin/elasticsearch文件,在该文件中加入指定的JAVA_HOME,于是按照相关文档,我在测试环境上进行了验证,测试环境版本和生产版本相同。
为方便修改和管理,我将ES自带的JDK目录拷贝到了当前用户家目录下,如下所示:
cp -a /home/esuser/deploy/elasticsearch-7.12.1-9300/jdk /home/esuser/OpenJDK16
对 /home/esuser/deploy/elasticsearch-7.12.1-9300/bin/elasticsearch文件进行修改,修改后的内容如下:
#!/bin/bash
#配置指定的JDK
export ES_HOME=/home/esuser/OpenJDK16
export PATH=$ES_HOME/bin:$PATH
#添加对JDK的判断
if [ -x "$ES_HOME/bin/java" ]; then
JAVA="/home/esuser/jdk/bin/java"
else
JAVA=`which java`
fi
source "`dirname "$0"`"/elasticsearch-env
CHECK_KEYSTORE=truepwd
DAEMONIZE=false
for option in "$@"; do
case "$option" in
-h|--help|-V|--version)
CHECK_KEYSTORE=false
;;
-d|--daemonize)
DAEMONIZE=true
;;
esac
done
if [ -z "$ES_TMPDIR" ]; then
ES_TMPDIR=`"$JAVA" "$XSHARE" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.TempDirectory`
fi
unset KEYSTORE_PASSWORD
KEYSTORE_PASSWORD=
if [[ $CHECK_KEYSTORE = true ]] \
&& bin/elasticsearch-keystore has-passwd --silent
then
if ! read -s -r -p "Elasticsearch keystore password: " KEYSTORE_PASSWORD ; then
echo "Failed to read keystore password on console" 1>&2
exit 1
fi
fi
# 确保传递正确的两个参数到JvmOptionsParser
ES_JAVA_OPTS=`export ES_TMPDIR; "$JAVA" "$XSHARE" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.JvmOptionsParser "$ES_PATH_CONF" "$ES_HOME/plugins"`
# manual parsing to find out, if process should be detached
if [[ $DAEMONIZE = false ]]; then
exec \
"$JAVA" \
"$XSHARE" \
$ES_JAVA_OPTS \
-Des.path.home="$ES_HOME" \
-Des.path.conf="$ES_PATH_CONF" \
-Des.distribution.flavor="$ES_DISTRIBUTION_FLAVOR" \
-Des.distribution.type="$ES_DISTRIBUTION_TYPE" \
-Des.bundled_jdk="$ES_BUNDLED_JDK" \
-cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch \
"$@" <<<"$KEYSTORE_PASSWORD"
else
exec \
"$JAVA" \
"$XSHARE" \
$ES_JAVA_OPTS \
-Des.path.home="$ES_HOME" \
-Des.path.conf="$ES_PATH_CONF" \
-Des.distribution.flavor="$ES_DISTRIBUTION_FLAVOR" \
-Des.distribution.type="$ES_DISTRIBUTION_TYPE" \
-Des.bundled_jdk="$ES_BUNDLED_JDK" \
-cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch \
"$@" \
<<<"$KEYSTORE_PASSWORD" &
retval=$?
pid=$!
[ $retval -eq 0 ] || exit $retval
if [ ! -z "$ES_STARTUP_SLEEP_TIME" ]; then
sleep $ES_STARTUP_SLEEP_TIME
fi
if ! ps -p $pid > /dev/null ; then
exit 1
fi
exit 0
fi
exit $?
修改后,我在前台测试启动elasticsearch,elasticsearch启动后,我通过ps -ef|grep java查看了ES所使用的jdk,发现并未使用/home/esuser/jdk/bin/java,而是使用了 /home/esuser/deploy/elasticsearch-7.12.1-9300/jdk/bin/java,不符合我的要求,如下所示:
[esuser@xsky-node1 ~]$ ps -ef|grep java
esuser 8570 31437 99 16:10 pts/0 00:01:21 /home/esuser/deploy/elasticsearch-7.12.1-9300/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 -XX:+ShowCodeDetailsInExceptionMessages -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,COMPAT --add-opens=java.base/java.io=ALL-UNNAMED -Xms8g -Xmx8g -XX:+UseG1GC -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 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/tmp/elasticsearch-1427292610172092269 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m -Djava.locale.providers=COMPAT -XX:UseAVX=2 -XX:MaxDirectMemorySize=4294967296 -XX:InitiatingHeapOccupancyPercent=30 -XX:G1ReservePercent=25 -Des.path.home=/home/esuser/deploy/elasticsearch-7.12.1-9300 -Des.path.conf=/home/esuser/deploy/elasticsearch-7.12.1-9300/config -Des.distribution.flavor=default -Des.distribution.type=tar -Des.bundled_jdk=true -cp /home/esuser/deploy/elasticsearch-7.12.1-9300/lib/* org.elasticsearch.bootstrap.Elasticsearch
根据脚本分析,发现是对环境变量 ES_HOME 的误用,在 Elasticsearch 中,ES_HOME 通常用来指代 Elasticsearch 的安装目录,而不是 JDK 的安装目录。应该使用 ES_JAVA_HOME 来设置 JDK 路径。这应该是导致 JDK 路径不正确的原因。
然后我对上面脚本进行了调整,使用 ES_JAVA_HOME 来设置 JDK 路径。
并确保所有引用 JDK 的地方都使用 ES_JAVA_HOME 而非 ES_HOME。
修改后的bin/elasticsearch脚本如下:
#!/bin/bash
#配置指定的JDK
export ES_JAVA_HOME=/home/esuser/OpenJDK16
export PATH=$ES_JAVA_HOME/bin:$PATH
#添加对JDK判断
if [ -x "$ES_JAVA_HOME/bin/java" ]; then
JAVA="$ES_JAVA_HOME/bin/java"
else
JAVA=`which java`
fi
source "`dirname "$0"`"/elasticsearch-env
CHECK_KEYSTORE=true
DAEMONIZE=false
for option in "$@"; do
case "$option" in
-h|--help|-V|--version)
CHECK_KEYSTORE=false
;;
-d|--daemonize)
DAEMONIZE=true
;;
esac
done
if [ -z "$ES_TMPDIR" ]; then
ES_TMPDIR=`"$JAVA" "$XSHARE" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.TempDirectory`
fi
unset KEYSTORE_PASSWORD
KEYSTORE_PASSWORD=
if [[ $CHECK_KEYSTORE = true ]] \
&& bin/elasticsearch-keystore has-passwd --silent
then
if ! read -s -r -p "Elasticsearch keystore password: " KEYSTORE_PASSWORD ; then
echo "Failed to read keystore password on console" 1>&2
exit 1
fi
fi
# 确保传递正确的两个参数到JvmOptionsParser
ES_JAVA_OPTS=`export ES_TMPDIR; "$JAVA" "$XSHARE" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.JvmOptionsParser "$ES_PATH_CONF" "$ES_HOME/plugins"`
# manual parsing to find out, if process should be detached
if [[ $DAEMONIZE = false ]]; then
exec \
"$JAVA" \
"$XSHARE" \
$ES_JAVA_OPTS \
-Des.path.home="$ES_HOME" \
-Des.path.conf="$ES_PATH_CONF" \
-Des.distribution.flavor="$ES_DISTRIBUTION_FLAVOR" \
-Des.distribution.type="$ES_DISTRIBUTION_TYPE" \
-Des.bundled_jdk="$ES_BUNDLED_JDK" \
-cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch \
"$@" <<<"$KEYSTORE_PASSWORD"
else
exec \
"$JAVA" \
"$XSHARE" \
$ES_JAVA_OPTS \
-Des.path.home="$ES_HOME" \
-Des.path.conf="$ES_PATH_CONF" \
-Des.distribution.flavor="$ES_DISTRIBUTION_FLAVOR" \
-Des.distribution.type="$ES_DISTRIBUTION_TYPE" \
-Des.bundled_jdk="$ES_BUNDLED_JDK" \
-cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch \
"$@" \
<<<"$KEYSTORE_PASSWORD" &
retval=$?
pid=$!
[ $retval -eq 0 ] || exit $retval
if [ ! -z "$ES_STARTUP_SLEEP_TIME" ]; then
sleep $ES_STARTUP_SLEEP_TIME
fi
if ! ps -p $pid > /dev/null ; then
exit 1
fi
exit 0
fi
exit $?
修改上述脚本后,我再次前台启动elasticsearch验证测试,发现这次采用了我指定的JDK目录 /home/esuser/OpenJDK16,如下所示,红色箭头指向的是其中一个ES节点使用了指定的JDK目录,另一个节点使用了/etc/profile中配置的JAVA_HOME目录
在测试过程中,我对/home/esuser/deploy/elasticsearch-7.12.1-9300/config/下的GC参赛进行了调整,修改了如下参数:
# -XX:+UseConcMarkSweepGC
# -XX:CMSInitiatingOccupancyFraction=75
# -XX:+UseCMSInitiatingOccupancyOnly
将上述参数进行了注释,
然后添加 -XX:+UseG1GC
通过如下命令 /home/esuser/OpenJDK16/bin/java -XX:+PrintFlagsFinal -version | grep Use.*GC.*=.*true,查看到的信息如下:
[esuser@xsky-node1 deploy]$ /home/esuser/OpenJDK16/bin/java -XX:+PrintFlagsFinal -version | grep Use.*GC.*=.*true
bool UseAdaptiveSizeDecayMajorGCCost = true {product} {default}
bool UseDynamicNumberOfGCThreads = true {product} {default}
bool UseG1GC = true {product} {ergonomic}
bool UseGCOverheadLimit = true {product} {default}
bool UseMaximumCompactionOnSystemGC = true {product} {default}
openjdk version "16" 2021-03-16
OpenJDK Runtime Environment AdoptOpenJDK (build 16+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 16+36, mixed mode, sharing)
和修改前对比,显示已经使用了G1GC垃圾回收,接下来就是要在生产环境上进行验证测试。
{default}
openjdk version “16” 2021-03-16
OpenJDK Runtime Environment AdoptOpenJDK (build 16+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 16+36, mixed mode, sharing)
和修改前对比,显示已经使用了G1GC垃圾回收,接下来就是要在生产环境上进行验证测试。