基於Prometheus搭建SpringCloud全方位立體監控體系


前提

最近公司在聯合運維做一套全方位監控的系統,應用集群的技術棧是SpringCloud體系。雖然本人沒有參與具體基礎架構的研發,但是從應用引入的包和一些資料的查閱大致推算出具體的實現方案,這里做一次推演,詳細記錄一下整個搭建過程。

Prometheus是什么

Prometheus(普羅米修斯,官網是https://prometheus.io/),是一個開源的系統監控和告警的工具包,其采用Pull方式采集時間序列的度量數據,通過Http協議傳輸。它的工作方式是被監控的服務需要公開一個Prometheus端點,這端點是一個HTTP接口,該接口公開了度量的列表和當前的值,然后Prometheus應用從此接口定時拉取數據,一般可以存放在時序數據庫中,然后通過可視化的Dashboard(例如Promdash或者Grafana)進行數據展示。當然,此文章不打算深入研究這個工具,只做應用層面的展示。這篇文章將會用到下面幾個技術棧:

  • SpringCloud體系,主要是注冊中心和注冊客戶端。
  • spring-boot-starter-actuator,主要是提供了Prometheus端點,不用重復造輪子。
  • Prometheus的Java客戶端。
  • Prometheus應用。
  • io.micrometer,SpringBoot標准下使用的度量工具包。
  • Grafana,可視化的Dashboard。

這里所有的軟件或者依賴全部使用當前的最新版本,如果有坑踩了再填。其實,Prometheus本身也開發了一套Counter、Gauge、Timer等相關接口,不過SpringBoot中使用了io.micrometer中的套件,所以本文不深入分析Prometheus的Java客戶端。

io.micrometer的使用

在SpringBoot2.X中,spring-boot-starter-actuator引入了io.micrometer,對1.X中的metrics進行了重構,主要特點是支持tag/label,配合支持tag/label的監控系統,使得我們可以更加方便地對metrics進行多維度的統計查詢及監控。io.micrometer目前支持Counter、Gauge、Timer、Summary等多種不同類型的度量方式(不知道有沒有遺漏),下面逐個簡單分析一下它們的作用和使用方式。 需要在SpringBoot項目下引入下面的依賴:

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-core</artifactId>
  <version>${micrometer.version}</version>
</dependency>

目前最新的micrometer.version為1.0.5。注意一點的是:io.micrometer支持Tag(標簽)的概念,Tag是其metrics是否能夠有多維度的支持的基礎,Tag必須成對出現,也就是必須配置也就是偶數個Tag,有點類似於K-V的關系。

Counter

Counter(計數器)簡單理解就是一種只增不減的計數器。它通常用於記錄服務的請求數量、完成的任務數量、錯誤的發生數量等等。舉個例子:

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/7/19 23:10
 */
public class CounterSample {

	public static void main(String[] args) throws Exception {
		//tag必須成對出現,也就是偶數個
		Counter counter = Counter.builder("counter")
				.tag("counter", "counter")
				.description("counter")
				.register(new SimpleMeterRegistry());
		counter.increment();
		counter.increment(2D);
		System.out.println(counter.count());
		System.out.println(counter.measure());
		//全局靜態方法
		Metrics.addRegistry(new SimpleMeterRegistry());
		counter = Metrics.counter("counter", "counter", "counter");
		counter.increment(10086D);
		counter.increment(10087D);
		System.out.println(counter.count());
		System.out.println(counter.measure());
	}
}

輸出:

3.0
[Measurement{statistic='COUNT', value=3.0}]
20173.0
[Measurement{statistic='COUNT', value=20173.0}]

Counter的Measurement的statistic(可以理解為度量的統計角度)只有COUNT,也就是它只具備計數(它只有增量的方法,因此只增不減),這一點從它的接口定義可知:

public interface Counter extends Meter {

  default void increment() {
        increment(1.0);
  }

  void increment(double amount);

  double count();

  //忽略其他方法或者成員
}

Counter還有一個衍生類型FunctionCounter,它是基於函數式接口ToDoubleFunction進行計數統計的,用法差不多。

Gauge

Gauge(儀表)是一個表示單個數值的度量,它可以表示任意地上下移動的數值測量。Gauge通常用於變動的測量值,如當前的內存使用情況,同時也可以測量上下移動的"計數",比如隊列中的消息數量。舉個例子:

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/7/19 23:30
 */
public class GaugeSample {

	public static void main(String[] args) throws Exception {
		AtomicInteger atomicInteger = new AtomicInteger();
		Gauge gauge = Gauge.builder("gauge", atomicInteger, AtomicInteger::get)
				.tag("gauge", "gauge")
				.description("gauge")
				.register(new SimpleMeterRegistry());
		atomicInteger.addAndGet(5);
		System.out.println(gauge.value());
		System.out.println(gauge.measure());
		atomicInteger.decrementAndGet();
		System.out.println(gauge.value());
		System.out.println(gauge.measure());
		//全局靜態方法,返回值竟然是依賴值,有點奇怪,暫時不選用
		Metrics.addRegistry(new SimpleMeterRegistry());
		AtomicInteger other = Metrics.gauge("gauge", atomicInteger, AtomicInteger::get);
	}
}

輸出結果:

5.0
[Measurement{statistic='VALUE', value=5.0}]
4.0
[Measurement{statistic='VALUE', value=4.0}]

Gauge關注的度量統計角度是VALUE(值),它的構建方法中依賴於函數式接口ToDoubleFunction的實例(如例子中的實例方法引用AtomicInteger::get)和一個依賴於ToDoubleFunction改變自身值的實例(如例子中的AtomicInteger實例),它的接口方法如下:

public interface Gauge extends Meter {

  double value();

  //忽略其他方法或者成員
}

Timer

Timer(計時器)同時測量一個特定的代碼邏輯塊的調用(執行)速度和它的時間分布。簡單來說,就是在調用結束的時間點記錄整個調用塊執行的總時間,適用於測量短時間執行的事件的耗時分布,例如消息隊列消息的消費速率。舉個例子:

import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

import java.util.concurrent.TimeUnit;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/7/19 23:44
 */
public class TimerSample {

	public static void main(String[] args) throws Exception{
		Timer timer = Timer.builder("timer")
				.tag("timer","timer")
				.description("timer")
				.register(new SimpleMeterRegistry());
		timer.record(()->{
			try {
				TimeUnit.SECONDS.sleep(2);
			}catch (InterruptedException e){
				//ignore
			}
		});
		System.out.println(timer.count());
		System.out.println(timer.measure());
		System.out.println(timer.totalTime(TimeUnit.SECONDS));
		System.out.println(timer.mean(TimeUnit.SECONDS));
		System.out.println(timer.max(TimeUnit.SECONDS));
	}
}

輸出結果:

1
[Measurement{statistic='COUNT', value=1.0}, Measurement{statistic='TOTAL_TIME', value=2.000603975}, Measurement{statistic='MAX', value=2.000603975}]
2.000603975
2.000603975
2.000603975

Timer的度量統計角度主要包括記錄執行的最大時間、總時間、平均時間、執行完成的總任務數,它提供多種的統計方法變體:

public interface Timer extends Meter, HistogramSupport {

  void record(long amount, TimeUnit unit);

  default void record(Duration duration) {
      record(duration.toNanos(), TimeUnit.NANOSECONDS);
  }

  <T> T record(Supplier<T> f);
    
  <T> T recordCallable(Callable<T> f) throws Exception;

  void record(Runnable f);

  default Runnable wrap(Runnable f) {
      return () -> record(f);
  }

  default <T> Callable<T> wrap(Callable<T> f) {
    return () -> recordCallable(f);
  }

  //忽略其他方法或者成員
}

這些record或者包裝方法可以根據需要選擇合適的使用,另外,一些度量屬性(如下限和上限)或者單位可以自行配置,具體屬性的相關內容可以查看DistributionStatisticConfig類,這里不詳細展開。

另外,Timer有一個衍生類LongTaskTimer,主要是用來記錄正在執行但是尚未完成的任務數,用法差不多。

Summary

Summary(摘要)用於跟蹤事件的分布。它類似於一個計時器,但更一般的情況是,它的大小並不一定是一段時間的測量值。在micrometer中,對應的類是DistributionSummary,它的用法有點像Timer,但是記錄的值是需要直接指定,而不是通過測量一個任務的執行時間。舉個例子:


import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/7/19 23:55
 */
public class SummarySample {

	public static void main(String[] args) throws Exception {
		DistributionSummary summary = DistributionSummary.builder("summary")
				.tag("summary", "summary")
				.description("summary")
				.register(new SimpleMeterRegistry());
		summary.record(2D);
		summary.record(3D);
		summary.record(4D);
		System.out.println(summary.measure());
		System.out.println(summary.count());
		System.out.println(summary.max());
		System.out.println(summary.mean());
		System.out.println(summary.totalAmount());
	}
}

輸出結果:

[Measurement{statistic='COUNT', value=3.0}, Measurement{statistic='TOTAL', value=9.0}, Measurement{statistic='MAX', value=4.0}]
3
4.0
3.0
9.0

Summary的度量統計角度主要包括記錄過的數據中的最大值、總數值、平均值和總次數。另外,一些度量屬性(如下限和上限)或者單位可以自行配置,具體屬性的相關內容可以查看DistributionStatisticConfig類,這里不詳細展開。

小結

一般情況下,上面的Counter、Gauge、Timer、DistributionSummary例子可以滿足日常開發需要,但是有些高級的特性這里沒有展開,具體可以參考micrometer-spring-legacy這個依賴包,畢竟源碼是老師,源碼不會騙人。

spring-boot-starter-actuator的使用

spring-boot-starter-actuator在2.X版本中不僅升級了metrics為io.micrometer,很多配置方式也和1.X完全不同,鑒於前段時間沒有維護SpringBoot技術棧的項目,現在重新看了下官網復習一下。引入依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>${springboot.version}</version>
</dependency>

目前最新的springboot.version為2.0.3.RELEASE。在spring-boot-starter-actuator中,最大的變化就是配置的變化,原來在1.X版本是通過management.security.enabled控制是否可以忽略權限訪問所有的監控端點,在2.X版本中,必須顯式配置不需要權限驗證對外開放的端點:

management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=env,beans
management.endpoints.jmx.exposure.include=
management.endpoints.jmx.exposure.include=*

例如上面的配置,訪問非/env和非/beans的端點,可以不受權限控制,也就是所有人都可以訪問非/env和非/beans的端點。例如,如果我只想暴露/health端點,只需配置:

management.endpoints.web.exposure.include=health

這一點需要特別注意,其他使用和1.X差不多。還有一點是,2.X中所有監控端點的訪問url的默認路徑前綴為:http://${host}/${port}/actuator/,也就是想訪問health端點就要訪問http://${host}/${port}/actuator/health,當然也可以修改/actuator這個路徑前綴。其他細節區別沒有深入研究,可以參考文檔

搭建SpringCloud應用

接着先搭建一個SpringCloud應用群,主要包括注冊中心(registry-center)和一個簡單的服務節點(cloud-prometheus-sample),其中注冊中心只引入eureka-server的依賴,而服務節點用於對接Prometheus,引入eureka-client、spring-boot-starter-actuator、prometheus等依賴。

registry-center

registry-center是一個單純的服務注冊中心,只需要引入eureka-server的依賴,添加一個啟動類即可,添加的依賴如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

添加一個啟動類:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/7/21 9:06
 */
@SpringBootApplication
@EnableEurekaServer
public class RegistryCenterApplication {

	public static void main(String[] args) {
		SpringApplication.run(RegistryCenterApplication.class, args);
	}
}

配置文件application.yaml如下:

server:
  port: 9091
spring:
  application:
    name: registry-center
eureka:
  instance:
    hostname: localhost
  client:
    enabled: true
    register-with-eureka: false
    fetch-registry: false
    service-url:
         defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

就是這么簡單,啟動入口類即可,啟動的端口為9091。

cloud-prometheus-sample

cloud-prometheus-sample主要作為eureka-client,接入spring-boot-starter-actuator和prometheus依賴,引入依賴如下:

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

這里引入的是micrometer-registry-prometheus而不是micrometer-spring-legacy是因為micrometer-spring-legacyspring-integration(spring系統集成)的依賴,這里沒有用到,但是里面很多實現可以參考。micrometer-registry-prometheus提供了基於actuator的端點,路徑是../prometheus。啟動類如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author throwable
 * @version v1.0
 * @description
 * @since 2018/7/21 9:13
 */
@SpringBootApplication
@EnableEurekaClient
public class SampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SampleApplication.class, args);
	}
}

