Ehcache Tiering Options
Ehcache supports the concept of tiered caching. This section covers the different available configuration options. It also explains rules and best practices to maximize the benefits of tiered caching.
For a general overview of available storage tiers, see the section on Storage Tiers.
1. Moving Out of Heap
When you have tiers other than the heap tier in your cache, several things happen:
- Adding a mapping to the cache means the key and value must be serialized.
- Reading a mapping from the cache means the key and value may need to be deserialized.
With these two points, you need to recognize that the binary representation of data and how it converts to and from serial data will play an important role in cache performance. Make sure you understand the options available for serializers (see the "Serializers" section). Also, this means some configurations, while making sense on paper, may not provide optimal performance based on your application's actual usage.
2. Single Tier Setup
All tiering options can be used individually. For example, you can have cache data only in off-heap or only in a cluster. The following are valid configurations:
- heap
- offheap
- disk
- clustered
To do this, simply define a single resource in the cache configuration:
// First define key and value types in the configuration builder.
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
// Then specify the resource (tier) to use. Here we only use off-heap resource.
ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(2, MemoryUnit.GB)).build();
2.1 Heap Tier
Since no serialization is required, it's also faster as the starting point for each cache. You can choose to pass keys and values by value (see "Serializers and Copiers" section), with the default being by reference. The heap tier can be sized by entries or by size.
// Only allow 10 entries on heap. Will overflow when full.
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, EntryUnit.ENTRIES);
// or shortcut for specifying 10 entries.
ResourcePoolsBuilder.heap(10);
// or only allow 10 MB. Will evict when full.
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB);
2.1.1 Byte-Sized Heap
For every tier except the heap tier, calculating cache size is very easy. You more or less sum the sizes of all byte buffers containing serialized entries. When heap is limited by size rather than entries, it's slightly more complex.
The impact of byte sizing on runtime performance depends on the size and graph complexity of cached data.
CacheConfiguration<Long, String> usesConfiguredInCacheConfig = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
// This limits the amount of memory the heap tier uses to store key-value pairs. Object sizing has a cost.
.heap(10, MemoryUnit.KB)
// These settings are only used by the heap tier. So off-heap won't use them at all.
.offheap(10, MemoryUnit.MB))
.withSizeOfMaxObjectGraph(1000)
// Sizing can also be further limited by 2 other configuration settings: the first specifies the maximum number of objects to traverse when moving through the object graph (default: 1000),
// the second defines the maximum size of a single object (default: Long.MAX_VALUE, so almost infinite).
// If sizing exceeds either of these limits, the entry won't be stored in the cache.
.withSizeOfMaxObjectSize(1000, MemoryUnit.B)
.build();
CacheConfiguration<Long, String> usesDefaultSizeOfEngineConfig = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(10, MemoryUnit.KB))
.build();
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withDefaultSizeOfMaxObjectSize(500, MemoryUnit.B)
// Unless explicitly defined, default configuration can be provided at CacheManager level for caches to use.
.withDefaultSizeOfMaxObjectGraph(2000)
.withCache("usesConfiguredInCache", usesConfiguredInCacheConfig)
.withCache("usesDefaultSizeOfEngine", usesDefaultSizeOfEngineConfig)
.build(true);
2.1.2 Off-Heap Tier
If you want to use off-heap resources, you must define a resource pool and provide the memory size to allocate.
// Only 10 MB allowed off-heap. Will evict when full.
ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(10, MemoryUnit.MB);
The above example allocates a very small amount of off-heap resources. Typically, you'll use larger amounts.
Remember that off-heap stored data must be serialized and deserialized - so it's slower than heap. Therefore, you should prefer off-heap for large amounts of data, as on-heap data would have too much impact on garbage collection. Don't forget to define the -XX:MaxDirectMemorySize option in java options based on the off-heap size you want to use.
2.1.3 Disk Tier
For the disk tier, data is stored on disk. The faster and more dedicated the disk, the faster data access will be.
// Get a PersistentCacheManager, which is a normal CacheManager but with the ability to destroy caches.
PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
// Provide the location where data should be stored.
.with(CacheManagerBuilder.persistence(new File(getStoragePath(), "myData")))
// Define the disk resource pool to be used by the cache. The third parameter is a boolean to set whether the disk pool is persistent. When set to true, the pool is persistent. When using the two-parameter version disk(long, MemoryUnit), the pool is not persistent.
.withCache("persistent-cache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().disk(10, MemoryUnit.MB, true))
)
.build(true);
persistentCacheManager.close();
The above example allocates a very small amount of disk storage. Typically, you'll use larger storage. Persistence means the cache will survive JVM restarts. After restarting the JVM and creating a CacheManager with disk persistence at the same location, everything in the cache still exists.
The disk tier cannot be shared between cache managers. The persistence directory is dedicated to one cache manager at a time.
Remember that data stored on disk must be serialized/deserialized before writing to/reading from disk - so it's slower than heap and off-heap. Therefore, disk storage is interesting when:
- You have large amounts of data that can't fit elsewhere
- Your disk is much faster than the storage it's caching
- You're interested in persistence
Ehcache 3 only provides persistence on clean shutdown (calling close()). If the JVM crashes, data integrity is not guaranteed. After restart, Ehcache detects that the CacheManager wasn't cleanly closed and will wipe the disk storage before using it.
Segments
Disk storage is divided into multiple segments that provide concurrent access but also keep open file pointers. The default is 16. In some cases, you may want to reduce concurrency and save resources by reducing the number of segments.
String storagePath = getStoragePath();
PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.with(CacheManagerBuilder.persistence(new File(storagePath, "myData")))
.withCache("less-segments",
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().disk(10, MemoryUnit.MB))
// Define an OffHeapDiskStoreConfiguration instance to specify the desired number of segments.
.withService(new OffHeapDiskStoreConfiguration(2))
)
.build(true);
persistentCacheManager.close();