Ehcache Replicated Caching Cache

Ehcache Replicated Caching 的介紹與使用

葉文華 2020/03/05 10:00:00
100

Ehcache是java中流行的快取技術,而它提供的Replicated Caching機制,可以讓資料同時存放在多應用節點(伺服器)中,一個節點資料更新了,其機制自動將資料以同步(synchronous)或異步(asyncrhonous)的方式傳播到每個節點。Ehcache Replicated Caching功能,讓應用開發時只需要實現Ehcache的本地快取用法,而不需要注意多應用節點資料同步的問題,即可讓每個節點都享受到快取的成果。

Ⅰ. 為什麼需要Replicated Caching

特定資料,有時候會有重覆使用的需求,即適合放置在快取當中。若多應用節點(伺服器)有同一組資料的使用需求時,即適合使用Replicated Caching機制。但假設將資料放在共用存取的磁碟空間或是資料庫中,或是將資料只存在單一節點記憶體中時,又失去了應用開發的彈性,應用架構也會限制了使用的情境,造成開發複雜度升高。因此列出以下可選方案的使用時機:

  • 外部分散式快取伺服器:一般外部的伺服器成本自然較高,可能無此選項可以利用。但其可擴充性及效能考量會是理想的方案之一。
  • 共用磁碟空間或資料庫:磁碟空間或資料庫若存放資料需要落地時,在請求數量及反應時間需求(TPS、RT)上容易產生瓶頸。若是採用記憶體型資料庫也是可以考慮的選項,且可應對排序、運算或較複雜查詢的需求。
  • 本地快取機制且支援擴充Replication: 在原已使用有快取技術,且該快取技術支援Replicated Caching機制,可快速擴充支援。不過此類機制一般對於資料的鎖定、同步一致性上自然沒有外部伺服器那麼完整強大,但仍然適合許多對資料的即時性、一致性沒有太嚴格要求的服務上使用。

Ehcache的Replicated Caching機制如下圖(以使用RMI為例)所示,對應用來說都是處理Local那一份Ehcache的快取即可,Replicated Caching會自動將資料同步到其他應用節點上,若有跨節點使用同樣資料需求時,將可簡化整體的應用架構,僅需要考慮單一節點內部的架構即可。

Ⅱ. 實作配置

Ehcache Replicated Caching提供了RMI、JGroups、JMS等做法。以下針對RMI叢集架構的手動配置方式進行實作,其他做法可參考:Replication方法列表

這裡實作兩台web伺服器(應用1、應用2),已經有完成本地端Ehcache使用的架構下,直接在預設的ehcache.xml,加上cacheManagerPeerProviderFactory、cacheManagerPeerListenerFactory,以及cache中的cacheEventListenerFactory、bootstrapCacheLoaderFactory等基本元素即可完成必須的設定(可參考using RMI),說明如下:

    ●  cacheManagerPeerProviderFactory:作用為尋找成員,在這個元素上可指定兩種Ehcache提供的尋找成員機制(peerDiscovery),以及尋找的位址等屬性。

        * peerDiscovery=automatic,以TCP廣播的方式自動尋找設定在multicastGroupAddress、multicastGroupPort中指定的廣播網段,它將可以自動的在RMI叢集中添加或移除成員。

        註:廣播方式,是一個複雜的過程,除了跨網段問題、位址跳接(hop)被阻斷問題外,還有伺服器是否開啟允許廣播等限制。

        * peerDiscovery=manual,尋找手動配置在rmiUrls中的服務器(快取)列表,將不能動態添加和移除成員。本篇實作即以手動配置方式來完成此RMI叢集的尋找。

        rmiUrls=//serverA:portA/cacheNameA|//serverB:portB/cacheNameB... 
        有多組成員時,用|符號分隔,並且必須指定其他成員hostName、port及要同步cache的name

    ●  cacheManagerPeerListenerFactory:作用為監聽從成員們傳播到當前CacheManager的訊息。

        hostName=192.168.43.28,port=40001,設定應用自己的hostName、port

    ●  cacheEventListenerFactory:作用為將需要同步的cache向CacheManager成員複製訊息的監聽器,需要在每個有同步需求的cache設定中增加此元素。

        cacheEventListenerFactory有更多的屬性可以設定同步複製的策略:
            * replicatePuts:新加入的element是否進行複製
            * replicateUpdates:覆寫掉的element是否進行複製
            * replicateRemovals:清除掉的element是否進行複製
            * replicateAsynchronously:以asyncrhonous(true)或synchronous (false)方式進行複製
            * replicateUpdatesViaCopy:在element複製到其他cache中時是否進行複製

    ●  bootstrapCacheLoaderFactory:作用為初始化快取,可在啟動時同步初始化資料(例:伺服器輪流重啟時,可將叢集中的資料狀態進行同步),需要在每個有同步需求的cache設定中增加此元素。

