初めに
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 構成
必要な作業は、以下の5つになります。
- Liberty の HTTP セッション・キャッシュを構成する
- Hazelcast の構成ファイルを準備する
- Hazelcast の設定を Liberty の bootstrap.properties および jvm.options に を追加する
- イメージをビルドする(Hazelcast の jar と構成ファイルを組み込む)
- 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
に配置することになります。
<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)
- Open Liberty: HTTP Session Cache (httpSessionCache)
Hazelcast の構成ファイルを準備する
次に、Hazelcast の構成ファイル hazelcast-config.yaml
を準備します。
今回は、Kubernetes Auto Discovery の Kubernetes API モードを使用して、メンバーを検出するように Hazelcast を構成します。構成に必要な情報は、Hazelcast のドキュメント「Kubernetes Auto Discovery」に記載されていますが、指定する必要があるのは、Hazelcast がメンバーを検出するために利用するサービスの名前、ネームスペースなどになります。
利用した構成ファイル hazelcast-config.yaml
は以下の通りです。
ネームスペースは、デフォルトで Pod のものが利用されるので指定していませんが、Pod のサービス提供用のサービス hazelcast-embedded-ibm-w をそのまま流用しているので、Hazelcast のポートが利用されるようにポート番号を明示的に指定しています。
追加で、Hazelcast の Jet エンジンが利用されるように指定しています。詳細は、Hazelcast のドキュメント「Configuring the Jet Engine」を参照してください。
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
オプションで指定することもできます。)
hazelcast.jcache.provider.type=member
この設定が抜けていると、Hazelcast が Client として起動し、正しく機能しません。この際、messages.log
には、以下のようなメッセージが出力されます。
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」に記載されていますので、その内容を 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
に出力されます。
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 の警告メッセージが消えました。
--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 のイメージからコピーする
初めの2つの方法に関しては、Hazelcast のドキュメント「Installing Hazelcast Community Edition」に記載されていますので、そちらを参照してください。
ここでは、WebSphere Liberty のドキュメント「コンテナー内のアプリケーションのセッション・キャッシングのセットアップ」でも利用されている、3番目の方法を利用します。
この方法は非常に手軽で、Dockerfile に以下の1行を追加するだけになります。jar ファイルは、Liberty の HTTP セッション・キャッシュの構成で指定したパスに組み込む必要があります。
COPY --from=hazelcast/hazelcast:5.5.0 \
--chown=default:root /opt/hazelcast/lib/hazelcast-5.5.0.jar /opt/hazelcast/lib/
Hazelcast の構成ファイルの組み込みに関しては、記載するまでもないと思いますが、Dockerfile に以下の1行を追加するだけになります。
こちらも、Liberty の HTTP セッション・キャッシュの構成で指定したパスに組み込む必要があります。
COPY --chown=default:root hazelcast-config.yaml /opt/hazelcast/
Pod のサービス・アカウントに必要なロールを割り当てる
Kubernetes Auto Discovery の Kubernetes API モードを利用すると、Hazelcast は Kubernetes API を使用してメンバーを検出します。このため、Pod のサービス・アカウントに必要なロールを付与する必要があります。
必要な定義は、kubernetes-rbac.yaml として公開されています。
この kubernetes-rbac.yaml
には、クラスター・ロールとクラスター・ロール・バインディングの定義が含まれていますが、管理しやすいように、ここでは内容を以下の2つの yaml ファイルに分割して利用します。
- クラスター・ロール: hazelcast-cluster-role.yaml
- ロール・バインディング: hazelcast-role-binding-liberty-default.yaml
また、ロール・バインディングは、クラスター・レベルの ClusterRoleBinding ではなく、ネームスペース・レベルの RoleBinding として定義したいので、以下の様に修正しました。
Pod は、ネームスペース liberty で、サービス・アカウント default で起動しますので、ロール・バインディングの metadata
と subjects
は、これに合わせて追加・修正しています。
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
に出力されます。
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
でこれらを定義します。
尚、クラスター・ロールの定義は1回だけ実行すれば良いですが、ロールのバインドはネームスペース内のサービス・アカウント毎に行う必要があります。
# 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 がリストされていれば、まずは、正しく構成されていることが分かります。
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
が出力され、想定した構成が認識されていることが分かります。
[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
に記録されることがあります。
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」を参照してください。
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 は別の方法でメンバーを検出することもできます。
以下の2つの方法を試してみましたので、その結果を記載しておきます。
- マルチキャスト
- Kubernetes Auto Discovery の DNS Lookup モード
尚、Kubernetes API モードを利用する時は、サービス・アカウントにロールをバインドしましたが、これらの方法を利用する場合は、不要となります。
マルチキャストを使用したメンバー検出
この方法は、UDP のマルチキャストを使用してメンバーを検出する方法で、Open Liberty のドキュメント「Caching HTTP session data using JCache and Hazelcast」でも利用している方法です。
この方法は、マルチキャストが使用できない環境(無効化されている環境)では機能しません。例えば、OpenShift では、デフォルトではマルチキャストが無効になっているようです。(参照:「Enabling multicast for a project」)また、マルチキャストが利用できても、マルチキャストの到達範囲内のメンバーしか検出できません。尚、マルチキャストの使用に関しては、その他にも留意しなければならない点があるようです。
この方法の詳細は、Hazelcast の下記のドキュメントを参照してください。
マルチキャストを利用する場合は、以下のような hazelcast-config.yaml
を利用します。cluster-name
で指定されている名前が一致するものだけが、メンバーとして検出されることになります。ここでは、hazelcast-embedded としていますが、Pod (Deployment) の名前などにするのが一般的と思われます。
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 のポート番号を指定しています。
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 の名前です。
hazelcast:
jet:
enabled: true
network:
join:
multicast:
enabled: false
kubernetes:
enabled: true
service-dns: hazelcast-embedded-headless
このモードも手持ちの環境で試してみましたが、なぜか、メンバーを認識するまでに5分程度を要する結果となりました。原因は不明ですが、この状況が改善しなければ、利用するのが難しいように思われます。
以下は、2つの Pod を同時に起動した状態で、上記の動作を確認した際の、messages.log
の一部です。同時起動した2つの Pod で同じ状況(最初に自 Pod だけを検出し、5分後に他の Pod を検出する)が確認できました。
[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 で注入するなどの対応が必要になると思われます。制約や考慮点があるようですが、マルチキャストを利用するという方法も考えられます。