緩存也許是程序員心中最熟悉的性能優(yōu)化手段之一庸诱, 在舊文中 微服務(wù)緩存漫談之Guava Cache 和 Redis 集群的構(gòu)建和監(jiān)控 中分別介紹了最常用的本地內(nèi)存的 Guava Cache 和遠(yuǎn)程的 Redis Cache. 這里我們重點(diǎn)聊聊緩存的度量扼鞋。
緩存的常見問題
對于緩存珍昨,我們關(guān)心這幾個(gè)問題:
- Cache hit ratio 緩存的命中率
- Cache key size 緩存的鍵值數(shù)量
- Cache resource usage 緩存的資源使用率
- Cache loading performance 緩存的加載性能
- Cache capacity 緩存的容量
- Cache lifetime 緩存的生命周期
Cache 不可能無限增長, 不可能永遠(yuǎn)有效, 所以對于 Cache 的清除策略和失效策略要細(xì)細(xì)考量.
對于放在 Cache 中的數(shù)據(jù)也最好是讀寫比較高的, 即讀得多, 寫得少, 不會(huì)頻繁地更新.
緩存不是萬能藥,緩存使用不當(dāng)會(huì)生成緩存穿透,擊穿和雪崩瘦黑,先簡單解釋一下這幾個(gè)概念
- 穿透
某條記錄壓根不存在感混,所以在緩存中找不到竹伸,每次都需要到數(shù)據(jù)庫中讀取泥栖,但是結(jié)果還是找不到。
常用的應(yīng)對方法是布隆過濾器(它的特點(diǎn)是)或者反向緩存(在緩存中保存這條記錄勋篓,標(biāo)識它是不存在的)
- 擊穿
某條記錄過期被移除了吧享,恰好大量相關(guān)的查詢請求這條記錄,導(dǎo)致瞬時(shí)間大量請求繞過緩存訪問數(shù)據(jù)庫
常用的應(yīng)對方法是將從數(shù)據(jù)庫加載數(shù)據(jù)的操作加鎖譬嚣,這樣就不會(huì)有很多訪問請求繞過緩存钢颂。
或者干脆不設(shè)置過期時(shí)間,而是用一個(gè)后臺job 定時(shí)刷新緩存拜银,外部的請求總能從緩存中讀到數(shù)據(jù)
3.雪崩
多條記錄在多個(gè)服務(wù)器上的緩存同時(shí)過期失效甸陌,導(dǎo)致瞬時(shí)間大量請求繞過緩存訪問數(shù)據(jù)庫,這個(gè)比擊穿更嚴(yán)重盐股。
常用的應(yīng)對方法是多個(gè)服務(wù)器上的多條記錄設(shè)置不同的失效時(shí)間,可以用個(gè)隨機(jī)值作為零頭耻卡,將大量的并發(fā)請求從某個(gè)時(shí)間點(diǎn)分布到一個(gè)時(shí)間段中
緩存的度量
緩存的命中率疯汁,加載性能等等都是我們關(guān)心的重點(diǎn),例如:
- 性能 Performance: Cache 加載的延遲 latency
- 吞吐量Throughput: 每秒請求次數(shù) CPS(Call Per Second)
- 命中率:sucess_ratio = hitCount / (hitCount + missCount)
- 資源使用量: 使用了多少內(nèi)存
- 資源飽和度 saturation: 由于容量限制被移出cache 的記錄數(shù)卵酪,緩存滿了無法增加的記錄數(shù)
注: 飽和度是資源負(fù)載超出其處理能力的地方幌蚊。
以 Guava Cache 為例,它的緩存統(tǒng)計(jì)信息根據(jù)以下規(guī)則遞增:
- 當(dāng)緩存查找遇到現(xiàn)有緩存條目時(shí)溃卡,hitCount會(huì)增加溢豆。
- 當(dāng)緩存查找第一次遇到丟失的緩存條目時(shí),將加載一個(gè)新條目瘸羡。
- 成功加載條目后漩仙,missCount和loadSuccessCount會(huì)增加,并將總加載時(shí)間(以納秒為單位)添加到totalLoadTime中。
- 在加載條目時(shí)引發(fā)異常時(shí)队他,missCount和loadExceptionCount會(huì)增加卷仑,并且總加載時(shí)間(以納秒為單位)將添加到totalLoadTime中。
- 遇到仍在加載的缺少高速緩存條目的高速緩存查找將等待加載完成(無論是否成功)麸折,然后遞增missCount锡凝。
- 從緩存中逐出條目時(shí),evictionCount會(huì)增加垢啼。
- 當(dāng)緩存條目無效或手動(dòng)刪除時(shí)窜锯,不會(huì)修改任何統(tǒng)計(jì)信息。
- 在緩存的asMap視圖上調(diào)用的操作不會(huì)修改任何統(tǒng)計(jì)信息芭析。
我們在寫代碼時(shí)可以調(diào)用它的 recordStats 來記錄這些度量數(shù)據(jù)
@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;
}
對基于 Spring boot 的應(yīng)用程序锚扎,我們可以用 Micrometer 這個(gè)軟件庫來暴露度量指標(biāo)。
監(jiān)控軟件百花齊放放刨,類似于 SLF4j 作為一個(gè)外觀模式的應(yīng)用把 log4j, logback 這些庫的異同封裝起來工秩, MicroMeter 也把對各種監(jiān)控和度量技術(shù)棧的細(xì)節(jié)封裝起來,這樣應(yīng)用程序的開發(fā)者可以把精力放到應(yīng)用本身的度量上面进统。
Micrometer provides a simple facade over the instrumentation clients for the most popular monitoring systems, allowing you to instrument your JVM-based application code without vendor lock-in. Think SLF4J, but for application metrics! Application metrics recorded by Micrometer are intended to be used to observe, alert, and react to the current/recent operational state of your environment.
它的應(yīng)用也很簡單助币,在 pom.xml 中加入下面的依賴項(xiàng)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
- WeatherCacheConfig 配置如下
package com.github.walterfan.hellocache;
import com.codahale.metrics.MetricRegistry;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by yafan on 14/10/2017.
*/
@EnableAspectJAutoProxy
@ComponentScan
@Configuration
@Slf4j
public class WeatherCacheConfig {//implements EnvironmentAware
@Autowired
private Environment environment;
@Bean
public WeatherCacheLoader weatherCacheLoader() {
return new WeatherCacheLoader();
}
@Bean
public RestTemplate restTemplate() {
final RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
messageConverters.add(converter);
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
@Bean
public String appToken() {
return this.environment.getProperty("BAIDU_AK");
}
@Bean
public LoadingCache<String, CityWeather> cityWeatherCache() {
LoadingCache<String, CityWeather> cache = CacheBuilder.newBuilder()
.recordStats()
.maximumSize(1000)
.expireAfterWrite(60, TimeUnit.MINUTES)
.build(weatherCacheLoader());
recordCacheMetrics("cityWeatherCache", cache);
recordCacheMeters("cityWeatherCache", cache);
return cache;
}
public void recordCacheMetrics(String cacheName, Cache cache) {
MetricRegistry metricRegistry = metricRegistry();
metricRegistry.gauge(makeMetricsKeyName(cacheName, "hitCount"), () -> () -> cache.stats().hitCount());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "hitRate"), () -> () -> cache.stats().hitRate());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "missCount"), () -> () -> cache.stats().missCount());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "missRate"), () -> () -> cache.stats().missRate());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "requestCount"), () -> () -> cache.stats().requestCount());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "loadCount"), () -> () -> cache.stats().loadCount());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "loadSuccessCount"), () -> () -> cache.stats().loadSuccessCount());
metricRegistry.gauge(makeMetricsKeyName(cacheName, "loadExceptionCount"), () -> () -> cache.stats().loadExceptionCount());
}
public void recordCacheMeters(String cacheName, Cache cache) {
MeterRegistry meterRegistry = meterRegistry();
Gauge.builder(makeMetricsKeyName(cacheName, "hitCount"), () -> cache.stats().hitCount()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "hitRate"), () -> cache.stats().hitRate()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "missCount"), () -> cache.stats().missCount()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "missRate"), () -> cache.stats().missRate()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "requestCount"), () -> cache.stats().requestCount()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "loadCount"), () -> cache.stats().loadCount()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "loadSuccessCount"), () -> cache.stats().loadSuccessCount()).register(meterRegistry);
Gauge.builder(makeMetricsKeyName(cacheName, "loadExceptionCount"), () -> cache.stats().loadExceptionCount()).register(meterRegistry);
}
public String makeMetricsKeyName(String cacheName, String keyName) {
String metricKey = MetricRegistry.name("cache", cacheName, keyName);
log.info("metric key generated for cache: {}", metricKey);
return metricKey;
}
@Bean
public DurationTimerAspect durationTimerAspect() {
return new DurationTimerAspect();
}
@Bean
@Lazy
public MetricRegistry metricRegistry() {
return new MetricRegistry();
}
@Bean
public MeterRegistry meterRegistry() {
CompositeMeterRegistry compositeRegistry = new CompositeMeterRegistry();
SimpleMeterRegistry simpleMeter = new SimpleMeterRegistry();
PrometheusMeterRegistry prometheusMeterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
compositeRegistry.add(simpleMeter);
compositeRegistry.add(prometheusMeterRegistry);
return compositeRegistry;
}
}
這樣打開 http://localhost:8080/actuator/metrics
可以看到如下度量指標(biāo)
{
names: [
"cache.cityWeatherCache.hitCount",
"cache.cityWeatherCache.hitRate",
"cache.cityWeatherCache.loadCount",
"cache.cityWeatherCache.loadExceptionCount",
"cache.cityWeatherCache.loadSuccessCount",
"cache.cityWeatherCache.missCount",
"cache.cityWeatherCache.missRate",
"cache.cityWeatherCache.requestCount",
"http.server.requests",
"jvm.buffer.count",
"jvm.buffer.memory.used",
"jvm.buffer.total.capacity",
"jvm.classes.loaded",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"jvm.gc.max.data.size",
"jvm.gc.memory.allocated",
"jvm.gc.memory.promoted",
"jvm.gc.pause",
"jvm.memory.committed",
"jvm.memory.max",
"jvm.memory.used",
"jvm.threads.daemon",
"jvm.threads.live",
"jvm.threads.peak",
"jvm.threads.states",
"logback.events",
"process.cpu.usage",
"process.files.max",
"process.files.open",
"process.start.time",
"process.uptime",
"system.cpu.count",
"system.cpu.usage",
"system.load.average.1m",
"tomcat.sessions.active.current",
"tomcat.sessions.active.max",
"tomcat.sessions.alive.max",
"tomcat.sessions.created",
"tomcat.sessions.expired",
"tomcat.sessions.rejected"
]
}
詳情可打開 http://localhost:8080/actuator/metrics/cache.cityWeatherCache.hitCount
{
name: "cache.cityWeatherCache.hitCount",
description: null,
baseUnit: null,
measurements: [
{
statistic: "VALUE",
value: 3
}
],
availableTags: [ ]
}