Java ヒープの OOM で Liberty を停止する

## はじめに
今回は、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 に情報が記載されていました。

- [WebSphere Liberty 導入・運用ガイド](https://community.ibm.com/community/user/wasdevops/viewdocument/websphere-liberty-1)
- [WebSphere Liberty ランタイム Tips 集](https://community.ibm.com/community/user/wasdevops/viewdocument/websphere-liberty-tips)

記載されている方法は、IBM Java または Semeru Runtime の `-Xdump:tool` オプションを利用して、OOM 発生時に `kill -9` コマンドで自分のプロセスを kill するというもので、Liberty の `jvm.options` ファイルに以下のような指定を追加すれば良いと記載されています。

```text:jvm.options
-Xdump:tool:events=systhrow,filter=java/lang/OutOfMemoryError,exec=kill -9 %pid
```

実際に試してみると、Java ヒープでの OOM 発生時に console.log に以下の出力があり、Liberty のプロセスが kill されました。（当然ながら、上記のオプションを指定しない場合は、Liberty のプロセスは停止しませんでした。）

```text:console.log への出力例
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]

```sh:
# 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 には、以下の様に指定することになります。

```text:jvm.options: オプション無しの kill
-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 には、以下の様に指定することになります。

```text:jvm.options: kill -11
-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 を参照してください。

- 参照：[Pod内のコンテナ間でプロセス名前空間を共有する](https://kubernetes.io/ja/docs/tasks/configure-pod-container/share-process-namespace/)

`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 コンテナ）のプロセスになります。プロセス名前空間を共有していない環境でも存在するプロセスですが、あまり目にすることは無いかと思います。

```sh:
# 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` が以下の様に定義されています。

```docker:Liberty のデフォルト定義 (Dockerfile)
(省略)
ENTRYPOINT ["/opt/ol/helpers/runtime/docker-server.sh"]
CMD ["/opt/ibm/wlp/bin/server", "run", "defaultServer"]
```

ここでは、Liberty 起動用のシェル `wlpStart.sh` を作成し、このシェルを実行するように `CMD` を書き換えます。Dockerfile の内容（一部）と `wlpStart.sh` の内容は以下の様にしました。

```docker:Dockerfile: ps コマンドのインストール、wlpStart.sh の組み込み、CMD の書き換え
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"]
```
[^1]: ps コマンドを実行するには、コードブロック「Dockerfile: ps コマンドのインストール、wlpStart.sh の組み込み、CMD の書き換え」に示した方法で、ps コマンドをイメージに追加する必要があります。

```sh:Liberty 起動用のシェル 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` では、以下のような処理をしています。
1. `server run` コマンドで Liberty を起動
1. 起動した Liberty の PID を取得
1. console.log の tail をバックエンドで起動
1. SIGTERM, SIGINT, SIGHUP の受信時に Liberty を停止ように設定
1. Liberty のプロセスが存在する間は待機


この方法で起動した Liberty コンテナに `kubectl exec` して `ps` コマンドで状況を確認すると、以下の様に、Liberty の PID が 1 以外になっていることが確認できます。
Liberty を `kill -9` で強制停止すると、`wlpStart.sh` が Liberty の停止を検出し、自身も停止するので、kubernetes によって、Liberty コンテナが再起動されます。

```sh:
# 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 を自動的に停止する方法をまとめてみました。
この情報が何かのお役に立てば幸いです。