使用Dropwizard度量标准监视和测量无功应用
在上一篇文章中,我們創(chuàng)建了一個(gè)簡(jiǎn)單的索引代碼,該代碼可以對(duì)ElasticSearch進(jìn)行數(shù)千個(gè)并發(fā)請(qǐng)求。 監(jiān)視系統(tǒng)性能的唯一方法是老式的日志記錄語(yǔ)句:
.window(Duration.ofSeconds(1)) .flatMap(Flux::count) .subscribe(winSize -> log.debug("Got {} responses in last second", winSize));很好,但是在生產(chǎn)系統(tǒng)上,我們寧愿有一些集中的監(jiān)視和圖表解決方案來(lái)收集各種指標(biāo)。 一旦在數(shù)千個(gè)實(shí)例中擁有數(shù)百個(gè)不同的應(yīng)用程序,這一點(diǎn)就變得尤為重要。 具有單個(gè)圖形儀表板,匯總??所有重要信息變得至關(guān)重要。 我們需要兩個(gè)組件來(lái)收集一些指標(biāo):
- 發(fā)布指標(biāo)
- 收集并可視化它們
使用Dropwizard指標(biāo)發(fā)布指標(biāo)
在Spring Boot 2中, Dropwizard指標(biāo)被千分尺取代。 本文使用前者,下一個(gè)將在實(shí)踐中顯示后者的解決方案。 為了利用Dropwizard指標(biāo),我們必須將MetricRegistry或特定指標(biāo)注入我們的業(yè)務(wù)類(lèi)別。
import com.codahale.metrics.Counter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;@Component @RequiredArgsConstructor class Indexer {private final PersonGenerator personGenerator;private final RestHighLevelClient client;private final Timer indexTimer;private final Counter indexConcurrent;private final Counter successes;private final Counter failures;public Indexer(PersonGenerator personGenerator, RestHighLevelClient client, MetricRegistry metricRegistry) {this.personGenerator = personGenerator;this.client = client;this.indexTimer = metricRegistry.timer(name("es", "index"));this.indexConcurrent = metricRegistry.counter(name("es", "concurrent"));this.successes = metricRegistry.counter(name("es", "successes"));this.failures = metricRegistry.counter(name("es", "failures"));}private Flux<IndexResponse> index(int count, int concurrency) {//....}}這么多樣板,以添加一些指標(biāo)!
- indexTimer測(cè)量索引請(qǐng)求的時(shí)間分布(平均值,中位數(shù)和各種百分位數(shù))
- indexConcurrent度量當(dāng)前有多少個(gè)待處理的請(qǐng)求(已發(fā)送請(qǐng)求,尚未收到響應(yīng)); 指標(biāo)隨時(shí)間上升和下降
- success和failures計(jì)算相應(yīng)的成功索引請(qǐng)求和失敗索引請(qǐng)求的總數(shù)
我們將在一秒鐘內(nèi)刪除樣板,但首先,讓我們看一下它在我們的業(yè)務(wù)代碼中的作用:
private Mono<IndexResponse> indexDocSwallowErrors(Doc doc) {return indexDoc(doc).doOnSuccess(response -> successes.inc()).doOnError(e -> log.error("Unable to index {}", doc, e)).doOnError(e -> failures.inc()).onErrorResume(e -> Mono.empty()); }每次請(qǐng)求完成時(shí),上述此輔助方法都會(huì)增加成功和失敗的次數(shù)。 而且,它記錄并吞下錯(cuò)誤,因此單個(gè)錯(cuò)誤或超時(shí)不會(huì)中斷整個(gè)導(dǎo)入過(guò)程。
private <T> Mono<T> countConcurrent(Mono<T> input) {return input.doOnSubscribe(s -> indexConcurrent.inc()).doOnTerminate(indexConcurrent::dec); }上面的另一種方法是在發(fā)送新請(qǐng)求時(shí)增加indexConcurrent指標(biāo),并在結(jié)果或錯(cuò)誤到達(dá)時(shí)將其遞減。 此指標(biāo)不斷上升和下降,顯示進(jìn)行中的請(qǐng)求數(shù)。
private <T> Mono<T> measure(Mono<T> input) {return Mono.fromCallable(indexTimer::time).flatMap(time ->input.doOnSuccess(x -> time.stop())); }最終的助手方法是最復(fù)雜的。 它測(cè)量編制索引的總時(shí)間,即發(fā)送請(qǐng)求和接收響應(yīng)之間的時(shí)間。 實(shí)際上,它非常通用,它只是計(jì)算訂閱任意Mono<T>到完成之間的總時(shí)間。 為什么看起來(lái)這么奇怪? 好吧,基本的Timer API非常簡(jiǎn)單
indexTimer.time(() -> someSlowCode())它只需要一個(gè)lambda表達(dá)式并測(cè)量調(diào)用它花費(fèi)了多長(zhǎng)時(shí)間。 另外,您可以創(chuàng)建一個(gè)小的Timer.Context對(duì)象,該對(duì)象可以記住創(chuàng)建時(shí)間。 當(dāng)您調(diào)用Context.stop()它將報(bào)告此度量:
final Timer.Context time = indexTimer.time(); someSlowCode(); time.stop();使用異步流,要困難得多。 任務(wù)的開(kāi)始(由預(yù)訂表示)和完成通常發(fā)生在代碼不同位置的線程邊界上。 我們可以做的是(懶惰地)創(chuàng)建一個(gè)新的Context對(duì)象(請(qǐng)參閱: fromCallable(indexTimer::time) ),并在包裝??的流完成時(shí),完成Context (請(qǐng)參閱: input.doOnSuccess(x -> time.stop() ))。這是您構(gòu)成所有這些方法的方式:
personGenerator.infinite().take(count).flatMap(doc -> countConcurrent(measure(indexDocSwallowErrors(doc))), concurrency);就是這樣,但是用這么多低級(jí)的度量收集細(xì)節(jié)污染業(yè)務(wù)代碼似乎很奇怪。 讓我們用專(zhuān)門(mén)的組件包裝這些指標(biāo):
@RequiredArgsConstructor class EsMetrics {private final Timer indexTimer;private final Counter indexConcurrent;private final Counter successes;private final Counter failures;void success() {successes.inc();}void failure() {failures.inc();}void concurrentStart() {indexConcurrent.inc();}void concurrentStop() {indexConcurrent.dec();}Timer.Context startTimer() {return indexTimer.time();}}現(xiàn)在,我們可以使用一些更高級(jí)的抽象:
class Indexer {private final EsMetrics esMetrics;private <T> Mono<T> countConcurrent(Mono<T> input) {return input.doOnSubscribe(s -> esMetrics.concurrentStart()).doOnTerminate(esMetrics::concurrentStop);}//...private Mono<IndexResponse> indexDocSwallowErrors(Doc doc) {return indexDoc(doc).doOnSuccess(response -> esMetrics.success()).doOnError(e -> log.error("Unable to index {}", doc, e)).doOnError(e -> esMetrics.failure()).onErrorResume(e -> Mono.empty());} }在下一篇文章中,我們將學(xué)習(xí)如何更好地組合所有這些方法。 并避免一些樣板。
發(fā)布和可視化指標(biāo)
僅僅收集指標(biāo)是不夠的。 我們必須定期發(fā)布匯總指標(biāo),以便其他系統(tǒng)可以使用,處理和可視化它們。 一種這樣的工具是Graphite和Grafana 。 但是,在開(kāi)始配置它們之前,讓我們首先將指標(biāo)發(fā)布到控制臺(tái)。 我發(fā)現(xiàn)在對(duì)度量進(jìn)行故障排除或開(kāi)發(fā)時(shí)特別有用。
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter;@Bean Slf4jReporter slf4jReporter(MetricRegistry metricRegistry) {final Slf4jReporter slf4jReporter = Slf4jReporter.forRegistry(metricRegistry.build();slf4jReporter.start(1, TimeUnit.SECONDS);return slf4jReporter; }這個(gè)簡(jiǎn)單的代碼片段采用現(xiàn)有的MetricRegistry并注冊(cè)Slf4jReporter 。 每秒您將看到所有度量標(biāo)準(zhǔn)被打印到日志中(Logback等):
type=COUNTER, name=es.concurrent, count=1 type=COUNTER, name=es.failures, count=0 type=COUNTER, name=es.successes, count=1653 type=TIMER, name=es.index, count=1653, min=1.104664, max=345.139385, mean=2.2166538118720576,stddev=11.208345077801448, median=1.455504, p75=1.660252, p95=2.7456, p98=5.625456, p99=9.69689, p999=85.062713,mean_rate=408.56403102372764, m1=0.0, m5=0.0, m15=0.0, rate_unit=events/second, duration_unit=milliseconds但這僅僅是為了解決問(wèn)題,為了將指標(biāo)發(fā)布到外部Graphite實(shí)例,我們需要一個(gè)GraphiteReporter :
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.graphite.Graphite; import com.codahale.metrics.graphite.GraphiteReporter;@Bean GraphiteReporter graphiteReporter(MetricRegistry metricRegistry) {final Graphite graphite = new Graphite(new InetSocketAddress("localhost", 2003));final GraphiteReporter reporter = GraphiteReporter.forRegistry(metricRegistry).prefixedWith("elastic-flux").convertRatesTo(TimeUnit.SECONDS).convertDurationsTo(TimeUnit.MILLISECONDS).build(graphite);reporter.start(1, TimeUnit.SECONDS);return reporter; }在這里,我向localhost:2003報(bào)告,其中帶有Graphite + Grafana的Docker鏡像恰好在其中。 每秒將所有度量標(biāo)準(zhǔn)發(fā)送到該地址。 我們稍后可以在Grafana上可視化所有這些指標(biāo):
上圖顯示了索引時(shí)間分布(從第50個(gè)百分位數(shù)到第99.9個(gè)百分位數(shù))。 使用此圖,您可以快速發(fā)現(xiàn)典型性能(P50)和(幾乎)最壞情況的性能(P99.9)。 對(duì)數(shù)標(biāo)度是不尋常的,但是在這種情況下,我們可以看到上下百分位。 底部圖更加有趣。 它結(jié)合了三個(gè)指標(biāo):
- 成功執(zhí)行索引操作的速率(每秒請(qǐng)求數(shù))
- 操作失敗率(紅色條,堆疊在綠色條上)
- 當(dāng)前并發(fā)級(jí)別(右軸):進(jìn)行中的請(qǐng)求數(shù)
此圖顯示了系統(tǒng)吞吐量(RPS),故障和并發(fā)性。 故障太多或并發(fā)級(jí)別異常高(許多操作正在等待響應(yīng))可能表明您的系統(tǒng)存在某些問(wèn)題。 儀表板定義在GitHub存儲(chǔ)庫(kù)中可用。
在下一篇文章中,我們將學(xué)習(xí)如何從Dropwizard指標(biāo)遷移到微米。 一個(gè)非常愉快的經(jīng)歷!
翻譯自: https://www.javacodegeeks.com/2018/01/monitoring-measuring-reactive-application-dropwizard-metrics.html
總結(jié)
以上是生活随笔為你收集整理的使用Dropwizard度量标准监视和测量无功应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Picocli 2.0:事半功倍
- 下一篇: 拆分为流