基于SpirngBoot、Prometheus、Grafana集成
集成了Micrometer
框架的JVM
应用使用到Micrometer
的API
收集的度量数据位于内存之中,因此,需要额外的存储系统去存储这些度量数据,需要有监控系统负责统一收集和处理这些数据,还需要有一些UI工具去展示数据,「一般情况下大佬或者老板只喜欢看炫酷的仪表盘或者动画」。常见的存储系统就是时序数据库,主流的有Influx
、Datadog
等。比较主流的监控系统(主要是用于数据收集和处理)就是Prometheus
(一般叫普罗米修斯,下面就这样叫吧)。而展示的UI目前相对用得比较多的就是Grafana
。另外,Prometheus
已经内置了一个时序数据库的实现,因此,在做一套相对完善的度量数据监控的系统只需要依赖目标JVM
应用,Prometheus
组件和Grafana
组件即可。下面花一点时间从零开始搭建一个这样的系统,之前写的一篇文章基于Windows
系统,操作可能跟生产环境不够接近,这次使用CentOS7
。
SpirngBoot中使用Micrometer
SpringBoot
中的spring-boot-starter-actuator
依赖已经集成了对Micrometer
的支持,其中的metrics
端点的很多功能就是通过Micrometer
实现的,prometheus
端点默认也是开启支持的,实际上actuator
依赖的spring-boot-actuator-autoconfigure
中集成了对很多框架的开箱即用的API
,其中prometheus
包中集成了对Prometheus
的支持,使得使用了actuator
可以轻易地让项目暴露出prometheus
端点,使得应用作为Prometheus
收集数据的客户端,Prometheus
(服务端软件)可以通过此端点收集应用中Micrometer
的度量数据。
jvm-m-1.png
我们先引入spring-boot-starter-actuator
和spring-boot-starter-web
,实现一个Counter
和Timer
作为示例。依赖:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.22</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.1.0</version> </dependency> </dependencies> 复制代码
接着编写一个下单接口和一个消息发送模块,模拟用户下单之后向用户发送消息:
//实体 @Data public class Message { private String orderId; private Long userId; private String content; } @Data public class Order { private String orderId; private Long userId; private Integer amount; private LocalDateTime createTime; } //控制器和服务类 @RestController public class OrderController { @Autowired private OrderService orderService; @PostMapping(value = "/order") public ResponseEntity<Boolean> createOrder(@RequestBody Order order) { return ResponseEntity.ok(orderService.createOrder(order)); } } @Slf4j @Service public class OrderService { private static final Random R = new Random(); @Autowired private MessageService messageService; public Boolean createOrder(Order order) { //模拟下单 try { int ms = R.nextInt(50) + 50; TimeUnit.MILLISECONDS.sleep(ms); log.info("保存订单模拟耗时{}毫秒...", ms); } catch (Exception e) { //no-op } //记录下单总数 Metrics.counter("order.count", "order.channel", order.getChannel()).increment(); //发送消息 Message message = new Message(); message.setContent("模拟短信..."); message.setOrderId(order.getOrderId()); message.setUserId(order.getUserId()); messageService.sendMessage(message); return true; } } @Slf4j @Service public class MessageService implements InitializingBean { private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500); private static BlockingQueue<Message> REAL_QUEUE; private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(); private static final Random R = new Random(); static { REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size); } public void sendMessage(Message message) { try { REAL_QUEUE.put(message); } catch (InterruptedException e) { //no-op } } @Override public void afterPropertiesSet() throws Exception { EXECUTOR.execute(() -> { while (true) { try { Message message = REAL_QUEUE.take(); log.info("模拟发送短信,orderId:{},userId:{},内容:{},耗时:{}毫秒", message.getOrderId(), message.getUserId(), message.getContent(), R.nextInt(50)); } catch (Exception e) { throw new IllegalStateException(e); } } }); } } //切面类 @Component @Aspect public class TimerAspect { @Around(value = "execution(* club.throwable.smp.service.*Service.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName()); ThrowableHolder holder = new ThrowableHolder(); Object result = timer.recordCallable(() -> { try { return joinPoint.proceed(); } catch (Throwable e) { holder.throwable = e; } return null; }); if (null != holder.throwable) { throw holder.throwable; } return result; } private class ThrowableHolder { Throwable throwable; } } 复制代码
yaml的配置如下:
server: port: 9091 management: server: port: 10091 endpoints: web: exposure: include: '*' base-path: /management 复制代码
注意多看spring官方文档关于Actuator
的详细描述,在SpringBoot2.x
之后,配置Web端点暴露的权限控制和SpringBoot1.x
有很大的不同。总结一下就是:除了shutdown
端点之外,其他端点默认都是开启支持的(「这里仅仅是开启支持,并不是暴露为Web端点,端点必须暴露为Web端点才能被访问」),禁用或者开启端点支持的配置方式如下:
management.endpoint.${端点ID}.enabled=true/false 复制代码
可以查看actuator-api文档查看所有支持的端点的特性,这个是2.1.0.RELEASE版本的官方文档,不知道日后链接会不会挂掉。端点只开启支持,但是不暴露为Web端点,是无法通过http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId}
访问的。暴露监控端点为Web端点的配置是:
management.endpoints.web.exposure.include=info,health management.endpoints.web.exposure.exclude=prometheus 复制代码
management.endpoints.web.exposure.include
用于指定暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。
management.endpoints.web.exposure.exclude
用于指定不暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。management.endpoints.web.exposure.include
默认指定的只有info
和health
两个端点,我们可以直接指定暴露所有的端点:management.endpoints.web.exposure.include=*
,如果采用YAML
配置,「记得要在星号两边加上英文单引号」。暴露所有Web监控端点是一件比较危险的事情,如果需要在生产环境这样做,请务必先确认http://{host}:{management.port}
不能通过公网访问(也就是监控端点访问的端口只能通过内网访问,这样可以方便后面说到的Prometheus服务端通过此端口收集数据)。
Prometheus的安装和配置
Prometheus目前的最新版本是2.5,鉴于笔者当前没深入玩过Docker
,这里还是直接下载它的压缩包解压安装。
wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz tar xvfz prometheus-*.tar.gz cd prometheus-* 复制代码
先编辑解压出来的目录下的Prometheus
配置文件prometheus.yml
,主要修改scrape_configs
节点的属性:
scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: 'prometheus' # metrics_path defaults to '/metrics' # scheme defaults to 'http'. # 这里配置需要拉取度量信息的URL路径,这里选择应用程序的prometheus端点 metrics_path: /management/prometheus static_configs: # 这里配置host和port - targets: ['localhost:10091'] 复制代码
配置拉取度量数据的路径为localhost:10091/management/metrics
,此前记得把前一节提到的应用在虚拟机中启动。接着启动Prometheus
应用:
# 可选参数 --storage.tsdb.path=存储数据的路径,默认路径为./data ./prometheus --config.file=prometheus.yml 复制代码
Prometheus
引用的默认启动端口是9090,启动成功后,日志如下:
jvm-m-2.png
此时,访问http://${虚拟机host}:9090/targets
就能看到当前Prometheus
中执行的Job
:
jvm-m-3.png
访问http://${虚拟机host}:9090/graph
可以查找到我们定义的度量Meter
和spring-boot-starter-actuator
中已经定义好的一些关于JVM或者Tomcat
的度量Meter
。我们先对应用的/order
接口进行调用,然后查看一下监控前面在应用中定义的order_count_total
和method_cost_time_seconds_sum
:
jvm-m-4.png
jvm-m-5.png
可以看到,Meter
的信息已经被收集和展示,但是显然不够详细和炫酷,这个时候就需要使用Grafana的UI做一下点缀。
Grafana的安装和使用
Grafana
的安装过程如下:
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm sudo yum localinstall grafana-5.3.4-1.x86_64.rpm 复制代码
安装完成后,通过命令service grafana-server start
启动即可,默认的启动端口为3000,通过http://${host}:3000
访问即可。初始的账号密码都为admin,权限是管理员权限。接着需要在Home
面板添加一个数据源,目的是对接Prometheus
服务端从而可以拉取它里面的度量数据。数据源添加面板如下:
jvm-m-6.png
其实就是指向Prometheus服务端的端口就可以了。接下来可以天马行空地添加需要的面板,就下单数量统计的指标,可以添加一个Graph
的面板:
jvm-m-7.png
配置面板的时候,需要在基础(General)中指定Title:
jvm-m-9.png
接着比较重要的是Metrics的配置,需要指定数据源和Prometheus的查询语句:
jvm-m-8.png
最好参考一下Prometheus
的官方文档,稍微学习一下它的查询语言PromQL
的使用方式,一个面板可以支持多个PromQL
查询。前面提到的两项是基本配置,其他配置项一般是图表展示的辅助或者预警等辅助功能,这里先不展开,可以去Grafana
的官网挖掘一下使用方式。然后我们再调用一下下单接口,过一段时间,图表的数据就会自动更新和展示:
jvm-m-10.png
接着添加一下项目中使用的Timer的Meter,便于监控方法的执行时间,完成之后大致如下:
jvm-m-11.png
上面的面板虽然设计相当粗糙,但是基本功能已经实现。设计面板并不是一件容易的事,如果有需要可以从Github
中搜索一下grafana dashboard
关键字找现成的开源配置使用或者二次加工后使用。
小结
常言道:工欲善其事,必先利其器。Micrometer
是JVM
应用的一款相当优异的度量框架,它提供基于Tag
和丰富的度量类型和API
便于多维度地进行不同角度度量数据的统计,可以方便地接入Prometheus
进行数据收集,使用Grafana
的面板进行炫酷的展示,提供了天然的spring-boot
体系支持。但是,在实际的业务代码中,度量类型Counter
经常被滥用,一旦工具被不加思考地滥用,就反而会成为混乱或者毒瘤。因此,这篇文章就是对Micrometer
中的各种Meter
的使用场景基于个人的理解做了调研和分析,后面还会有系列的文章分享一下这套方案在实战中的经验和踩坑经历。
参考资料:
(本文完 To be continue c-10-d n-e-20181102 最近有点忙,没办法经常更新)