SpringBoot結(jié)合prometheus自定義埋點方式
快速入門
spring-actuator做度量統(tǒng)計收集,使用Prometheus(普羅米修斯)進行數(shù)據(jù)收集,Grafana(增強ui)進行數(shù)據(jù)展示,用于監(jiān)控生成環(huán)境機器的性能指標(biāo)和業(yè)務(wù)數(shù)據(jù)指標(biāo)。
一般,我們叫這樣的操作為”埋點”。SpringBoot中的依賴spring-actuator中集成的度量統(tǒng)計API使用的框架是Micrometer,官網(wǎng)是https://Micrometer.io。
Micrometer提供的度量類庫
Meter是指一組用于收集應(yīng)用中的度量數(shù)據(jù)的接口,Meter單詞可以翻譯為”米”或者”千分尺”,但是顯然聽起來都不是很合理,因此下文直接叫Meter,理解它為度量接口即可。
Meter是由MeterRegistry創(chuàng)建和保存的,可以理解MeterRegistry是Meter的工廠和緩存中心,一般而言每個JVM應(yīng)用在使用Micrometer的時候必須創(chuàng)建一個MeterRegistry的具體實現(xiàn)。
Micrometer中,Meter的具體類型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。
下面分節(jié)詳細介紹這些類型的使用方法和實戰(zhàn)使用場景。而一個Meter具體類型需要通過名字和Tag(這里指的是Micrometer提供的Tag接口)作為它的唯一標(biāo)識,這樣做的好處是可以使用名字進行標(biāo)記,通過不同的Tag去區(qū)分多種維度進行數(shù)據(jù)統(tǒng)計。
MeterRegistry
MeterRegistry在Micrometer是一個抽象類,主要實現(xiàn)包括:
SimpleMeterRegistry
:每個Meter的最新數(shù)據(jù)可以收集到SimpleMeterRegistry實例中,但是這些數(shù)據(jù)不會發(fā)布到其他系統(tǒng),也就是數(shù)據(jù)是位于應(yīng)用的內(nèi)存中的。CompositeMeterRegistry
:多個MeterRegistry聚合,內(nèi)部維護了一個MeterRegistry的列表。- 全局的
MeterRegistry
:工廠類io.micrometer.core.instrument.Metrics中持有一個靜態(tài)final的CompositeMeterRegistry實例globalRegistry。
當(dāng)然,使用者也可以自行繼承MeterRegistry去實現(xiàn)自定義的MeterRegistry。
SimpleMeterRegistry適合做調(diào)試的時候使用,它的簡單使用方式如下:
MeterRegistry registry = new SimpleMeterRegistry(); Counter counter = registry.counter("counter"); counter.increment();
CompositeMeterRegistry實例初始化的時候,內(nèi)部持有的MeterRegistry列表是空的,如果此時用它新增一個Meter實例,Meter實例的操作是無效的
CompositeMeterRegistry composite = new CompositeMeterRegistry(); Counter compositeCounter = composite.counter("counter"); compositeCounter.increment(); // <- 實際上這一步操作是無效的,但是不會報錯 SimpleMeterRegistry simple = new SimpleMeterRegistry(); composite.add(simple); // <- 向CompositeMeterRegistry實例中添加SimpleMeterRegistry實例 compositeCounter.increment(); // <-計數(shù)成功
全局的MeterRegistry的使用方式更加簡單便捷,因為一切只需要操作工廠類Metrics的靜態(tài)方法:
Metrics.addRegistry(new SimpleMeterRegistry()); Counter counter = Metrics.counter("counter", "tag-1", "tag-2"); counter.increment();
Tag與Meter的命名
Micrometer中,Meter的命名約定使用英文逗號(dot,也就是”.”)分隔單詞。但是對于不同的監(jiān)控系統(tǒng),對命名的規(guī)約可能并不相同,如果命名規(guī)約不一致,在做監(jiān)控系統(tǒng)遷移或者切換的時候,可能會對新的系統(tǒng)造成破壞。
Micrometer中使用英文逗號分隔單詞的命名規(guī)則,再通過底層的命名轉(zhuǎn)換接口NamingConvention進行轉(zhuǎn)換,最終可以適配不同的監(jiān)控系統(tǒng),同時可以消除監(jiān)控系統(tǒng)不允許的特殊字符的名稱和標(biāo)記等。開發(fā)者也可以覆蓋NamingConvention實現(xiàn)自定義的命名轉(zhuǎn)換規(guī)則:
registry.config().namingConvention(myCustomNamingConvention);
在Micrometer中,對一些主流的監(jiān)控系統(tǒng)或者存儲系統(tǒng)的命名規(guī)則提供了默認(rèn)的轉(zhuǎn)換方式,例如當(dāng)我們使用下面的命名時候:
MeterRegistry registry = ... registry.timer("http.server.requests");
對于不同的監(jiān)控系統(tǒng)或者存儲系統(tǒng),命名會自動轉(zhuǎn)換如下:
- Prometheus - http_server_requests_duration_seconds。
- Atlas - httpServerRequests。
- Graphite - http.server.requests。
- InfluxDB - http_server_requests。
其實NamingConvention已經(jīng)提供了5種默認(rèn)的轉(zhuǎn)換規(guī)則:dot、snakeCase、camelCase、upperCamelCase和slashes。
另外:
Tag(標(biāo)簽)是Micrometer的一個重要的功能,嚴(yán)格來說,一個度量框架只有實現(xiàn)了標(biāo)簽的功能,才能真正地多維度進行度量數(shù)據(jù)收集。
Tag的命名一般需要是有意義的,所謂有意義就是可以根據(jù)Tag的命名可以推斷出它指向的數(shù)據(jù)到底代表什么維度或者什么類型的度量指標(biāo)。
假設(shè)我們需要監(jiān)控數(shù)據(jù)庫的調(diào)用和Http請求調(diào)用統(tǒng)計,一般推薦的做法是:
MeterRegistry registry = ... registry.counter("database.calls", "db", "users") registry.counter("http.requests", "uri", "/api/users")
有兩點需要注意:
1、Tag的值必須不為null。
2、Micrometer中,Tag必須成對出現(xiàn),也就是Tag必須設(shè)置為偶數(shù)個,實際上它們以Key=Value
的形式存在,具體可以看io.micrometer.core.instrument.Tag
接口。
Meter的命名和Meter的Tag相互結(jié)合,以命名為軸心,以Tag為多維度要素,可以使度量數(shù)據(jù)的維度更加豐富,便于統(tǒng)計和分析。
Meters
前面提到Meter主要包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。
下面逐一分析它們的作用和個人理解的實際使用場景(應(yīng)該說是生產(chǎn)環(huán)境)。
Counter
Counter是一種比較簡單的Meter,它是一種單值的度量類型,或者說是一個單值計數(shù)器。Counter接口允許使用者使用一個固定值(必須為正數(shù))進行計數(shù)。
準(zhǔn)確來說:Counter就是一個增量為正數(shù)的單值計數(shù)器。
使用場景:
Counter的作用是記錄XXX的總量或者計數(shù)值,適用于一些增長類型的統(tǒng)計,例如下單、支付次數(shù)、Http請求總量記錄等等,通過Tag可以區(qū)分不同的場景,對于下單,可以使用不同的Tag標(biāo)記不同的業(yè)務(wù)來源或者是按日期劃分,對于Http請求總量記錄,可以使用Tag區(qū)分不同的URL。【增長無上限】
Timer
Timer(計時器)適用于記錄耗時比較短的事件的執(zhí)行時間,通過時間分布展示事件的序列和發(fā)生頻率。所有的Timer的實現(xiàn)至少記錄了發(fā)生的事件的數(shù)量和這些事件的總耗時,從而生成一個時間序列。
Timer的基本單位基于服務(wù)端的指標(biāo)而定,但是實際上我們不需要過于關(guān)注Timer的基本單位,因為Micrometer在存儲生成的時間序列的時候會自動選擇適當(dāng)?shù)幕締挝弧?/p>
比較常用和方便的方法是幾個函數(shù)式接口入?yún)⒌姆椒ǎ?/p>
Timer timer = ... timer.record(() -> dontCareAboutReturnValue()); timer.recordCallable(() -> returnValue()); Runnable r = timer.wrap(() -> dontCareAboutReturnValue()); Callable c = timer.wrap(() -> returnValue());
使用場景:
根據(jù)個人經(jīng)驗和實踐,總結(jié)如下:
- 記錄指定方法的執(zhí)行時間用于展示。
- 記錄一些任務(wù)的執(zhí)行時間,從而確定某些數(shù)據(jù)來源的速率,例如消息隊列消息的消費速率等。
這里舉個實際的例子,要對系統(tǒng)做一個功能,記錄指定方法的執(zhí)行時間,還是用下單方法做例子:
Metrics.addRegistry(new SimpleMeterRegistry()); Timer timer = Metrics.timer("timer", "createOrder", "cost"); timer.record(() -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("下單成功了。。。。。"); } );
在實際生產(chǎn)環(huán)境中,可以通過spring-aop把記錄方法耗時的邏輯抽象到一個切面中,這樣就能減少不必要的冗余的模板代碼。
另外,Timer的使用還可以基于它的內(nèi)部類Timer.Sample,通過start和stop兩個方法記錄兩者之間的邏輯的執(zhí)行耗時。例如:
SimpleMeterRegistry registry = new SimpleMeterRegistry(); Timer.Sample sample = Timer.start(registry); // 業(yè)務(wù)邏輯處理 sample.stop(registry.timer("my.timer", "response", "200"));
Gauge
Gauge(儀表)是獲取當(dāng)前度量記錄值的句柄,也就是它表示一個可以任意上下浮動的單數(shù)值度量Meter。Gauge通常用于變動的測量值,測量值用ToDoubleFunction參數(shù)的返回值設(shè)置,如當(dāng)前的內(nèi)存使用情況,同時也可以測量上下移動的”計數(shù)”,比如隊列中的消息數(shù)量。
官網(wǎng)文檔中提到Gauge的典型使用場景是用于測量集合或映射的大小或運行狀態(tài)中的線程數(shù)。Gauge一般用于監(jiān)測有自然上界的事件或者任務(wù),而Counter一般使用于無自然上界的事件或者任務(wù)的監(jiān)測,所以像Http請求總量計數(shù)應(yīng)該使用Counter而非Gauge。
使用場景:
根據(jù)個人經(jīng)驗和實踐,總結(jié)如下:
- 有自然(物理)上界的浮動值的監(jiān)測,例如物理內(nèi)存、集合、映射、數(shù)值等。
- 有邏輯上界的浮動值的監(jiān)測,例如積壓的消息、(線程池中)積壓的任務(wù)等,其實本質(zhì)也是集合或者映射的監(jiān)測。
DistributionSummary
Summary(摘要)主要用于跟蹤事件的分布,在Micrometer中,對應(yīng)的類是DistributionSummary(分發(fā)摘要)。它的使用方式和Timer十分相似,但是它的記錄值并不依賴于時間單位。
使用場景:
根據(jù)個人經(jīng)驗和實踐,總結(jié)如下:
1、不依賴于時間單位的記錄值的測量,例如服務(wù)器有效負載值,緩存的命中率等。
直方圖和百分?jǐn)?shù)配置
直方圖和百分?jǐn)?shù)配置適用于Summary和Timer,這部分相對復(fù)雜,等研究透了再補充。
Histogram類型總共上報5種數(shù)據(jù):count、sum,最大值、最小值、bucket
平均耗時可以通過 sum/count來計算
所以,用Histogram類型,可以一個指標(biāo)多個維度來使用。
完整的類型介紹:Metric Types | client_java
基于SpirngBoot、Prometheus、Grafana集成
集成了Micrometer框架的JVM應(yīng)用使用到Micrometer的API收集的度量數(shù)據(jù)位于內(nèi)存之中,因此,需要額外的存儲系統(tǒng)去存儲這些度量數(shù)據(jù),需要有監(jiān)控系統(tǒng)負責(zé)統(tǒng)一收集和處理這些數(shù)據(jù),還需要有一些UI工具去展示數(shù)據(jù),一般大佬只喜歡看炫酷的圖表或者動畫。
常見的存儲系統(tǒng)就是時序數(shù)據(jù)庫,主流的有Influx、Datadog等。比較主流的監(jiān)控系統(tǒng)(主要是用于數(shù)據(jù)收集和處理)就是Prometheus(一般叫普羅米修斯,下面就這樣叫吧)。而展示的UI目前相對用得比較多的就是Grafana。
另外,Prometheus已經(jīng)內(nèi)置了一個時序數(shù)據(jù)庫的實現(xiàn),因此,在做一套相對完善的度量數(shù)據(jù)監(jiān)控的系統(tǒng)只需要依賴目標(biāo)JVM應(yīng)用,Prometheus組件和Grafana組件即可。
SpirngBoot中使用Micrometer
SpringBoot中的spring-boot-starter-actuator依賴已經(jīng)集成了對Micrometer的支持,其中的metrics端點的很多功能就是通過Micrometer實現(xiàn)的,prometheus端點默認(rèn)也是開啟支持的,實際上actuator依賴的spring-boot-actuator-autoconfigure中集成了對很多框架的開箱即用的API。
其中prometheus包中集成了對Prometheus的支持,使得使用了actuator可以輕易地讓項目暴露出prometheus端點,作為Prometheus收集數(shù)據(jù)的客戶端,Prometheus(服務(wù)端軟件)可以通過此端點收集應(yīng)用中Micrometer的度量數(shù)據(jù)。
我們先引入spring-boot-starter-actuator和spring-boot-starter-web,實現(xiàn)一個Counter和Timer作為示例。依賴:
<!-- Micrometer core dependecy --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
注意多看spring官方文檔關(guān)于Actuator的詳細描述,在SpringBoot-2.x之后,配置Web端點暴露的權(quán)限控制和1.x有很大的不同。
總結(jié)一下就是:除了shutdown端點之外,其他端點默認(rèn)都是開啟支持的這里僅僅是開啟支持,并不是暴露為Web端點,端點必須暴露為Web端點才能被訪問,禁用或者開啟端點支持的配置方式如下:
management.endpoint.${端點ID}.enabled=true/false可以查
暴露監(jiān)控端點為Web端點的配置是:
management.endpoints.web.exposure.include=info,health,metrics,prometheus
management.endpoints.web.exposure.exclude
用于指定不暴露為Web端點的監(jiān)控端點,指定多個的時候用英文逗號分隔
management.endpoints.web.exposure.include
默認(rèn)指定的只有info和health兩個端點,我們可以直接指定暴露所有的端點:management.endpoints.web.exposure.include=*
,如果采用YAML配置,記得要加單引號’‘。
暴露所有Web監(jiān)控端點是一件比較危險的事情,如果需要在生產(chǎn)環(huán)境這樣做,請務(wù)必先確認(rèn)http://{host}:{management.port}
不能通過公網(wǎng)訪問(也就是監(jiān)控端點訪問的端口只能通過內(nèi)網(wǎng)訪問,這樣可以方便后面說到的Prometheus服務(wù)端通過此端口收集數(shù)據(jù))。
由于引入了springboot-actuator依賴,會在spring容器啟動時自動注入prometheus Registry實例
這樣就可以在spring boot框架中直接使用上報API了,如
Metrics.counter("order.count", "order.channel", "huawei").increment(); Timer timer = Metrics.timer("method.cost.time", "method.name", "hello"); timer.record(3, TimeUnit.SECONDS);
最好參考一下Prometheus的官方文檔,稍微學(xué)習(xí)一下它的查詢語言PromQL的使用方式,一個面板可以支持多個PromQL查詢。
提供一個例子:
public final class MeterRegistryCenter { private static final Logger logger = LoggerFactory.getLogger(MeterRegistryCenter.class); private static CompositeMeterRegistry METER_REGISTRY = null; static { try { METER_REGISTRY = Metrics.globalRegistry; } catch (Throwable t) { logger.warn("Metrics init failed :", t); } } public static Counter counter(String name, Iterable<Tag> tags) { if (METER_REGISTRY != null) { return METER_REGISTRY.counter(name, tags); } return null; } public static Counter counter(String name, String... tags) { if (METER_REGISTRY != null) { return METER_REGISTRY.counter(name, tags); } return null; } public static <T extends Number> T gauge(String name, Iterable<Tag> tags, T number) { if (METER_REGISTRY != null) { return METER_REGISTRY.gauge(name, tags, number); } return null; } public static Timer timer(String name, Iterable<Tag> tags) { if (METER_REGISTRY != null) { return METER_REGISTRY.timer(name, tags); } return null; } public static Timer timer(String name, String... tags) { if (METER_REGISTRY != null) { return METER_REGISTRY.timer(name, tags); } return null; } public static DistributionSummary summary(String name, Iterable<Tag> tags) { if (METER_REGISTRY != null) { return METER_REGISTRY.summary(name, tags); } return null; } public static DistributionSummary summary(String name, String... tags) { if (METER_REGISTRY != null) { return METER_REGISTRY.summary(name, tags); } return null; } }
public final class MetricsMonitor { private static final Logger logger = LoggerFactory.getLogger(MetricsMonitor.class); private static final String DEFAULT_APP_ID = "unknown"; private static final String DEFAULT_SDK_LANG = "unknown"; private static final String DEFAULT_IDC = "unknown"; private static final String DEFAULT_URL_PATH = "unknown"; private static final String APOLLO_QUERY_CONFIG_SOURCE_METER = "apollo_query_config_source"; private static final String APOLLO_URL_REQUEST_METER = "apollo_url_request"; public static Counter getQueryConfigSourceCounter(String appId, String sdkLang, String idc) { if (StringUtils.isBlank(appId)) { appId = DEFAULT_APP_ID; } if (StringUtils.isBlank(sdkLang)) { sdkLang = DEFAULT_SDK_LANG; } if (StringUtils.isBlank(idc)) { idc = DEFAULT_IDC; } return ApolloMeterRegistryCenter.counter(APOLLO_QUERY_CONFIG_SOURCE_METER, "appId", appId, "sdkLang", sdkLang, "idc", idc); } public static Counter getUrlRequestCounter(String url) { if (StringUtils.isBlank(url)) { url = DEFAULT_URL_PATH; } return ApolloMeterRegistryCenter.counter(APOLLO_URL_REQUEST_METER, "url", url); } public static void recordQueryConfigSource(String appId, String sdkLang, String idc) { try { Counter counter = getQueryConfigSourceCounter(appId, sdkLang, idc); if (!Objects.isNull(counter)) { counter.increment(); } } catch (Throwable t) { logger.warn("recordQueryConfigSource failed,msg: " + t.getMessage()); } } public static void recordUrlRequest(String url) { try { Counter counter = getUrlRequestCounter(url); if (!Objects.isNull(counter)) { counter.increment(); } } catch (Throwable t) { logger.warn("recordUrlRequest failed,msg: " + t.getMessage()); } } }
使用push Gateway方式上報數(shù)據(jù)
Prometheus是一款開源的監(jiān)控和報警系統(tǒng),而Pushgateway是Prometheus的一個組件,用于接收短期的指標(biāo)數(shù)據(jù)。
在某些情況下,我們可能需要通過Java代碼將指標(biāo)數(shù)據(jù)推送到Prometheus Pushgateway中。
github: GitHub - prometheus/client_java: Prometheus instrumentation library for JVM applications
各種模式的接入方式文檔地址:Quickstart | client_java
一個例子:
1、maven引入依賴
<dependency> <groupId>io.prometheus</groupId> <artifactId>prometheus-metrics-core</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>io.prometheus</groupId> <artifactId>prometheus-metrics-exporter-pushgateway</artifactId> <version>1.3.0</version> </dependency>
2、編寫service
import io.prometheus.metrics.core.metrics.Counter; import io.prometheus.metrics.exporter.pushgateway.PushGateway; import io.prometheus.metrics.model.registry.PrometheusRegistry; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Slf4j @Service public class MeterService implements InitializingBean { @Value("${metrics.pushgateway.address:127.0.0.1:9091}") private String prometheusPushGateWayAddress; private static final String PROMETHEUS_JOB_NAME = "test_metrics"; private PushGateway prometheusPushGateWay; private final ScheduledExecutorService reportSchedule = Executors.newSingleThreadScheduledExecutor(); private Counter testCounter; @Override public void afterPropertiesSet() throws Exception { // 1.定義push Gateway PrometheusRegistry registry = new PrometheusRegistry(); prometheusPushGateWay = PushGateway.builder() .address(prometheusPushGateWayAddress) .job(PROMETHEUS_JOB_NAME) .registry(registry) .build(); // 2.定義各種業(yè)務(wù)指標(biāo) testCounter = Counter.builder().name("test_01_metrics_total").labelNames("region", "code").register(registry); // 3.定期推送數(shù)據(jù)到prometheus server reportSchedule.scheduleAtFixedRate(() -> { try { if (!Objects.isNull(prometheusPushGateWay)) { prometheusPushGateWay.pushAdd(); log.info("push data success"); } } catch (Exception e) { log.error("pushGateway push failed!", e); } }, 60, 60, TimeUnit.SECONDS); } public void report(String region, String code) { testCounter.labelValues(region, code).inc(); } }
3、在業(yè)務(wù)埋點調(diào)用即可。數(shù)據(jù)將會上報到prometheus,然后通過配置grafana報表即可。
特別注意:使用push Gateway方式上報數(shù)據(jù),tag一定要加上機器的IP,否則服務(wù)端區(qū)分不出是哪個機器的上報,導(dǎo)致數(shù)據(jù)不準(zhǔn)。
維度問題,注意不要發(fā)散,如果發(fā)散的影響有:
- 導(dǎo)致promethues服務(wù)端的cpu、mem等都會有一定的壓力
- grafana查詢會變慢,導(dǎo)致有時候結(jié)果查詢不出來
- 程序調(diào)用pushAdd函數(shù)也有出現(xiàn)超時(數(shù)據(jù)量太多)
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java編程實現(xiàn)根據(jù)EXCEL列名求其索引的方法
這篇文章主要介紹了java編程實現(xiàn)根據(jù)EXCEL列名求其索引的方法,涉及Java元素遍歷與數(shù)學(xué)運算的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11