以Hazelcast + Spring Session建立Cluster Web

Hazelcast 就像她首頁講的:分散式計算的記憶體網格,簡單來說就是把資料分散放在各個伺服器的記憶體中;像Hazelcast類似的架構還有像RedismemcachedInfinispan…(網路上也可以找到很多的比較),那為什麼我選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做為反向代理伺服器。
首先我們要在開發環境中建立我們的Java程式,請先執行以下指令建立專案
$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
然後將產生的localhost.cer與localhost.key拷貝到 Nginx 設定檔 nginx.conf 的相同目錄中,下以是我在nginx.conf的主要設定部分
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;
       }
   }
}
:上面的設定是Nginx 1.9.5+支持 HTTP/2 的版本設定,若是在Linux環境則 openssl 必須是1.0.2以後的版本,若是之前的版本設定,請參考這裡
現在可以開啟 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 有幾點想要說明一下:
  1. <hz:spring-aware /> 這行我會建議拿掉,理由是因為衝擊效能,除非您想把一些Spring 環境下的Bean放進Hazelcast中,例如這個Bean 有 implements ApplicationContextAwareInitializingBeanBeanNameAware,或是有Autowired一些外部物件,又或本身某些方法有標記為PostConstruct,因為在Hazelcast的物件會分散到各個機器中,而Spring的環境是沒有必要帶入到Hazelcast裡,所以當Spring Bean物件從Hazelcast裡取出後必須重建這些物件到Spring環境中,所以從範例中不難看出,為什麼她要把像是ApplicationContext、SomeBean標記為transient?為的就是不要將這些環境物件一起被序列化到Hazelcast裡。
  2. <hz:serialization>這個段落我也建議移除,會放進去是因為Spring Session範例程式放入了這樣的代碼,所以我照章也把ObjectStreamSerializer放了進來,實際上Hazelcast已經序列化做了很精細的規劃,像這樣直接把Object用一個物件來做序列化實在有些粗糙了,除非您的物件有很特殊的序列化規劃,否則還是不要自已操心的好。
  3. 設定檔中的Element屬性有 id 與 name 的意義:如果不使用Spring,Hazelcast取得物件的方式是透過HazelcastInstance.getXXX(name)的方式取得,所以Spring Session實際上是把Session資料放在一個 name=”spring:session:sessions”的Map中,而id則是純粹是為了直接Autowired用。
  4. 最後一個問題是<hz:map>這放在<hz:config>內與放在最外層有何不同?說實在,我並沒有在網路上看到這裡面的說法。以下是我的理解:
    1. 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"/>就可以了。
    2. <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了。

留言

這個網誌中的熱門文章

企業人員的統一管理-FreeIPA學習筆記

證交所最佳五檔的程式解析

Postgresql HA