以Hazelcast + Spring Session建立Cluster Web
Hazelcast 就像她首頁講的:分散式計算的記憶體網格,簡單來說就是把資料分散放在各個伺服器的記憶體中;像Hazelcast類似的架構還有像Redis、memcached、Infinispan…(網路上也可以找到很多的比較),那為什麼我選Hazelcast呢?很簡單,因為像Redis可以在伺服器單獨建置而無須Java,而Hazelcast則是採用Java開發,所以程式在單元測試開始時可以一併建置Hazelcast,而無須確認記憶體網格伺服器啟動後後再來進行測試,而且因為可隨程式一起建置,更方便開發者在不同的環境內進行測試(比如您下載我的程式,就可以直接執行了,除了Java環境外不須要再建置任何系統)。另外,Hazelcast有Atomic與Transaction的機制,可適用於開發一些特殊的系統,例如:搶票系統(Hazelcast支援AWS)。
由於記憶體網格的特性,所以Java Web程式有機會在不同的電腦中複製Session資料,從而實質上達到 Cluster Web。
以前要用Java 建立Cluster Web無疑是煩瑣與費用高昂,而現今仍然也是需要一些成本(近來像Nginx這種反向代理伺服器可以用Sticky的方式建立叢集,也就是把用戶各自分配到不同的機器,然後從此用戶就黏著到特定的機器,當然,除非您自己結合nginx-sticky-module來編譯Nginx,否則也是要花錢買她的商業版本才有提供,而社群版只提供Round Robin的方式將來自用戶的存取分配到各伺服器,但這樣就沒法Keep Session了);Hazelcast的商業版本目前有提供Tomcat Cluster,而Redis目前也只提供Tomcat7的solution,那麼其它的 Web Server咋辦呢(JBoss有,但設定煩瑣)?所以Spring Session實做了各種不同Cluster儲存機制的Session Cluster,最棒的是我們根本不用改變我們的程式寫法,因為當我們使用Session.setAttribute()時,Spring Session在背後已經將資料寫入到Cluster Memories了。
後面我要來實做Web Cluster,前提是在開發編譯要先有Maven的環境(不熟的話,請先參考這裡), 而發佈環境則要有3台機器,其中兩台做為Java Web Server,而另一台要安裝Nginx做為反向代理伺服器。
$mvn archetype:generate -DarchetypeGroupId=com.github.kentyeh -DarchetypeArtifactId=springJdbiArch -DarchetypeVersion=2.5.1
Define value for property 'groupId'(程式群組): :idv.kent
Define value for property 'artifactId'(專案名稱): : ttxxx
Define value for property 'version'(自訂版本): 1.0-SNAPSHOT: :
Define value for property 'package': idv.kent: :
[INFO] Using property: CLUSTER_MEMBERS(Cluster成員) = 127.0.0.1
[INFO] Using property: DomainName = CN=localhost, OU=MIS, O=Spring Software Dev Co., L=Kaohsiung, ST=Taiwan, C=TW
[INFO] Using property: HTTP_PORT = 8080
[INFO] Using property: JndiDataSourceName = jdbc/JNDIDS
[INFO] Using property: SSL_PORT = 8443
Confirm properties configuration:
groupId: idv.kent
artifactId: ttxxx
version: 1.0-SNAPSHOT
package: kent.idv
CLUSTER_MEMBERS: 127.0.0.1
DomainName: CN=localhost, OU=MIS, O=Spring Software Dev Co., L=Kaohsiung, ST=Taiwan, C=TW
HTTP_PORT: 8080
JndiDataSourceName: jdbc/JNDIDS
SSL_PORT: 8443
Y: :
|
由上面的操作,我們建立了一個簡單的Java Web程式,當然,上述的設定針對程式只在一部伺服器執行是足夠了,但之前我說過,我們準了兩部機器要來實做叢集,假設兩台機器的IP分別是192.168.10.74與192.168.10.78,所以我首先要針對這點進行設定,首先開啟檔案 ttxxx/src/main/resources/applicationContext-hazelcast.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans ..
…
<hz:members>192.168.10.74,192.168.10.78</hz:members>
</beans>
|
上述的成員設定我是用逗點(,)分隔,當然也可以用 192.168.10.74-78 指定一連串的機器做為Cluster Members。
在檔案 ttxxx/pom.xml 內我預設了 http 使用 8080 port,https 使用了 8443 port,若您要指定 80 與 443 port,請自行修改。
為了方便測試,請用以下指令建立可執 行 War 檔
$mvn -Prunwar package
…
[INFO] Building war: ./ttxxx/target/ttxxx.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
|
完成後將 ttxxx.war 拷貝到 192.168.10.74 與 192.168.10.78 兩台電腦並執行以下指令啟動Web程式
$java -jar ttxxx.war
|
請注意:防火牆請開放 8080(http)、8443(ssl)與5701(Hazelcast通訊)三個埠號。
再來我們在第三部機器上(也就是本機localhost)建置Nginx。因為上述的程式在登錄時會轉用SSL進行登錄,所以程式編譯的同時也產生了臨時憑證,憑證檔案在 ttxxx/target/ttxxx/WEB-INF/ssl.keystore,要先轉換才能用在 Nginx上,請到WEB-INF下執行以下轉換(其中的localhost是我們要的Domain Name)
$keytool -importkeystore -srckeystore ssl.keystore -destkeystore localhost.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass password -deststorepass newpassword -srcalias tomcat -destalias localhost -srckeypass password -destkeypass newpassword -noprompt
$openssl pkcs12 -in localhost.p12 -nodes -nocerts -out localhost.key
Enter Import Password:newpassword
MAC verified OK
$openssl pkcs12 -in localhost.p12 -nodes -nokeys -clcerts -out localhost.cer
Enter Import Password:newpassword
MAC verified OK
|
http{
…
upstream httpGroup {
server 192.168.10.74:8080 weight=1;
server 192.168.10.78:8080 weight=1;
}
upstream httpsGroup {
server 192.168.10.74:8443 weight=1;
server 192.168.10.78:8443 weight=1;
}
server {
listen 80;
server_name localhost;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /ttxxx {
proxy_pass http://httpGroup/ttxxx;
}
#因為登錄時會轉向login所以必須將之導向https://
location /ttxxx/login {
rewrite ^(.*) https://$host$1 permanent;
}
}
server {
listen 443 ssl http2 default_server;
server_name localhost;
ssl_certificate localhost.cer;
ssl_certificate_key localhost.key;
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:!RC4:!aNULL:!eNULL:!MD5:!EXPORT:!EXP:!LOW:!SEED:!CAMELLIA:!IDEA:!PSK:!SRP:!SSLv:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /ttxxx {
proxy_pass https://httpsGroup/ttxxx;
}
}
}
|
現在可以開啟 http://localhost/ttxxx/ 將後進行一系列操作並觀察192.168.10.74與192.168.10.78兩部機器的 Console ,由於上述nginx.conf將兩台機器的權重設定成一樣,所以兩部機器會輪流處理來自用戶的請求,現在按右上角的我的個人資料按紐,進行登錄後,可以看到登錄者的資料,連續更新畫面數次,用戶請求分別輪流由兩台機器處理,但仍然可看見登錄的用戶資料,所以確定 Session 資料確實有在兩台機器中複製。
Hazelcast的教學,網路上可以找到很多,這裡就不提了,如何將Hazelcast的物件Inject到程式碼中,可以參考 ttxxx/src/main/java/idv/kent/controller/DefaultController.java 的內容,針對 ttxxx/src/main/resources/applicationContext-hazelcast.xml 有幾點想要說明一下:
- <hz:spring-aware /> 這行我會建議拿掉,理由是因為衝擊效能,除非您想把一些Spring 環境下的Bean放進Hazelcast中,例如這個Bean 有 implements ApplicationContextAware、InitializingBean或BeanNameAware,或是有Autowired一些外部物件,又或本身某些方法有標記為PostConstruct,因為在Hazelcast的物件會分散到各個機器中,而Spring的環境是沒有必要帶入到Hazelcast裡,所以當Spring Bean物件從Hazelcast裡取出後必須重建這些物件到Spring環境中,所以從範例中不難看出,為什麼她要把像是ApplicationContext、SomeBean標記為transient?為的就是不要將這些環境物件一起被序列化到Hazelcast裡。
- <hz:serialization>這個段落我也建議移除,會放進去是因為Spring Session的範例程式放入了這樣的代碼,所以我照章也把ObjectStreamSerializer放了進來,實際上Hazelcast已經序列化做了很精細的規劃,像這樣直接把Object用一個物件來做序列化實在有些粗糙了,除非您的物件有很特殊的序列化規劃,否則還是不要自已操心的好。
- 設定檔中的Element屬性有 id 與 name 的意義:如果不使用Spring,Hazelcast取得物件的方式是透過HazelcastInstance.getXXX(name)的方式取得,所以Spring Session實際上是把Session資料放在一個 name=”spring:session:sessions”的Map中,而id則是純粹是為了直接Autowired用。
- 最後一個問題是<hz:map>這放在<hz:config>內與放在最外層有何不同?說實在,我並沒有在網路上看到這裡面的說法。以下是我的理解:
- Hazelcast可分Server與Client,Server要負責資料互相備援與資料存放等相關設定,而Client只負責連上去用就是了,毋論Server或Client,其類別都是HazelcastInstance,所以不管是Server或是Client,只要那一個先呼叫了HazelcastInstance.getXXX(name),該物件都會被建立,後面叫用的則取得前次建立的物件參考,即使是clientInstance.getQueue("aQueue");一樣能在Server Cluster建立一個"aQueue"物件。那麼Spring如何所在Client程式的inject這個Queue呢?直接一個最外層的設定<hz:queue id="aQueue" instance-ref="hazelcastInstance" name="aQueue"/>就可以了。
- 在<hz:config>內的物件設定,程式透過HazelcastInstance.getXXX(name)呼叫一樣能取得該物件,其好處是可以針對物件作更精細的設定,缺點則是不能直接inject到Spring beans裡,那怎麼辦,物件雖在<hz:config>內建立,但我們只要在最外層加一個同name的參考如<hz:map id="sessionMap" instance-ref="hazelcastInstance" name="spring:session:sessions"/>就可以用來inject到Spring Beans了。
留言
張貼留言