應用1的ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation = "http://ehcache.org/ehcache.xsd">

    <diskStore path = "java.io.tmpdir"/>

    <cacheManagerPeerProviderFactory 
         class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" 
         properties="peerDiscovery=manual,rmiUrls=//192.168.43.28:40002/bootCache"
    />

    <cacheManagerPeerListenerFactory
	       class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
         properties="hostName=192.168.43.28,port=40001" />
  
    <cache
         name = "bootCache"
         eternal = "false"
         maxElementsInMemory = "1000"
         overflowToDisk = "false"
         diskPersistent = "false"
         timeToIdleSeconds = "0"
         timeToLiveSeconds = "180"
         memoryStoreEvictionPolicy = "LRU">
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
        <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
    </cache>
</ehcache>
應用2的ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation = "http://ehcache.org/ehcache.xsd">

    <diskStore path = "java.io.tmpdir"/>

    <cacheManagerPeerProviderFactory 
         class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" 
         properties="peerDiscovery=manual,rmiUrls=//192.168.43.28:40001/bootCache"
    />

    <cacheManagerPeerListenerFactory
	       class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
         properties="hostName=192.168.43.28,port=40002" />
  
    <cache
         name = "bootCache"
         eternal = "false"
         maxElementsInMemory = "1000"
         overflowToDisk = "false"
         diskPersistent = "false"
         timeToIdleSeconds = "0"
         timeToLiveSeconds = "180"
         memoryStoreEvictionPolicy = "LRU">
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
        <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
    </cache>
</ehcache>
啟動Log(應用1)

由log中可觀察到在應用1啟動時,將應用2(port號40002)註冊進應用1的叢集名單中(元素cacheManagerPeerProviderFactory),開始應用1的監聽器(元素cacheManagerPeerListenerFactory),並初始化應用1中bootCache的快取(元素cache)。

[main] [DEBUG] [net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory:91] - Registering peer //192.168.43.28:40002/bootCache
[main] [DEBUG] [net.sf.ehcache.distribution.RMICacheManagerPeerListener:215] - 0 RMICachePeers bound in registry for RMI listener
...
[main] [DEBUG] [net.sf.ehcache.store.MemoryStore:180] - Initialized net.sf.ehcache.store.MemoryStore for bootCache
[main] [DEBUG] [net.sf.ehcache.Cache:1262] - Initialised cache: bootCache
...
[Bootstrap Thread for cache bootCache] [DEBUG] [net.sf.ehcache.distribution.RMIBootstrapCacheLoader:193] - Attempting to acquire cache peers for cache bootCache to bootstrap from. Will wait up to 0ms for cache to join cluster.
[main] [DEBUG] [net.sf.ehcache.distribution.RMICacheManagerPeerListener:516] - Adding to RMI listener
[Bootstrap Thread for cache bootCache] [DEBUG] [net.sf.ehcache.distribution.RMICacheManagerPeerProvider:126] - Lookup URL //192.168.43.28:40002/bootCache

Ⅲ. 測試驗證

1. 應用1同步快取到應用2: 正常

    應用1寫入快取->應用1將快取資料變更同步到叢集名單伺服器->應用2監聽到傳入的快取資料變更

應用1的Log

[Replication Thread] [DEBUG] [net.sf.ehcache.distribution.RMICacheManagerPeerProvider:126] - Lookup URL //192.168.43.28:40002/bootCache

應用2的Log

[RMI TCP Connection(15)-192.168.43.28] [DEBUG] [net.sf.ehcache.distribution.RMICachePeer:180] - RMICachePeer for cache bootCache: remote put received. Element is: [ key = count, value=0, version=1, hitCount=0, CreationTime = 1583204581000, LastAccessTime = 1583204580600 ]

2. 應用2同步快取到應用1:正常

    應用2寫入快取->應用2將快取資料變更同步到叢集名單伺服器->應用1監聽到傳入的快取資料變更

應用2的Log

[Replication Thread] [] [DEBUG] [net.sf.ehcache.distribution.RMICacheManagerPeerProvider:126] - Lookup URL //192.168.43.28:40001/bootCache

應用1的Log

[RMI TCP Connection(20)-192.168.43.28] [] [DEBUG] [net.sf.ehcache.distribution.RMICachePeer:180] - RMICachePeer for cache bootCache: remote put received. Element is: [ key = count, value=1, version=1, hitCount=0, CreationTime = 1583205417000, LastAccessTime = 1583205417240 ]