配置文件application.yaml如下:

server:
  port: 9092
spring:
  application:
    name: cloud-prometheus-sample
eureka:
  instance:
    hostname: localhost
  client:
    service-url: http://localhost:9091/eureka/

啟動端口為9092,eureka的服務注冊地址為:http://localhost:9091/eureka/,也就是registry-center中指定的默認數據區(defaultZone)的注冊地址,先啟動registry-center,再啟動cloud-prometheus-sample,然后訪問http://localhost:9091/:

sp-p-1

訪問http://localhost:9092/actuator/prometheus:

sp-p-2

這些數據就是實時的度量數據,Prometheus(軟件)配置好任務並且啟動執行后,就是通過定時拉取/prometheus這個端點返回的數據進行數據聚合和展示的。

接着,我們先定制一個功能,統計cloud-prometheus-sample所有入站的Http請求數量(包括成功、失敗和非法的),添加如下代碼:

//請求攔截器
@Component
public class SampleMvcInterceptor extends HandlerInterceptorAdapter {

	private static final Counter COUNTER = Counter.builder("Http請求統計")
			.tag("HttpCount", "HttpCount")
			.description("Http請求統計")
			.register(Metrics.globalRegistry);

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
								Object handler, Exception ex) throws Exception {
		COUNTER.increment();
	}
}
//自定義Mvc配置
@Component
public class SampleWebMvcConfigurer implements WebMvcConfigurer {

