微服務(wù)緩存的使用度量

緩存也許是程序員心中最熟悉的性能優(yōu)化手段之一庸诱, 在舊文中 微服務(wù)緩存漫談之Guava CacheRedis 集群的構(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è)概念

  1. 穿透
    某條記錄壓根不存在感混,所以在緩存中找不到竹伸,每次都需要到數(shù)據(jù)庫中讀取泥栖,但是結(jié)果還是找不到。

常用的應(yīng)對方法是布隆過濾器(它的特點(diǎn)是)或者反向緩存(在緩存中保存這條記錄勋篓,標(biāo)識它是不存在的)

  1. 擊穿
    某條記錄過期被移除了吧享,恰好大量相關(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: [ ]
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者螟碎。
  • 序言:七十年代末眉菱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子掉分,更是在濱河造成了極大的恐慌俭缓,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酥郭,死亡現(xiàn)場離奇詭異华坦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)不从,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門惜姐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人椿息,你說我怎么就攤上這事歹袁。” “怎么了寝优?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵条舔,是天一觀的道長。 經(jīng)常有香客問我乏矾,道長孟抗,這世上最難降的妖魔是什么迁杨? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮夸浅,結(jié)果婚禮上仑最,老公的妹妹穿的比我還像新娘。我一直安慰自己帆喇,他們只是感情好警医,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坯钦,像睡著了一般预皇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婉刀,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天吟温,我揣著相機(jī)與錄音,去河邊找鬼突颊。 笑死鲁豪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的律秃。 我是一名探鬼主播爬橡,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼棒动!你這毒婦竟也來了糙申?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤船惨,失蹤者是張志新(化名)和其女友劉穎柜裸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粱锐,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疙挺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怜浅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衔统。...
    茶點(diǎn)故事閱讀 39,977評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖海雪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舱殿,我是刑警寧澤奥裸,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站沪袭,受9級特大地震影響湾宙,放射性物質(zhì)發(fā)生泄漏樟氢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一侠鳄、第九天 我趴在偏房一處隱蔽的房頂上張望埠啃。 院中可真熱鬧伟恶,春花似錦、人聲如沸潦牛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至母赵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痊银,已是汗流浹背施绎。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工谷醉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抖单。 一個(gè)月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓矛绘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親货矮。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評論 2 355