3. 應用2重啟服務自動同步叢集中快取資料:正常

    應用2停止服務->應用1更新快取->應用1將快取資料變更同步到應用2失敗->應用2重新啟動服務->應用2由叢集中將快取同步進來

應用1的Log

[Replication Thread] [DEBUG] [net.sf.ehcache.distribution.ManualRMICacheManagerPeerProvider:99] - Looking up rmiUrl //192.168.43.28:40002/bootCache through exception Connection refused to host: 192.168.43.28; nested exception is:
    java.net.ConnectException: Connection refused (Connection refused). This may be normal if a node has gone offline. Or it may indicate network connectivity difficulties

應用2的Log

[Bootstrap Thread for cache bootCache] [DEBUG] [net.sf.ehcache.distribution.RMIBootstrapCacheLoader:212] - cache peers: [RMICachePeer_Stub[UnicastRef2 [liveRef: [endpoint:[192.168.43.28:64803,net.sf.ehcache.distribution.ConfigurableRMIClientSocketFactory@1d4c0](remote),objID:[-1e9f7c95:1709e25ffa1:-7fff, -970160376792800271]]]]]
[Bootstrap Thread for cache bootCache] [DEBUG] [net.sf.ehcache.distribution.RMIBootstrapCacheLoader:140] - Bootstrapping bootCache from RMICachePeer_Stub[UnicastRef2 [liveRef: [endpoint:[192.168.43.28:64803,net.sf.ehcache.distribution.ConfigurableRMIClientSocketFactory@1d4c0](remote),objID:[-1e9f7c95:1709e25ffa1:-7fff, -970160376792800271]]]]
[Bootstrap Thread for cache bootCache] [DEBUG] [net.sf.ehcache.distribution.RMIBootstrapCacheLoader:173] - Bootstrap of bootCache from RMICachePeer_Stub[UnicastRef2 [liveRef: [endpoint:[192.168.43.28:64803,net.sf.ehcache.distribution.ConfigurableRMIClientSocketFactory@1d4c0](remote),objID:[-1e9f7c95:1709e25ffa1:-7fff, -970160376792800271]]]] finished. 1 keys requested.

4. 應用1及應用2更新資料組(Map),但取得、回寫交叉完成:異常

    應用1取得Map->應用2取得Map->

          應用1加入aaa=0101回寫->應用2加入bbb=0202->

                最終Map只剩下bbb=0202

cache中Map={zzz=2626}
應用1取得Map={zzz=2626}
應用2取得Map={zzz=2626}
應用1加入aaa=0101,回寫cache的Map={aaa=0101, zzz=2626}
應用2加入bbb=0202,回寫cache的Map={bbb=020, zzz=2626}

註:此測試未進行並發事務的讀寫鎖機制或是使用阻塞式的快取,故會造成2次加入資料操作結果與預期的{aaa=0101, bbb=020, zzz=2626}不同,無法滿足事務的獨立性 (Isolation)。

Ⅳ. 注意事項

Replicated Caching機制的使用上雖然有方便,但也必然有其需要事先評估的地方,以下列舉常見的問題:

  • 一致性問題:分散式系統要保障一致性相當不容易,愈強的一致性標準代表愈弱的處理效能及愈差的擴充性,所以許多時候只能保證最終一致性。如果在資料的時效上沒有太嚴格的要求標準時,最終一致性仍然讓結果是理想的。但若更新資料頻率比較高,或是服務的吞吐量比較高時,會造成同步過程中一直處在不一致的狀況,仍然要考慮此機制的問題。
  • 快取粒度問題:快取的資料,在使用上一個較大的資料組(例:Map、List、Object)通常比較簡單,但快取的同步則像是相反。比如測試驗證案例4中由維護一個Map改成個別的變數,即不會發生該獨立性問題;而愈小粒度的資料異動,需要同步更新的資料量、網路流量等等壓力也會較小。
  • 系統負載問題:若Replicated Caching機制實作時是與應用綁定在一起,代表會使用到應用服務本身可利用的資源,例如: 記憶體、網路甚至是磁碟空間。

Ⅴ. 總結

快取機制,在許多系統中已經是最常見的資料使用方式,可以較容易的提高系統的回應時間及吞吐量。而本文所介紹,應用服務結合Ehcache的Replicated Caching機制,使用上也相當容易,更簡化了開發系統中容易碰到的資料共享的架構性問題,在無更強大的外部快取伺服器選項時,是一個值得考慮的方案。

Ⅵ. 參考資料

Replicated Caching using RMI

深入探討在集群環境中使用Ehcache緩存系統

 

葉文華