	@Autowired
	private SampleMvcInterceptor sampleMvcInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(sampleMvcInterceptor);
	}
}

重啟cloud-prometheus-sample,直接訪問幾次不存在的根節點路徑http://localhost:9092/,再查看端點統計數據:

sp-p-3

安裝和使用Prometheus

先從Prometheus官方下載地址下載軟件,這里用Windows10平台演示,直接下載prometheus-2.3.2.windows-amd64.tar.gz,個人有軟件潔癖,用軟件或者依賴喜歡最高版本,出現坑了自己填。解壓后目錄如下:

sp-p-4

啟動的話,直接運行prometheus.exe即可,這里先配置一下prometheus.yml:

global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093
scrape_configs:
  - job_name: 'prometheus'
    metrics_path: /actuator/prometheus
    static_configs:
    - targets: ['localhost:9092']

我們主要修改的是scrape_configs節點下的配置,這個節點時配置同步任務的,這里配置一個任務為'prometheus',拉取數據的路徑為/actuator/prometheus,目標host-port為'localhost:9092',也就是cloud-prometheus-sample暴露的prometheus端點,Prometheus(軟件)的默認啟動端口為9090。啟動后,同級目錄下會生成一個data目錄,實際上起到"時序數據庫"的類似作用。訪問Prometheus(軟件)的控制台http://localhost:9090/targets:

