Liberty の HTTP セッション・キャッシュを Hazelcast の Embedded 構成で利用する

# 初めに
WebSphere Liberty の HTTP セッション・キャッシュを使用すると、HTTP セッション・データを JCache プロバイダーに保存することができます。これにより、Liberty 間で HTTP セッション・データを共有し、HTTP セッション・データの可用性を確保することができます。
JCache プロバイダーとしては、Infinispan や Hazelcast などの JCache (JSR 107) 仕様に準拠したものが利用できます。
構成としては、Client-Server 構成や Embedded 構成(Peer-to-Peer 構成) が一般的になります。

* Client-Server 構成
  * JCache プロバイダーを専用のサーバーや Pod で稼働させ、Liberty から利用する形態
* Embedded 構成（Peer-to-Peer 構成）
  * JCache プロバイダーを Liberty と同じ JVM 内で 稼働させ、複数の Liberty サーバー間で相互にデータを保持する形態

今回は、Kubernetes 環境で稼働する Liberty の Pod で、JCache プロバイダーとして **Hazelcast を使用**し、**Embedded 構成**で利用してみます。

# Hazelcast の Embedded 構成
必要な作業は、以下の５つになります。
1. Liberty の HTTP セッション・キャッシュを構成する
1. Hazelcast の構成ファイルを準備する
1. Hazelcast の設定を Liberty の bootstrap.properties および jvm.options に を追加する
1. イメージをビルドする（Hazelcast の jar と構成ファイルを組み込む）
1. Pod のサービス・アカウントに必要なロールを割り当てる

ロールの割り当てが必要になるのは、Hazelcast の Kubernetes Auto Discovery を Kubernetes API モードで利用する場合のみです。
今回は、この Kubernetes API モードを利用してみますが、このモードを利用すると、Kubernetes のノード構成を加味した、データのキャッシングが可能になります。

## Liberty の HTTP セッション・キャッシュを構成する
HTTP セッション・キャッシュを構成するには、`sessionCache-1.0` フィーチャーを組み込み、`<httpSessionCache>` タグに構成を記述します。`<httpSessionCache>` タグでは、JCache プロバイダーである、Hazelcast の jar ファイルと構成ファイルのパスを指定します。
今回は、以下のような定義を `server.xml` に追加しました。
Hazelcast の jar ファイルは、ここで指定した `/opt/hazelcast/lib/` ディレクトリー内に、構成ファイルは `/opt/hazelcast/hazelcast-config.yaml` に配置することになります。

```xml: HTTP セッション・キャッシュを構成
    <featureManager>
        <feature>sessionCache-1.0</feature>
    </featureManager>
    <httpSessionCache libraryRef="JCacheLib" uri="file:/opt/hazelcast/hazelcast-config.yaml"/>
    <library id="JCacheLib">
        <fileset dir="/opt/hazelcast/lib/" includes="*.jar"/>
    </library>
```

