SCG GlobalFilter实战 -- Metrics

363 阅读5分钟

         上一篇分享了一个关于 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;
    }
}