sp-p-5

sp-p-6

Prometheus度量統計的所有監控項可以在http://localhost:9090/graph中查看到。這里可以觀察到HttpCount的統計,但是界面不夠炫酷,配置項也少,因此需要引入Grafana。

安裝和接入Grafana

Grafana的安裝也十分簡單,它也是開箱即用的,就是配置的時候需要熟悉它的語法。先到Grafana官網下載頁面下載一個適合系統的版本,這里選擇Windows版本。解壓之后,直接運行bin目錄下的grafana-server.exe即可,默認的啟動端口是3000,訪問http://localhost:3000/,初始化賬號密碼是admin/admin,首次登陸需要修改密碼,接着添加一個數據源:

sp-p-7

接着添加一個新的命名為'sample'的Dashboard,添加一個Graph類型的Panel,配置其屬性:

sp-p-8

sp-p-9

A記錄(查詢命令)就是對應http://localhost:9090/graph中的查詢命令的目標:

sp-p-10

很簡單,配置完畢之后,就可以看到高大上的統計圖:

sp-p-11

這里只是介紹了Grafana使用的冰山一角,更多配置和使用命令可以自行查閱它的官方文檔。

原理和擴展

原理

下面是Prometheus的工作原理流程圖,來源於其官網:

sp-p-12

