Monday, December 2, 2013

making guava cache better with jmx

caching with jmx is just so much better

If you've never used this before, you're missing out. Being able to remotely check statistics on your cache to measure its effectiveness, as well as being able to purge it at runtime is invaluable. Sadly Guava doesn't have this baked in the way ehcache does, but it's relatively easy to add.

Most of my work is a slightly different take on some work a fellow Github user named kofemann produced (located here) which contains the JMX beans and bean registration logic. I made a few alterations to the code, pulling the registration out into a separate class (I really didn't like the bean doing all that work in the constructor) and adding a refreshAll method.

taking advantage of refresh after write functionality

If you've read my previous blog post about the awesomeness that is Guava's refresh after write functionality, then you'll see how it can be advantageous when it comes to JMX management. If you didn't read my post (shame on you), then it's worth calling out using refresh after write allows for asynchronous loading of cache values, meaning you never block barring the initial loading of the cache.

This can be used via JMX management as well by iterating through the keys of the cache and calling refresh for each one, which will load new values without causing clients of the cache to block (as opposed to purging the cache). Purging a cache is a dangerous thing to do under certain circumstances, since missing values will trigger loading events that will block clients at runtime and potentially overwhelm either your application server or even your underlying data storage. I would argue that ehcache is particularly bad because of potential read contention caused by write blocking. To clarify, several threads in your application can block waiting for cache values to be reloaded, and all of those blocking threads will then compete over a limited number of read locks after the write lock has been released, potentially causing a CPU spike and considerable latency in your application under the worst conditions. When I say worst conditions, I'm speaking from very recent and harrowing experience, so I have the lumps to say with the utmost certainty this can happen. :)

the implementation

For JMX you need an interface and an implementation. The interface can be found on my Gist and doesn't really need to be shown in the post. The implementation is below; it's really a wrapper around Guava's CacheStats object and the cleanup/invalidateAll methods, as well as my refreshAll method:

import java.util.concurrent.ConcurrentMap;
/**
* A JMX wrapper for google's Guava Cache
*/
public class GuavaCacheMXBeanImpl<K, V> implements GuavaCacheMXBean<K, V> {
private final com.google.common.cache.LoadingCache<K, V> cache;
public GuavaCacheMXBeanImpl(com.google.common.cache.LoadingCache<K, V> cache) {
this.cache = cache;
}
@Override
public long getRequestCount() {
return cache.stats().requestCount();
}
@Override
public long getHitCount() {
return cache.stats().hitCount();
}
@Override
public double getHitRate() {
return cache.stats().hitRate();
}
@Override
public long getMissCount() {
return cache.stats().missCount();
}
@Override
public double getMissRate() {
return cache.stats().missRate();
}
@Override
public long getLoadCount() {
return cache.stats().loadCount();
}
@Override
public long getLoadSuccessCount() {
return cache.stats().loadSuccessCount();
}
@Override
public long getLoadExceptionCount() {
return cache.stats().loadExceptionCount();
}
@Override
public double getLoadExceptionRate() {
return cache.stats().loadExceptionRate();
}
@Override
public long getTotalLoadTime() {
return cache.stats().totalLoadTime();
}
@Override
public double getAverageLoadPenalty() {
return cache.stats().averageLoadPenalty();
}
@Override
public long getEvictionCount() {
return cache.stats().evictionCount();
}
@Override
public long getSize() {
return cache.size();
}
@Override
public void cleanUp() {
cache.cleanUp();
}
@Override
public void invalidateAll() {
cache.invalidateAll();
}
@Override
public void refreshAll() {
ConcurrentMap<K,V> map = cache.asMap();
for (K key : map.keySet()) {
cache.refresh(key);
}
}
}

As I said before, refreshAll has the advantage of not causing your application to potentially lock up due to cache contention; everything will load up in the background. Depending on how you have your thread pool set up for performing refreshes, you can also throttle how hard you're hitting your data store by restricting the number of concurrent fetches of data by limiting the threads available.

registering your cache in jmx

This is pretty straightforward: just pass your cache (in this case a LoadingCache because of refreshAll) to the method shown below and you'll expose it via JMX for statistics and management:

import java.lang.management.ManagementFactory;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import com.google.common.cache.LoadingCache;
public final class LoadingCacheJMXManager {
private LoadingCacheJMXManager() {}
public static <K, V> void registerLoadingCacheInJMX(String cacheName, LoadingCache<K, V> cache) {
String name = String.format("%s:type=%s,name=%s",
cache.getClass().getPackage().getName(), cache.getClass().getSimpleName(), cacheName);
try {
Object mBean = new GuavaCacheMXBeanImpl<K, V>(cache);
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName mxBeanName = new ObjectName(name);
if (!server.isRegistered(mxBeanName)) {
server.registerMBean(mBean, mxBeanName);
}
} catch (MalformedObjectNameException | InstanceAlreadyExistsException | MBeanRegistrationException
| NotCompliantMBeanException ex) {
throw new IllegalStateException(
String.format("An exception was thrown registering the JMX bean with the name '%s'", name), ex);
}
}
}

feedback

Let me know if this works for you; I plan on using this soon in a high load environment, so I'll follow up with any results I find to help out my readers. I feel kind of bad bagging on ehcache so much recently, but it's caused me enough gray hair over the last month that I plan on focusing several blog posts around caching.

1 comment:

  1. How are you registering the cache if you are using cache managers in spring boot?

    ReplyDelete