話說在前面 #
這篇實作奠基於前篇文章規劃,會記錄下使用 Spring Session 和 Hazelcast 達到 Session 同步的流程紀錄。
部署方式 #
重提一下我選擇的是 Client/Server 的部署方式,Hazelcast 還有另外幾種部署方式,可以參考官方文件或網路上的介紹,這邊不再贅述。
測試目標 #
在這個階段我的測試目標為 Spring Application 接得通 Hazelcast,會較多著墨在設定試錯的過程,這篇我只起了一顆 Spring Application Pod,下一步才會是以 Deployment 來做測試。
為求方便與瞭解原理,Client 端測試既然都要以 Spring Boot 起的應用程式,配置就使用 Java API Configuration 方式;Server 端我整個發懶,官方包好的 image 也沒有再用 Dockerfile 再包一層,直接在起 Pod 的時候使用環境變數傳入所需的設定。
環境 #
- Java 8: Open JDK 1.8
- minikube: Starts with 4GB memory,我最初嘗試起 3 個 Hazelcast Pod 時,不開 4GB 非常卡,不確定起一個的時候再小一點是否就夠用
kubectl: K8S Client command line tool- Docker with minikube env(容後再述)
- Spring Boot: 2.7.9
- Hazelcast: Image version tag 1.5.1,為了配合 Spring Boot 2.7.9 依賴的 Hazelcast 版本
資料來源 #
參考資料是沒這麼多的,因為大部分教學文件不是 Hazelcast in K8S 就是 Hazelcast Embedded With Spring Session,而我們要做的事情剛好是兩邊都拿一點。
來源主要是官方的這幾個文件:
接著還有一個很重要的資料來源,是 Hazelcast Discovery Plugin for Kubernetes 這個官方已經宣布過時的 git repository,這邊很清楚的寫到服務發現的機制,幫助我很多。
而現在已經被整合到上面的 Hazelcast for Kubernetes。
測試準備 #
minikube start #
起好 minikube,我一口氣起 4GB:
minikube start --memory=4096
Docker with minikube env #
這個步驟主要是讓我們的實驗環境排除「你的 localhost 不是你的 localhost」的狀況:
我們預備的應用程式會先容器化,接著以 Pod 的形式起在 minikube 的 K8S 叢集當中,此時 K8S 的 Node 完全是另外一台主機,並不是我們的實體本機,所以它會拉不到實體本機 localhost 的 image list,必須在 build image 之前先將 Docker 切到 minikube 的環境中。
其實非常簡單,在 terminal 當中下以下指令就可以了:
eval $(minikube docker-env)
將著在同個 terminal session 就可以安心起 Pod,切換回來則使用 minikube docker-env --unset。
另外還需注意:
- 此指令只 for Mac,Windows 需要使用 Invoke-Expression 的方式
- 每個 terminal session 若要讓 Docker 切到 minikube 的環境,都必須要下這行指令,它並沒有擴及全域
- 在 minikube 嘗試起我們自己所包裝的應用程式 Pod 時,需要加上
--image-pull-policy=Never 的設定,不然預設會從 Dockerhub 試圖拉 image 下來
開始動手做 #
準備 Spring Boot Application (Client) #
Dependencies #
我的目標是使用 Spring Session,並且確定 Session 真的有灌到 Hazelcast 當中,因此除了 Spring Boot 基本的依賴以外,還需要依賴這些包(以下以 Maven 管理 pom.xml 為例):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-hazelcast</artifactId>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>${hazelcast.version}</version>
</dependency>
Spring Boot 及其對應的 Hazelcast 版本如前述。
Controller #
為了測試 Session 的正常性,準備了一隻 Controller:
package com.cherry.hazelcast.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("session")
public class SessionController {
@Autowired
private HttpServletRequest request;
@GetMapping("/getSessionIfExist")
public String getSessionIfExist() {
HttpSession session = request.getSession(false);
return session == null ? null : session.getId();
}
@GetMapping("/getSession")
public String getSession() {
return request.getSession().getId();
}
}
Configuration #
這部分才是重頭戲!程式碼下方,會開始描述配置的緣由。
在此之前還是得先重述一下:
這邊我圖方便(可以點進去看 Java Method 都做了啥XD),使用 Java API Configuration 方式配置,這些內容完全是可以拉出去以 XML 或 YAML 來配置,在外部使用檔案配置讀入,應屬於比較好的方案,下回也會嘗試這麼做。
package com.cherry.hazelcast.configuration;
import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.config.SSLConfig;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.hazelcast.Hazelcast4SessionUpdateEntryProcessor;
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
@Configuration
@EnableHazelcastHttpSession
public class SessionConfiguration {
@Value("${hazelcast.service.namespace}")
private String serviceNamespace;
@Value("${hazelcast.service.name}")
private String serviceName;
@Value("${hazelcast.service.port}")
private int servicePort;
@Bean
@SpringSessionHazelcastInstance
public HazelcastInstance hazelcastInstance() {
ClientConfig config = new ClientConfig();
config.getNetworkConfig().setConnectionTimeout(50000);
config.getNetworkConfig().setSSLConfig(new SSLConfig().setEnabled(false));
config.getUserCodeDeploymentConfig()
.setEnabled(true)
.addClass(Session.class)
.addClass(MapSession.class)
.addClass(Hazelcast4SessionUpdateEntryProcessor.class);
config.getNetworkConfig().getKubernetesConfig()
.setEnabled(true)
.setProperty("namespace", serviceNamespace)
.setProperty("service-name", serviceName);
return HazelcastClient.newHazelcastClient(config);
}
}
服務發現 #
先講講我於 application.properties 自訂的各項參數,這邊需要跟 K8S 中部署的 Service 名稱、Namespace、Port 等等完全一致,不可以亂寫:
hazelcast.service.name=hz-hazelcast
hazelcast.service.namespace=default
hazelcast.service.port=5701
另外 config.setProperty("service-port", Integer.toString(servicePort)) 被我註解,是因為 Hazelcast 預設會監聽的就是 5701 Port,所以完全不設定也無所謂。
這邊是使用到 Kubernetes API 來做服務發現,而不是使用 DNS Lookup 的服務發現機制,我認為是比較直觀的。
超時與 SSL 設置(暫時擺爛) #
Connection Timeout 時間的設定是為了將時間拖長,因為在測試期間我發生過幾次 Pod IP 被加入黑名單的狀況,網路上查到一些資料說和這件事有關,果然一拖長就沒再發生過了。測試環境沒有 SSL,也是關掉了事。
驗證設置(暫時擺爛) #
應該有不少眼尖的人,發現到我並沒有設定連線驗證,也沒有指定 Hazelcast 叢集名稱,這還是跟我的目標有關,我想要先確定 Spring Session 和 Hazelcast Server in Kubernetes 是真的能連接上的,不想在此時被驗證干擾。
而且 Hazelcast Server 端也必須要設定開啟 Client 驗證並設定驗證方式如帳號密碼,才會同意 Client 做登入動作;不過就像我前面所說,現階段我還沒打算再包一次 Dockerfile,所以這部分先被我輕輕的略過了XD。
在此還是以註解方式奉上 Client 端 Java API Configuration 相關設定:
至於為什麼和叢集名稱有關呢?這就很有意思了!
我去看過 ClientConfig.class 這隻檔案,clusterName 居然是有預設值的:

一查證才發現,ClientConfig 中的預設 clusterName 為 dev,而在此狀況下,帳號密碼驗證會直接關閉。
為了關閉掉相關驗證,不設定叢集讓它維持 dev 的預設值,能讓我們連上 Hazelcast Server 的測試腳步更快速。
User Code Deployment 設置 #
這部分非常重要,且是我試到一半出錯才發現居然還要補設定這段。當時我遇到了 ClassNotFoundException 的錯誤,導致 Map 序列化失敗,神奇的是我的 Dependencies 完全沒少,然後這些解答也都沒什麼參考價值。
通過幾番查找我發現我需要的答案就在官方文件當中,Create a Hazelcast Instance Bean 這段說明了原因,我直接將官方的說明 copy and paste:
clientConfig.getUserCodeDeploymentConfig().setEnabled(true).addClass(Session.class)
.addClass(MapSession.class).addClass(Hazelcast4SessionUpdateEntryProcessor.class);
如果 spring-session package 不存在於 Hazelcast 成員的 classpath 中,那麼這些 class 需要在 Client 端部署。這是因為 Hazelcast 通過 entry processor 更新 Session 時需要這些 class。
哪些 class 呢?
Session.classMapSession.classHazelcast4SessionUpdateEntryProcessor.class
成員指的其實是 Server 端,因為那邊是單純的 Hazelcast jar image,並沒有對 Spring Session 依賴,吃不到這些 Class 是非常正常的呀!
接著我們就要來對 Server 端做一些設定,讓它反過來吃 Client 端設定的這些 Class,完整的起 Pod 指令會在下個步驟中說明。
其實這件事情超乎想像的簡單,只需要加上 HZ_USERCODEDEPLOYMENT_ENABLED=true 這個參數即可。
至於怎麼知道有這招的,其實是來自錯誤訊息,官方 git repo 的程式碼錯誤訊息也可一窺端倪。錯誤截圖放在下面:

(所以說這篇文章真的是試錯血淚史)
啟動 Hazelcast in K8S 提供服務 (Server) #
此時我們要準備啟動 Hazelcast Sever 囉!加上前段所說缺少的設定部分,一起來將 Pod 服務起起來吧!
這部分做完之後,我們才會回頭去包 Spring Application 的 image,並將它起在 K8S 當中。
先起 Hazelcast Server,再起 Spring Application,才能讓這個 Server 為我們的應用程式服務。
以下的內容主要是官方文件的步驟,目的應該也是為了讓使用者學習如何快速上手,所以設定上是蠻簡陋的,真的需要部署的話有些地方可以再將權限再小化。
預備好 Kubernetes API 所需要的 RBAC #
Server 端在這次的實驗中我們只會起一個 Pod,但其實他們是可以透過相同的 Group Name 組成一個叢集的。
至於它們是怎麼發現彼此的呢?以及 Client 端是如何發現它們的呢?就需要靠 Kubernetes API 來做這件事了。
首先,我們必須配置一組 RBAC 提供給 Hazelcast Cluster 使用:
kubectl apply -f https://raw.githubusercontent.com/hazelcast/hazelcast-kubernetes/master/rbac.yaml
再讓我們一窺這個 RBAC 設定的內容,我們準備好一個 ClusterRole 和一組與其綁定的 ClusterRoleBinding,綁定的 subject 是 default namespace 底下 default 的 Service Account。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: hazelcast-cluster-role
rules:
- apiGroups:
- ""
resources:
- endpoints
- pods
- nodes
- services
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: hazelcast-cluster-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: hazelcast-cluster-role
subjects:
- kind: ServiceAccount
name: default
namespace: default
起 Hazelcast Server,並加上必要參數 #
下面的指令當中 --env HZ_USERCODEDEPLOYMENT_ENABLED=true 代表前面提到的 Server 端需要開啟的 User Code Deployment 設定,並貼上 role=hazelcas 的 label。
我們先起一個 Pod 即可,當然你想起很多顆也是 OK 的,只要改改 Pod name 就可以了。
kubectl run hz-hazelcast-0 --image=hazelcast/hazelcast:5.1.5 -l "role=hazelcast" --env HZ_USERCODEDEPLOYMENT_ENABLED=true
透過 Service 暴露 Hazelcast Server Pod #
我們將服務以 ClusterIP 型態的 Service 暴露出去,--tcp=5701 則會讓這個 Service 的 Target port (Container port)、Service port 以 TCP 協定的 5701 為埠號。
接著再為這個 Service 選定帶有 role=hazelcast 標籤的 Pod 為負載平衡的目標,也就是我們剛剛起的 Hazelcast Pod。
kubectl create service clusterip hz-hazelcast --tcp=5701 -o yaml --dry-run=client | kubectl set selector --local -f - "role=hazelcast" -o yaml | kubectl create -f -
此時可能需要短暫等待 Hazelcast 起服務,我們接著來打包並啟動我們的 Spring Boot Application。
啟動 Spring Boot Application (Client) #
整個試驗階段最簡單的部分可能就在這裡,讓我們將剛剛準備好的 Spring Boot 應用程式容器化並啟動。
Dockerization 設定準備 #
為了辨識,我將應用程式開在 9999 port。
Dockerfile #
FROM openjdk:8-jre-alpine
EXPOSE 9999
ADD target/hazelcast-*.jar test-hazelcast.jar
ENTRYPOINT ["java", "-jar", "test-hazelcast.jar"]
Spring Boot Application Server Port #
Spring Boot application.properties 也要將 server port 設定在 9999。
server.port=9999
Maven Build #
mvn package -f pom.xml
Docker Build #
在 docker build 之前,別忘了 terminal 要先將 Docker 切到 minikube 的環境設定。
test-hazelcast 可以替換成任意的 image 名稱,要加上 tag version 也是可以的,我這邊是有點懶:
docker build -t test-hazelcast .
Kubernetes Run #
在 run pod 之前記得檢查一下 kubeconfig context 是不是在 minikube 的 cluster。
--image-pull-policy=Never 前面也有提到,也別忘記了:
kubectl run --image=test-hazelcast test-hazelcast --image-pull-policy=Never
部署完成,開始測試 #
接下來是 Controller 的測試部分,提供測法參考:
- 進入
getSessionIfExist API,會發現一片空白,因為不存在。 - 進入
getSession API,取得 session,Rest API 會給我們當前 session id。 - 重整或再進入
getSessionIfExist API,取得同一個 session id。 - 重整或再進入
getSession API,取得同一個 session id。 - 在不同瀏覽器重複以上流程,發生情況類似,但會取得一組新的 session id。
至此可以發現 session 機制正常,即 Spring Session 是可以串到 Hazelcast 去的,但有個很重要的部分還沒測試:這次只有起單顆 Pod,我們怎麼知道 Pod 之間的 Session 有同步呢?
下一篇文章將會來測試這個部分!
其他 Ref #
Published