在SpringBoot項目中,它的工作原理如下:

sp-p-13

這就是為什么能夠使用Metrics的靜態方法直接進行數據統計,因為Spring內部用MeterRegistryPostProcessor對Metrics內部持有的全局的CompositeMeterRegistry進行了合成操作,也就是所有MeterRegistry類型的Bean都會添加到Metrics內部持有的靜態globalRegistry。

擴展

下面來個相對有生產意義的擴展實現,這篇文章提到SpringCloud體系的監控,我們需要擴展一個功能,記錄一下每個有效的請求的執行時間。添加下面幾個類或者方法:

//注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodMetric {

	String name() default "";

	String description() default "";

	String[] tags() default {};
}
//切面類
@Aspect
@Component
public class HttpMethodCostAspect {

	@Autowired
	private MeterRegistry meterRegistry;

	@Pointcut("@annotation(club.throwable.sample.aspect.MethodMetric)")
	public void pointcut() {
	}

	@Around(value = "pointcut()")
	public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
		Method targetMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
		//這里是為了拿到實現類的注解
		Method currentMethod = ClassUtils.getUserClass(joinPoint.getTarget().getClass())
				.getDeclaredMethod(targetMethod.getName(), targetMethod.getParameterTypes());
		if (currentMethod.isAnnotationPresent(MethodMetric.class)) {
			MethodMetric methodMetric = currentMethod.getAnnotation(MethodMetric.class);
			return processMetric(joinPoint, currentMethod, methodMetric);
		} else {
			return joinPoint.proceed();
		}
	}

	private Object processMetric(ProceedingJoinPoint joinPoint, Method currentMethod,
								 MethodMetric methodMetric) throws Throwable {
		String name = methodMetric.name();
		if (!StringUtils.hasText(name)) {
			name = currentMethod.getName();
		}
		String desc = methodMetric.description();
		if (!StringUtils.hasText(desc)) {
			desc = name;
		}
		String[] tags = methodMetric.tags();
		if (tags.length == 0) {
			tags = new String[2];
			tags[0] = name;
			tags[1] = name;
		}
		Timer timer = Timer.builder(name).tags(tags)
				.description(desc)
				.register(meterRegistry);
		return timer.record(() -> {
			try {
				return joinPoint.proceed();
			} catch (Throwable throwable) {
				throw new IllegalStateException(throwable);
			}
		});
	}
}
//啟動類里面添加方法
@SpringBootApplication
@EnableEurekaClient
@RestController
public class SampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SampleApplication.class, args);
	}

	@MethodMetric
	@GetMapping(value = "/hello")
	public String hello(@RequestParam(name = "name", required = false, defaultValue = "doge") String name) {
		return String.format("%s say hello!", name);
	}
}

配置好Grafana的面板,重啟項目,多次調用/hello接口:

sp-p-14

后記

如果想把監控界面做得更炫酷、更直觀、更詳細,可以先熟悉一下Prometheus的查詢語法和Grafana的面板配置,此外,Grafana或者Prometheus都支持預警功能,可以接入釘釘機器人等以便及時發現問題作出預警。

參考資料:

本文Demo項目倉庫:https://github.com/zjcscut/spring-cloud-prometheus-sample

(本文完)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM