上一篇分享了一个关于 API 协议的GlobalFilter 实战的例子,本文我们来分享另一个关于 Metrics 案例:一个研发阶段可能不怎么关注,但是上线以后却是极其重要的一个模块。
GatewayMetricFitler 分析
还是一样的思路,先来研究下 SCG 是不是已经提供了类似组件。SCG 确实已经提供了一个 GatewayMetricFitler 的全局过滤器,其 Order 比 NettyWriteResponseFilter 加1,也就是在 NettyWriteResponseFilter 触发之后开始执行。
PS: 如果已经忘记了各个内置 GlobalFilter 执行顺序的话,可以翻阅一下前两篇文章的内容。
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
接下来我们来分析 GatewayMetricFitler 的来龙去脉.
首先 GatewayMetricFitler 是在 GatewayMetricsAutoConfiguration 中自动配置的,里面其实定义了两个 Metric 相关的 Bean,其中一个 GatewayMetricFitler 是今天我们实战对象,另一个是 RouteDefinitionMetrics 是用来记录每次 Route 刷新时候路由数量的。他俩有这样几处共同点:
-
都依赖 io.micrometer,体现在@ConditionalOnBean(MeterRegistry.class) 这个注解
-
指标前缀默认都是 spring.cloud.gateway
-
都可以通过配置 spring.cloud.gateway.metrics.enabled = false 来禁掉
@Bean @ConditionalOnBean(MeterRegistry.class) @ConditionalOnProperty(name = GatewayProperties.PREFIX + ".metrics.enabled", matchIfMissing = true) public GatewayMetricsFilter gatewayMetricFilter(MeterRegistry meterRegistry, List tagsProviders, GatewayMetricsProperties properties) { return new GatewayMetricsFilter(meterRegistry, tagsProviders, properties.getPrefix()); }
@Bean @ConditionalOnBean(MeterRegistry.class) @ConditionalOnProperty(name = GatewayProperties.PREFIX + ".metrics.enabled", matchIfMissing = true) public RouteDefinitionMetrics routeDefinitionMetrics(MeterRegistry meterRegistry, RouteDefinitionLocator routeDefinitionLocator, GatewayMetricsProperties properties) { return new RouteDefinitionMetrics(meterRegistry, routeDefinitionLocator, properties.getPrefix()); }
接着,我们来看看 GatewayMetricFitler 的代码实现。其构造函数会初始化三个东西:
- MeterRegistry: 这个 spring boot 集成 Micrometer 提供的一个聚合注册表,可以很方便的集成到各个监控平台
- GatewayTagsProvider:提供指标的tags,我们知道一个指标除了名字外,会有多个tags来区分,便于我们统计各个维度的数据
- metricsPrefix:指标名字的前缀,默认是 spring.cloud.gateway
然后,我们再来看主要逻辑代码,前置逻辑就是开启一个 sample,然后执行 filter 链,等 filter 链结束回到其后置逻辑:endTimerRespectingCommit
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Sample sample = Timer.start(meterRegistry);
return chain.filter(exchange).doOnSuccess(aVoid -> endTimerRespectingCommit(exchange, sample))
.doOnError(throwable -> endTimerRespectingCommit(exchange, sample));
}
而在 endTimerRespectingCommit 方法中,主要就是 endTimerInner 方法
private void endTimerInner(ServerWebExchange exchange, Sample sample) {
Tags tags = compositeTagsProvider.apply(exchange);
if (log.isTraceEnabled()) {
log.trace(metricsPrefix + ".requests tags: " + tags);
}
sample.stop(meterRegistry.timer(metricsPrefix + ".requests", tags));
}
从代码看,最终 sample stop,meterRegistry 以 前缀.request为名字,compositeTagsProvider 提供的 tags 作为标签完成本次指标度量。
代码分析完毕,来看下运行效果。
对于基于 spring boot 搞开发的我们都知道,spring boot Actuator 可以帮助我们监控和管理应用系统,包括健康检查、指标统计 、HTTP追踪等,这些都可以通过JMX或者HTTP endpoints来获得。假设我们项目仅仅依赖 spring-boot-starter-actuator,那么我们启动 SCG 进行访问之后,可以使用 /actuator/metrics 查看暴露的各个指标:
"application....",
"disk....",
"executor....",
"http.server.requests",
"jvm....",
"lettuce.command....",
"logback.events",
"process.cpu.usage",
"process.files.max",
"process.files.open",
"process.start.time",
"process.uptime",
"spring.cloud.gateway.requests",
"spring.cloud.gateway.routes.count",
"system.cpu.count",
"system.cpu.usage",
"system.load.average.1m"
然后我们继续查看具体的指标 /actuator/metrics/
spring.cloud.gateway.requests,
{
"name": "spring.cloud.gateway.requests",
"description": null,
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 140.0
},
{
"statistic": "TOTAL_TIME",
"value": 1.637071325
},
{
"statistic": "MAX",
"value": 0.0
}
],
"availableTags": [
{
"tag": "routeUri",
"values": [
"http://localhost:8080"
]
},
{
"tag": "routeId",
"values": [
"rt-test"
]
},
{
"tag": "httpMethod",
"values": [
"GET"
]
},
{
"tag": "outcome",
"values": [
"CLIENT_ERROR",
"SUCCESSFUL"
]
},
{
"tag": "status",
"values": [
"TOO_MANY_REQUESTS",
"OK"
]
},
{
"tag": "httpStatusCode",
"values": [
"200",
"429"
]
}
]
}
这是Spring Boot Actuator 通过http endpoint获得的指标数据,在实际项目中我们往往还需要和外部监控系统整合起来,这取决于公司的监控平台使用什么样的基础设施。那么,我们需要集成 Micrometer, Spring boot 自动配置了聚合注册(Composite MeterRegistry) 为我们集成 Micrometer 提供了最大的方便,我们只需要添加 micrometer-registry-{system} 这样的依赖。假设我们现在是要集成 promethues,我们只需要添加依赖:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
然后重启,继续多访问次网关,为了产生一些指标数据。然后我们就可以通过 /actuator/prometheus 获取符合 prometheus 格式的metrics:
如上图所示,spring_cloud_gateway_requests指标有count、sum、max三个指标,分别以 httpMethod、httpStatusCode、outcome、routeId、routeUri、status 这六个标签作为维度进行度量统计。这样我们是可以通过配置让 promthues 来拉取指标进行大盘展示、监控告警。
SdMetricsFilter 实战分析
上面看起来已是一个完整的闭环,但在实际项目中还是不够用,比如指标统计维度不够(也就是需要增标签),或者有些标签冗余了(也就是我们要减标签),或者因为公司内部监控平台对指标名字有自己的规范(我们需要修改指标名)。总之,因为各种原因导致我们需要提供自己的 Metrics:
第一种需求:修改指标前缀
如果仅仅是指标前缀的问题,我们不需要来定制自己的 Metric Global Filter的,从上文实例化Bean的代码我们可以发现前缀是可以通过spring.cloud.gateway.metrics.prefix这个配置进行覆盖的,我只需要增加配置即可:
spring.cloud.gateway.metrics.prefix=scg_core_cluster
这样我们的指标就会变成 scg_core_cluster_requests
第二种情况:修改指标名字
如果除了前缀之外,连名字也需要修改,那么就不能使用 GatewayMetricsFilter 了,因为代码中我们发现指标名最后结尾 requests 是写死的。
第三种情况:标签需要增减
如果是仅仅需要增加path标签的话,我们还可以通过spring.cloud.gateway.metrics.tags.path.enabled=true来开启,因为在GatewayMetricsAutoConfiguration中有这个 Bean
@Bean
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".metrics.tags.path.enabled")
public GatewayPathTagsProvider gatewayPathTagsProvider() {
return new GatewayPathTagsProvider();
}
倘若我们要增加异常标签,或者api标签,或者某些业务场景需要增加osType标签等等,那么我们也只能实现自己的 Metric Global Filter。
ok,现在假设我们现在对 Metrics 的需求是这样的:
-
指标名字:系统角色(每个系统在公司内部都有一个角色定义,在系统里通过cmRoleName获得) + "_requests"
-
tags:routeId, apiCode, exception, httpStatusCode, osType
实现代码非常简单,直接贴代码吧:
@Slf4j
public class SdMetricsFilter implements GlobalFilter, Ordered {
private String metricsName;
@Value("${cmRoleName:gateway}")
private String systemRoleName;
private final MeterRegistry meterRegistry;
public SdMetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@PostConstruct
public void init(){
metricsName = systemRoleName + "_requests";
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Timer.Sample sample = Timer.start(meterRegistry);
return chain.filter(exchange).doOnSuccess(aVoid -> endTimerRespectingCommit(exchange, sample, null))
.doOnError(throwable -> endTimerRespectingCommit(exchange, sample, throwable));
}
private void endTimerRespectingCommit(ServerWebExchange exchange, Timer.Sample sample, Throwable t) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
endTimerInner(exchange, sample, t);
}
else {
response.beforeCommit(() -> {
endTimerInner(exchange, sample, t);
return Mono.empty();
});
}
}
private void endTimerInner(ServerWebExchange exchange, Timer.Sample sample, Throwable t) {
Tags tags = SdMetricsTagsSupport.buildTags(exchange, t);
sample.stop(meterRegistry.timer(metricsName, tags));
}
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
}