缓存也许是程序员心中最熟悉的性能优化手段之一, 在旧文中 微服务缓存漫谈之Guava Cache 和 Redis 集群的构建和监控 中分别介绍了最常用的本地内存的 Guava Cache 和远程的 Redis Cache. 这里我们重点聊聊缓存的度量。
缓存的常见问题
对于缓存,我们关心这几个问题:
- Cache hit ratio 缓存的命中率
- Cache key size 缓存的键值数量
- Cache resource usage 缓存的资源使用率
- Cache loading performance 缓存的加载性能
- Cache capacity 缓存的容量
- Cache lifetime 缓存的生命周期
Cache 不可能无限增长, 不可能永远有效, 所以对于 Cache 的清除策略和失效策略要细细考量.
对于放在 Cache 中的数据也最好是读写比较高的, 即读得多, 写得少, 不会频繁地更新.
缓存不是万能药,缓存使用不当会生成缓存穿透,击穿和雪崩,先简单解释一下这几个概念
- 穿透
某条记录压根不存在,所以在缓存中找不到,每次都需要到数据库中读取,但是结果还是找不到。
常用的应对方法是布隆过滤器(它的特点是)或者反向缓存(在缓存中保存这条记录,标识它是不存在的)
- 击穿
某条记录过期被移除了,恰好大量相关的查询请求这条记录,导致瞬时间大量请求绕过缓存访问数据库
常用的应对方法是将从数据库加载数据的操作加锁,这样就不会有很多访问请求绕过缓存。
或者干脆不设置过期时间,而是用一个后台job 定时刷新缓存,外部的请求总能从缓存中读到数据
3.雪崩
多条记录在多个服务器上的缓存同时过期失效,导致瞬时间大量请求绕过缓存访问数据库,这个比击穿更严重。
常用的应对方法是多个服务器上的多条记录设置不同的失效时间,可以用个随机值作为零头,将大量的并发请求从某个时间点分布到一个时间段中
缓存的度量
缓存的命中率,加载性能等等都是我们关心的重点,例如:
- 性能 Performance: Cache 加载的延迟 latency
- 吞吐量Throughput: 每秒请求次数 CPS(Call Per Second)
- 命中率:sucess_ratio = hitCount / (hitCount + missCount)
- 资源使用量: 使用了多少内存
- 资源饱和度 saturation: 由于容量限制被移出cache 的记录数,缓存满了无法增加的记录数
注: 饱和度是资源负载超出其处理能力的地方。
以 Guava Cache 为例,它的缓存统计信息根据以下规则递增:
- 当缓存查找遇到现有缓存条目时,hitCount会增加。
- 当缓存查找第一次遇到丢失的缓存条目时,将加载一个新条目。
- 成功加载条目后,missCount和loadSuccessCount会增加,并将总加载时间(以纳秒为单位)添加到totalLoadTime中。
- 在加载条目时引发异常时,missCount和loadExceptionCount会增加,并且总加载时间(以纳秒为单位)将添加到totalLoadTime中。
- 遇到仍在加载的缺少高速缓存条目的高速缓存查找将等待加载完成(无论是否成功),然后递增missCount。
- 从缓存中逐出条目时,evictionCount会增加。
- 当缓存条目无效或手动删除时,不会修改任何统计信息。
- 在缓存的asMap视图上调用的操作不会修改任何统计信息。
我们在写代码时可以调用它的 recordStats 来记录这些度量数据
@Bean
public LoadingCache<String, CityWeather> cityWeatherCache() {
LoadingCache<String, CityWeather> cache = CacheBuilder.newBuilder()
.recordStats()
.maximumSize(1000)
.expireAfterWrite(60, TimeUnit.MINUTES)
.build(weatherCacheLoader());
recordCacheMetrics("cityWeatherCache", cache);
return cache;
}
public void recordCacheMetrics(String cacheName, Cache cache) {
MetricRegistry metricRegistry = metricRegistry();
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "hitCount"), () -> () -> cache.stats().hitCount());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "hitRate"), () -> () -> cache.stats().hitRate());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "missCount"), () -> () -> cache.stats().missCount());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "missRate"), () -> () -> cache.stats().missRate());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "requestCount"), () -> () -> cache.stats().requestCount());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadCount"), () -> () -> cache.stats().loadCount());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadSuccessCount"), () -> () -> cache.stats().loadSuccessCount());
metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadExceptionCount"), () -> () -> cache.stats().loadExceptionCount());
}
public String generateMetricsKeyForCache(String cacheName, String keyName) {
String metricKey = MetricRegistry.name("cache", cacheName, keyName);
log.info("metric key generated for cache: {}", metricKey);
return metricKey;
}
未完待续...