はじめに
今回は、Java ヒープで OOM (OutOfMemory) が発生した場合に、WebSphere Liberty を強制的に停止する方法をまとめてみました。
Java プロセスの OOM は、Java ヒープと Native ヒープ(メモリー)で発生することがあります。Java ヒープで OOM が発生しても、Java プロセスは稼働し続けますが、OOM の影響で Java ヒープ内のオブジェクトの情報に不整合が生じ、正常に動作しなくなる可能性があります。このため、Java ヒープで OOM が発生した場合は、WebSphere Liberty を速やかに停止し、再起動した方が良いと言えます。(Native ヒープで OOM が発生した場合は、Java プロセスは自動的に停止します。)
ここでは、Linux や AIX 上の IBM Java または Semeru Runtime 上で稼働する WebSphere Liberty を対象とします。
非コンテナ環境の場合
通常の OS 環境で稼働している場合に関しては、下記 URL に情報が記載されていました。
記載されている方法は、IBM Java または Semeru Runtime の -Xdump:tool
オプションを利用して、OOM 発生時に kill -9
コマンドで自分のプロセスを kill するというもので、Liberty の jvm.options
ファイルに以下のような指定を追加すれば良いと記載されています。
-Xdump:tool:events=systhrow,filter=java/lang/OutOfMemoryError,exec=kill -9 %pid
実際に試してみると、Java ヒープでの OOM 発生時に console.log に以下の出力があり、Liberty のプロセスが kill されました。(当然ながら、上記のオプションを指定しない場合は、Liberty のプロセスは停止しませんでした。)
JVMDUMP007I JVM は Tool ダンプ ('kill -9 17236240' を使用する) を要求しています
JVMDUMP011I Tool ダンプはプロセス 15008218 を作成しました
尚、本番環境等であれば、Liberty のプロセスが kill された後は、運用製品等のプロセス監視機能により Liberty の停止が検出され、回復処理が実施されるような設計・運用になっていると思われます。
コンテナ環境の場合
ここでは、Kubernetes 環境で Liberty を稼働させるケースを考えます。この環境では、デフォルトの状態では、Liberty のプロセスID (PID) は常に 1 になります。
コンテナ環境でも上記の方法を流用することになりますが、
コンテナ環境(Linux 環境)では、PID が 1 のプロセスに対して、kill -9
コマンドが機能しない点を考慮する必要があります。
試しに、Liberty のイメージ 24.0.0.12-full-java21-openj9-ubi-minimal
を使用したコンテナに kubectl exec
して、kill -9 1
を実行してみましたが、以下の通りで全く効き目がありませんでした。1
# kubectl exec my-liberty-0 -it -- bash
bash-5.1$ ps -ef
UID PID PPID C STIME TTY TIME CMD
default 1 0 48 04:42 ? 00:00:13 /opt/java/openjdk/bin/java -javaagent:/opt/ibm/wlp/bin/tools/ws-javaagent.jar -Djava.awt.headless=
default 130 0 0 04:43 pts/0 00:00:00 bash
default 136 130 0 04:43 pts/0 00:00:00 ps -ef
bash-5.1$ kill -9 1
bash-5.1$ echo $?
0
bash-5.1$ ps -ef
UID PID PPID C STIME TTY TIME CMD
default 1 0 36 04:42 ? 00:00:15 /opt/java/openjdk/bin/java -javaagent:/opt/ibm/wlp/bin/tools/ws-javaagent.jar -Djava.awt.headless=
default 130 0 0 04:43 pts/0 00:00:00 bash
default 137 130 0 04:43 pts/0 00:00:00 ps -ef
bash-5.1$
対応方法としては、次のようなものが考えられるかと思います。
Liberty を kill -15
コマンドで正常終了させる
Liberty は、SIGTERM(15)、SIGINT(2)、または、SIGHUP(1) のシグナルを受け取ると、正常終了を開始するようになっています。これは、Liberty を server run
コマンドでフロントエンド起動したときに、Ctrl+C キー(SIGINT)で Liberty を停止させる際の動作です。(コンテナ用の Liberty は server run
コマンドで起動されています。)
kill -15
、または、オプション無しの kill
を実行すると、SIGTERM(Termination シグナル)が送られます。
jvm.options には、以下の様に指定することになります。
-Xdump:tool:events=systhrow,filter=java/lang/OutOfMemoryError,exec=kill %pid
この方法の欠点は、強制終了(kill)ではなく、正常終了となる点です。OOM の影響や Java ヒープの枯渇により、Liberty の正常終了が完了せず、Liberty のプロセスが終了しない可能性があります。また、停止にも時間がかかります。
Liberty を kill -11
コマンドで異常終了させる
SIGSEGV(Segmentation Violation シグナル)を送ることで、Liberty を異常停止させることができます。通常、SIGSEGV(11) は、許可されていないメモリ領域に Native コード等がアクセスを試みた際に発生するものです。
この方法を使用した場合、必ず core ダンプも出力されるようになりますので、出力領域の容量に注意する必要があります。
jvm.options には、以下の様に指定することになります。
-Xdump:tool:events=systhrow,filter=java/lang/OutOfMemoryError,exec=kill -11 %pid
Kubernetes の shareProcessNamespace を有効にする
Pod 内のコンテナでプロセス名前空間を共有すると、Liberty の PID が 1 以外になり、kill -9
で Liberty を強制停止(kill)できます。
プロセス名前空間を共有するには、Pod の spec
に shareProcessNamespace: true
を指定します。尚、プロセス名前空間の共有は、Kubenetes v1.17 から安定化した機能です。詳細は、以下の URL を参照してください。
shareProcessNamespace: true
を指定した Pod の Liberty コンテナに kubectl exec
して ps
コマンド1で状況を確認すると、Liberty が 1 以外の PID で起動されていることが確認できます。下に示した例の場合、Liberty の PID は 7 になっています。
この状態で、Liberty に対して kill -9
を実行すると、Liberty が強制終了します。終了した Liberty は、Kubernetes によって再起動されます。当然ながら、PID も変わります。
尚、PID が 1 のプロセスは、pause コンテナ(POD コンテナ)のプロセスになります。プロセス名前空間を共有していない環境でも存在するプロセスですが、あまり目にすることは無いかと思います。
# kubectl exec my-liberty-0 -it -- bash
bash-5.1$ ps -ef
UID PID PPID C STIME TTY TIME CMD
default 1 0 0 05:33 ? 00:00:00 /pause
default 7 0 45 05:33 ? 00:00:14 /opt/java/openjdk/bin/java -javaagent:/opt/ibm/wlp/bin/tools/ws-javaagent.jar -Djava.awt.headless=
default 136 0 0 05:34 pts/0 00:00:00 bash
default 142 136 0 05:34 pts/0 00:00:00 ps -ef
bash-5.1$ kill -9 7
bash-5.1$ command terminated with exit code 137
# kubectl exec my-liberty-0 -it -- bash
bash-5.1$ ps -ef
UID PID PPID C STIME TTY TIME CMD
default 1 0 0 05:33 ? 00:00:00 /pause
default 143 0 36 05:34 ? 00:00:14 /opt/java/openjdk/bin/java -javaagent:/opt/ibm/wlp/bin/tools/ws-javaagent.jar -Djava.awt.headless=
default 272 0 0 05:35 pts/0 00:00:00 bash
default 278 272 0 05:35 pts/0 00:00:00 ps -ef
bash-5.1$
プロセス名前空間を共有した際の影響はほとんど無いと思いますが、例えば、Liberty の PID が常に 1 であることを想定して実装されているシェル等には影響があるかもしれません。
また、基本的には、あまり利用されていない機能であると推測されますので、その点もほんの少し気になります。
Liberty の起動方法を変える
この方法は、自前の実装により Liberty の起動方法を変えることで、Liberty の PID が 1 にならないようにするものです。
WebSphere Liberty のイメージでは、ENTRYPOINT
と CMD
が以下の様に定義されています。
(省略)
ENTRYPOINT ["/opt/ol/helpers/runtime/docker-server.sh"]
CMD ["/opt/ibm/wlp/bin/server", "run", "defaultServer"]
ここでは、Liberty 起動用のシェル wlpStart.sh
を作成し、このシェルを実行するように CMD
を書き換えます。Dockerfile の内容(一部)と wlpStart.sh
の内容は以下の様にしました。
FROM icr.io/appcafe/websphere-liberty:24.0.0.12-full-java21-openj9-ubi-minimal
(省略)
USER root
RUN microdnf install -y procps
USER default
COPY --chown=1001:0 --chmod=755 wlpStart.sh /
CMD ["/wlpStart.sh"]
#!/bin/bash
startServer() {
echo "Starting Liberty..."
/opt/ibm/wlp/bin/server start defaultServer
rc=$?
if [ $rc = 0 ]; then
echo "Liberty started successfully."
else
echo "Failed to start Liberty."
fi
return $rc
}
stopServer() {
echo "Stop Liberty."
kill $1
}
# Starting Liberty
startServer
rc=$?
if [ $rc != 0 ]; then
exit $rc
fi
# Get PID of Liberty
pid=$(ps -e | grep java | awk '{print $1}')
echo "PID is $pid."
if [ "$pid" = "" ]; then
exit 1
fi
# Tail console.log
tail -f /logs/console.log &
# Setup Signal Traps
trap "stopServer $pid" SIGTERM
trap "stopServer $pid" SIGINT
trap "stopServer $pid" SIGHUP
# Sleep and Wait
while [ $(ps --no-headers -p ${pid} | wc -l) = 1 ]; do
sleep 5
done
exit 0
wlpStart.sh
では、以下のような処理をしています。
-
server run
コマンドで Liberty を起動 - 起動した Liberty の PID を取得
- console.log の tail をバックエンドで起動
- SIGTERM, SIGINT, SIGHUP の受信時に Liberty を停止ように設定
- Liberty のプロセスが存在する間は待機
この方法で起動した Liberty コンテナに kubectl exec
して ps
コマンドで状況を確認すると、以下の様に、Liberty の PID が 1 以外になっていることが確認できます。
Liberty を kill -9
で強制停止すると、wlpStart.sh
が Liberty の停止を検出し、自身も停止するので、kubernetes によって、Liberty コンテナが再起動されます。
# kubectl exec my-liberty-0 -it -- bash
bash-5.1$ ps -ef
UID PID PPID C STIME TTY TIME CMD
default 1 0 0 05:54 ? 00:00:00 /bin/bash /wlpStart.sh
default 39 1 22 05:54 ? 00:00:16 /opt/java/openjdk/bin/java -javaagent:/opt/ibm/wlp/bin/tools/ws-javaagent.jar -Djava.awt.headless=
default 116 0 0 05:54 pts/0 00:00:00 bash
default 170 1 0 05:54 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=tail /usr/bin/tail -f /logs/console.log
default 227 1 0 05:55 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 5
default 228 116 0 05:56 pts/0 00:00:00 ps -ef
bash-5.1$
この実装で概ね問題ないことは確認しましたが、自作部分が多くなるので、今回紹介した対応方法の中では一番お勧めできない方法かもしれません。
終わりに
今回は、Java ヒープで OOM (OutOfMemory) が発生した場合に、WebSphere Liberty を自動的に停止する方法をまとめてみました。
この情報が何かのお役に立てば幸いです。