`<httpSessionCache>` タグの `writeFrequency` 属性や `writeInterval` 属性などを利用して、HTTP セッション・データの更新頻度などを指定できます。詳細は、WebSphere Liberty や Open Liberty のドキュメントを参照してください。
* [WebSphere Liberty: HTTP セッション・キャッシュ (httpSessionCache)](https://www.ibm.com/docs/ja/was-liberty/base?topic=configuration-httpsessioncache)
* [Open Liberty: HTTP Session Cache (httpSessionCache)](https://openliberty.io/docs/latest/reference/config/httpSessionCache.html)

## Hazelcast の構成ファイルを準備する
次に、Hazelcast の構成ファイル `hazelcast-config.yaml` を準備します。
今回は、Kubernetes Auto Discovery の Kubernetes API モードを使用して、メンバーを検出するように Hazelcast を構成します。構成に必要な情報は、Hazelcast のドキュメント「[Kubernetes Auto Discovery](https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery)」に記載されていますが、指定する必要があるのは、Hazelcast がメンバーを検出するために利用するサービスの名前、ネームスペースなどになります。

利用した構成ファイル `hazelcast-config.yaml` は以下の通りです。
ネームスペースは、デフォルトで Pod のものが利用されるので指定していませんが、Pod のサービス提供用のサービス *hazelcast-embedded-ibm-w* をそのまま流用しているので、Hazelcast のポートが利用されるようにポート番号を明示的に指定しています。
追加で、Hazelcast の Jet エンジンが利用されるように指定しています。詳細は、Hazelcast のドキュメント「[Configuring the Jet Engine](https://docs.hazelcast.com/hazelcast/5.5/configuration/jet-configuration)」を参照してください。

```yaml: hazelcast-config.yaml
hazelcast:
  jet:
    enabled: true
  network:
    join:
      multicast:
        enabled: false
      kubernetes:
        enabled: true
        service-name: hazelcast-embedded-ibm-w
        service-port: 5701
```

## bootstrap.properties および jvm.options に設定を追加する
Hazelcast を Embedded 構成で利用する場合は、`bootstrap.properties` に以下の指定を追加する必要があります。（`jvm.options` 内に `-D` オプションで指定することもできます。）

```sh:bootstrap.properties
hazelcast.jcache.provider.type=member
```

この設定が抜けていると、Hazelcast が Client として起動し、正しく機能しません。この際、`messages.log` には、以下のようなメッセージが出力されます。

```text: Hazelcast が Client として起動してしまった場合
file:/opt/hazelcast/hazelcast-config.yaml [dev] [5.5.0] HazelcastClient 5.5.0 (20240725) is STARTING
file:/opt/hazelcast/hazelcast-config.yaml [dev] [5.5.0] HazelcastClient 5.5.0 (20240725) is STARTED
```

Java 17 以降の Java Platform Module System が強制される環境では、以下のオプションを指定するように、Hazelcast のドキュメント「[Installing Hazelcast Community Edition](https://docs.hazelcast.com/hazelcast/5.5/getting-started/install-hazelcast)」に記載されていますので、その内容を `jvm.options` に念のため追加しておきます。（指定しなくても、問題なく稼働するように見えましたが...）

```sh:jvm.options
--add-modules java.se
--add-exports java.base/jdk.internal.ref=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.management/sun.management=ALL-UNNAMED
--add-opens jdk.management/com.ibm.lang.management.internal=ALL-UNNAMED
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
```

尚、このオプションを指定しないと、以下のような Hazelcast の警告メッセージが `messages.log` に出力されます。

```text: JVM オプションが指定されていない場合の警告メッセージ
Hazelcast is starting in a Java modular environment (Java 9 and newer) but without proper access to required Java packages. Use additional Java arguments to provide Hazelcast access to Java internal API. The internal API access is used to get the best performance results. Arguments to be used:
 --add-modules java.se --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-exports jdk.management/com.ibm.lang.management.internal=ALL-UNNAMED
```

但し、私の環境（使用したイメージは、`ibmcom/websphere-liberty:24.0.0.6-full-java17-openj9-ubi` ）では、上記のオプションを指定しても、警告メッセージが消えませんでした。調査したところ、JVM がオプションを無視していることが分かったので、試しに以下の様に指定したところ、オプションが正しく認識され、Hazelcast の警告メッセージが消えました。

```sh:jvm.options
--add-modules=java.se
--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.nio=ALL-UNNAMED
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
--add-opens=java.management/sun.management=ALL-UNNAMED
--add-opens=jdk.management/com.ibm.lang.management.internal=ALL-UNNAMED
--add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
```

## イメージをビルドする
Hazelcast の jar と構成ファイルをイメージに組み込みます。(Liberty の `server.xml`, `bootstrap.properties`, `jvm.options` などは既にイメージに組み込むように Dockerfile が準備できている前提で、特に手順などは記載していません。)

Hazelcast の jar ファイルを入手する方法は幾つかあります。

* maven を利用して jar を入手する
* 手作業でダウンロードしておく
* Hazelcast のイメージからコピーする

初めの２つの方法に関しては、Hazelcast のドキュメント「[Installing Hazelcast Community Edition](https://docs.hazelcast.com/hazelcast/5.5/getting-started/install-hazelcast)」に記載されていますので、そちらを参照してください。

ここでは、WebSphere Liberty のドキュメント「[コンテナー内のアプリケーションのセッション・キャッシングのセットアップ](https://www.ibm.com/docs/ja/was-liberty/base?topic=images-setting-up-session-caching)」でも利用されている、３番目の方法を利用します。
この方法は非常に手軽で、Dockerfile に以下の１行を追加するだけになります。jar ファイルは、Liberty の HTTP セッション・キャッシュの構成で指定したパスに組み込む必要があります。

```Dockerfile: jar を組み込む
COPY --from=hazelcast/hazelcast:5.5.0 \
     --chown=default:root /opt/hazelcast/lib/hazelcast-5.5.0.jar /opt/hazelcast/lib/
```

Hazelcast の構成ファイルの組み込みに関しては、記載するまでもないと思いますが、Dockerfile に以下の１行を追加するだけになります。
こちらも、Liberty の HTTP セッション・キャッシュの構成で指定したパスに組み込む必要があります。

```Dockerfile: 構成ファイルを組み込む
COPY --chown=default:root hazelcast-config.yaml /opt/hazelcast/
```

## Pod のサービス・アカウントに必要なロールを割り当てる
Kubernetes Auto Discovery の Kubernetes API モードを利用すると、Hazelcast は Kubernetes API を使用してメンバーを検出します。このため、Pod のサービス・アカウントに必要なロールを付与する必要があります。

必要な定義は、[kubernetes-rbac.yaml](https://raw.githubusercontent.com/hazelcast/hazelcast/master/kubernetes-rbac.yaml) として公開されています。
この `kubernetes-rbac.yaml` には、クラスター・ロールとクラスター・ロール・バインディングの定義が含まれていますが、管理しやすいように、ここでは内容を以下の２つの yaml ファイルに分割して利用します。

* クラスター・ロール： hazelcast-cluster-role.yaml
* ロール・バインディング： hazelcast-role-binding-liberty-default.yaml

また、ロール・バインディングは、クラスター・レベルの ClusterRoleBinding ではなく、ネームスペース・レベルの RoleBinding として定義したいので、以下の様に修正しました。
Pod は、ネームスペース liberty で、サービス・アカウント default で起動しますので、ロール・バインディングの `metadata` と `subjects` は、これに合わせて追加・修正しています。

```yaml: ロール・バインディング： hazelcast-role-binding-liberty-default.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: hazelcast-role-binding-default
  namespace: liberty
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: hazelcast-cluster-role
subjects:
  - kind: ServiceAccount
    name: default
    namespace: liberty
```

尚、必要なロールが割り当てられていないと、以下のような Hazelcast の警告メッセージが `messages.log` に出力されます。

``` text: ロールが割り当てられていない場合の警告メッセージ
Kubernetes API access is forbidden! Starting standalone.
To use Hazelcast Kubernetes discovery, configure the required RBAC.
For 'default' service account in 'default' namespace execute `kubectl apply -f https://raw.githubusercontent.com/hazelcast/hazelcast/master/kubernetes-rbac.yaml` If you want to use a different service account and a different namespace, you can update the mentioned rbac.yaml file accordingly and use it.
Error Kubernetes API Cause details: (以下省略)
```

説明が長くなりましたが、クラスター・ロールとロール・バインディングを定義するための yaml ファイルが準備できましたので、`kubectl` でこれらを定義します。
尚、クラスター・ロールの定義は１回だけ実行すれば良いですが、ロールのバインドはネームスペース内のサービス・アカウント毎に行う必要があります。

```text: クラスター・ロールの定義とロールのバインド
# kubectl apply -f hazelcast-cluster-role.yaml
clusterrole.rbac.authorization.k8s.io/hazelcast-cluster-role created

# kubectl get clusterrole hazelcast-cluster-role
NAME                     CREATED AT
hazelcast-cluster-role   2024-08-13T02:49:53Z

# kubectl apply -f hazelcast-role-binding-liberty-default.yaml
rolebinding.rbac.authorization.k8s.io/hazelcast-role-binding-default created

# kubectl get rolebinding hazelcast-role-binding-default -n liberty
NAME                             ROLE                                 AGE
hazelcast-role-binding-default   ClusterRole/hazelcast-cluster-role   5s
```

## Pod を起動して動作を確認
Pod (Deployment) を起動して、正しく構成されているか確認します。
Pod 起動後に `messages.log` に以下のような出力があり、起動した全ての Pod がリストされていれば、まずは、正しく構成されていることが分かります。

```text: メンバーの検出
Members {size:2, ver:2} [
        Member [10.36.0.2]:5701 - 300267cf-6ede-4dbe-b006-23ab972ec736 this
        Member [10.42.0.2]:5701 - e18786ae-eadd-44ba-9db3-565105d23144
]
```

Hazelcast の構成ファイルで指定した内容が正しく反映されているかも、`messages.log` に出力される Kubernetes Discovery properties の内容から確認できます。
今回の場合であれば、Kubernetes Discovery properties に `service-name: hazelcast-embedded-ibm-w`、`service-port: 5701` および `namespace: liberty` が出力され、想定した構成が認識されていることが分かります。

```text: Kubernetes Discovery properties
[10.36.0.2]:5701 [dev] [5.5.0] Kubernetes Discovery properties: { service-dns: null, service-dns-timeout: 5, service-name: hazelcast-embedded-ibm-w, service-port: 5701, service-label: null, service-label-value: true, namespace: liberty, pod-label: null, pod-label-value: null, resolve-not-ready-addresses: true, expose-externally-mode: AUTO, use-node-name-as-external-address: false, service-per-pod-label: null, service-per-pod-label-value: null, kubernetes-api-retries: 3, kubernetes-master: https://kubernetes.default.svc
```

確認結果等は省略しますが、アクセスしている Pod を強制停止したり、Pod 数の増減により割り振り先の Pod が変わったりしても、HTTP セッション・データが継続的に利用できることが確認できます。


尚、Hazelcast には稼働状況を報告する機能（Phone Home 機能）が組み込まれており、Hazelcast のサイトにアクセスします。このため、以下のようなエラー・メッセージが `messages.log` に記録されることがあります。

```text: SSL HANDSHAKE FAILURE
SSL HANDSHAKE FAILURE:  A signer with SubjectDN [CN=hazelcast.com] was sent from the host [phonehome.hazelcast.com:443]. (以下省略)
```

# Kubernetes API モードの Node Aware 機能
Kubernetes API モードを利用すると、Kubernetes のノード構成を加味した、データのキャッシングが可能になります。つまり、HTTP セッション・データは、別のノードで稼働する Pod へ保存されるようになり、ノード障害が発生しても HTTP セッション・データが維持できるようになります。
尚、この機能は、Pod がノードに均一に配置されていることを想定したものとなります。（Pod の配置が不均一な場合、キャッシュ・データの量に偏りが発生することになります。）

この機能を利用するには、以下の様に `hazelcast-config.yaml` に必要な定義を追加する必要があります。詳細は、Hazelcast のドキュメント「[Partition Group Configuration](https://docs.hazelcast.com/hazelcast/5.5/clusters/partition-group-configuration)」を参照してください。

```yaml: hazelcast-config.yaml - Node Aware 機能を有効化
hazelcast:
  jet:
    enabled: true
  network:
    join:
      multicast:
        enabled: false
      kubernetes:
        enabled: true
        service-name: hazelcast-embedded-ibm-w
        service-port: 5701
  partition-group:
    enabled: true
    group-type: NODE_AWARE
```

別ノードで稼働している Pod にキャッシュされているか確認したいところですが、Hazelcast のマネージメント・センターでノード毎にパーティション・グループが分割されている状況は見られたものの、別ノード上の Pod にキャッシュされているかまでは確認できませんでした。

# その他のモード
ここまでは、Kubernetes Auto Discovery の Kubernetes API モードを利用してきましたが、Hazelcast は別の方法でメンバーを検出することもできます。

以下の２つの方法を試してみましたので、その結果を記載しておきます。
* マルチキャスト
* Kubernetes Auto Discovery の DNS Lookup モード

尚、Kubernetes API モードを利用する時は、サービス・アカウントにロールをバインドしましたが、これらの方法を利用する場合は、不要となります。

## マルチキャストを使用したメンバー検出
この方法は、UDP のマルチキャストを使用してメンバーを検出する方法で、Open Liberty のドキュメント「[Caching HTTP session data using JCache and Hazelcast](https://openliberty.io/guides/sessions.html)」でも利用している方法です。

この方法は、マルチキャストが使用できない環境（無効化されている環境）では機能しません。例えば、OpenShift では、デフォルトではマルチキャストが無効になっているようです。（参照：「[Enabling multicast for a project](https://docs.openshift.com/container-platform/4.16/networking/ovn_kubernetes_network_provider/enabling-multicast.html)」）また、マルチキャストが利用できても、マルチキャストの到達範囲内のメンバーしか検出できません。尚、マルチキャストの使用に関しては、その他にも留意しなければならない点があるようです。

この方法の詳細は、Hazelcast の下記のドキュメントを参照してください。

* [Discovery Mechanisms - Multicast](https://docs.hazelcast.com/hazelcast/5.5/clusters/discovery-mechanisms#multicast)
* [Discovering Members by Multicast](https://docs.hazelcast.com/hazelcast/5.5/clusters/discovering-by-multicast)
* [Network Configurations](https://docs.hazelcast.com/hazelcast/5.5/clusters/network-configuration)

マルチキャストを利用する場合は、以下のような `hazelcast-config.yaml` を利用します。`cluster-name` で指定されている名前が一致するものだけが、メンバーとして検出されることになります。ここでは、hazelcast-embedded としていますが、Pod (Deployment) の名前などにするのが一般的と思われます。

```yaml: hazelcast-config.yaml - マルチキャスト用
hazelcast:
  cluster-name: hazelcast-embedded
  jet:
    enabled: true
  network:
    join:
      multicast:
        enabled: true
```

手持ちの素の Kubernetes で試してみましたが、問題なくメンバーが検出でき、メンバーが増減した際の検出もスピーディーに行われているようでした。

## DNS Lookup モードを使用したメンバー検出
Kubernetes Auto Discovery の DNS Lookup モードは、Kubernetes の Headless Service を利用してメンバーの IP アドレスを取得することで、メンバーを検出する方法です。通常の Service であれば、DNS Lookup により Service のクラスター IP が取得できますが、Headless Service では、DNS Lookup により Pod の IP アドレスのリストが取得できます。

このモードを利用するには、Headless Service が必要になるわけですが、今回利用した Headless Service の yaml は以下の様になります。`clusterIP: None` となっている部分が Headless Service 固有の部分で、残りの部分は通常の Service と変わりがありません。ポート番号は Hazelcast のポート番号を指定しています。

```yaml: hazelcast-embedded-headless.yaml
  apiVersion: v1
  kind: Service
  metadata:
    namespace: liberty
    name: hazelcast-embedded-headless
  spec:
    type: ClusterIP
    clusterIP: None
    ports:
    - name: hazelcast
      port: 5701
      protocol: TCP
      targetPort: 5701
    selector:
      app: hazelcast-embedded-ibm-w
    sessionAffinity: None
```

`hazelcast-config.yaml` の定義は以下のようになります。`service-dns` で指定しているのが、Headless Service の名前です。

```yaml: hazelcast-config.yaml - DNS Lookup モード用
hazelcast:
  jet:
    enabled: true
  network:
    join:
      multicast:
        enabled: false
      kubernetes:
        enabled: true
        service-dns: hazelcast-embedded-headless
```

このモードも手持ちの環境で試してみましたが、なぜか、メンバーを認識するまでに５分程度を要する結果となりました。原因は不明ですが、この状況が改善しなければ、利用するのが難しいように思われます。
以下は、２つの Pod を同時に起動した状態で、上記の動作を確認した際の、`messages.log` の一部です。同時起動した２つの Pod で同じ状況（最初に自 Pod だけを検出し、５分後に他の Pod を検出する）が確認できました。

```text: DNS Lookup モードでのメンバーの検出
[8/13/24, 3:50:05:192 UTC] 00000037 com.hazelcast.internal.cluster.ClusterService                I [10.42.0.2]:5701 [dev] [5.5.0]

Members {size:1, ver:1} [
        Member [10.42.0.2]:5701 - d779e1b2-6cb4-4598-bb54-f332b989c218 this
]
..........

[8/13/24, 3:55:00:175 UTC] 00000050 com.hazelcast.internal.cluster.ClusterService                I [10.42.0.2]:5701 [dev] [5.5.0]

Members {size:2, ver:2} [
        Member [10.36.0.2]:5701 - 9323316e-bb3c-4213-bbe6-1caf9042eb6b
        Member [10.42.0.2]:5701 - 0238775a-654d-4cad-8e65-f4b81e610a97 this
]
```

# 終わりに
Kubernetes 環境で稼働する Liberty の Pod で、HTTP セッション・キャッシュを Hazelcast の Embedded 構成で利用してみました。
主に、Kubernetes API モードでメンバーを検出する方法を記載しましたが、マルチキャストや DNS Lookup モードの利用に関しても触れてみました。

Kubernetes API モードを利用すると、Kubernetes のノード構成を加味した、データのキャッシングが利用可能になりますが、Kubernetes 環境でなければ Liberty が正しく起動しないというデメリットもあります。
Kubernetes に依存しないイメージをビルドしたいのであれば、HTTP セッション・キャッシュの定義を Kubernetes の ConfigMap で注入するなどの対応が必要になると思われます。制約や考慮点があるようですが、マルチキャストを利用するという方法も考えられます。