指标与追踪:协同增效

工程 | Tommy Ludwig | 2021 年 2 月 10 日 | ...

这篇博文由我们自己那位始终对 Spring 的一切充满热情的 Josh Long 合著。

你决定将你的才能奉献给人类服务——在疫情时代,除了软件之外没有其他真正的技能——你打算构建一个人们可以用来检查备受推崇的 PlayStation 5 游戏主机可用性的 Web 服务,放在你的新网站 www.ps5ownersarebetterpeople.com.net 上。

一切都始于如此吉兆...

前往可信赖的 Spring Initializr,使用最新版本的 Java(*当然!*)生成一个新项目(称为 service),并向项目中添加 Reactive WebWavefrontLombokSleuthActuator 依赖。点击 Generate 按钮下载包含项目代码的 .zip 文件,你应该在喜欢的 IDE 中打开它。

将以下配置值添加到 application.properties 中。我们将在它们变得相关时进行回顾。目前,需要记住的关键是我们在端口 8083 上运行 service,并且使用 spring.application.name 属性为其命名为 service

spring.application.name=service
server.port=8083
wavefront.application.name=console-availability
management.metrics.export.wavefront.source=my-cloud-server

这里是 Java 代码

@Slf4j
@SpringBootApplication
public class ServiceApplication {

    public static void main(String[] args) {
        log.info("starting server");
        SpringApplication.run(ServiceApplication.class, args);
    }
}

@RestController
class AvailabilityController {

    private boolean validate(String console) {
        return StringUtils.hasText(console) &&
               Set.of("ps5", "ps4", "switch", "xbox").contains(console);
    }

    @GetMapping("/availability/{console}")
    Map<String, Object> getAvailability(@PathVariable String console) {
        return Map.of("console", console,
                "available", checkAvailability(console));
    }

    private boolean checkAvailability(String console) {
        Assert.state(validate(console), () -> "the console specified, " + console + ", is not valid.");
        return switch (console) {
            case "ps5" -> throw new RuntimeException("Service exception");
            case "xbox" -> true;
            default -> false;
        };
    }
}

给定对特定类型主机(ps5, nintendo, xbox, ps4)的请求,API 返回主机的可用性(大概来源于当地电子产品商店)。除了某种原因,出于演示目的我们姑且认为是*机械降神*——PlayStation 5 没有可用性。更糟的是,每次有人敢于询问 PlayStation 5 时,服务本身都会出错并崩溃!我们将利用这条特定的代码路径——特别是询问 PlayStation 5 的可用性——来模拟系统中的错误。别评判。你可能也犯过错误。也许吧。

我们希望尽可能多地获取关于单个微服务及其交互的信息,而当我们尝试排查系统中的 bug 时,这些信息将最为需要。让我们看看追踪和指标如何协同工作,以提供比单独使用指标或追踪更优越的可观测性姿态。

我们需要一个客户端与服务通信并驱动一些流量。回到 Spring Initializr,生成另一个与 service 完全相同的项目,但将其 spring.application.name 值设为 client

这里是 配置文件

spring.application.name=client
wavefront.application.name=console-availability
management.metrics.export.wavefront.source=my-cloud-server

代码使用响应式的非阻塞 WebClient 向服务发出请求。整个应用程序——包括 clientservice——都使用响应式的非阻塞 HTTP。你也可以轻松使用传统的基于 Servlet 的 Spring MVC。或者你可以完全避免 HTTP,转而使用消息技术。或者两者都用。这里是 Java 代码

@Slf4j
@SpringBootApplication
public class ClientApplication {

    public static void main(String[] args) {
        log.info("starting client");
        SpringApplication.run(ClientApplication.class, args);
    }

    @Bean
    WebClient webClient(WebClient.Builder builder) {
        return builder.build();
    }

    @Bean
    ApplicationListener<ApplicationReadyEvent> ready(AvailabilityClient client) {
        return applicationReadyEvent -> {
            for (var console : "ps5,xbox,ps4,switch".split(",")) {
                Flux.range(0, 20).delayElements(Duration.ofMillis(100)).subscribe(i ->
                        client
                                .checkAvailability(console)
                                .subscribe(availability ->
                                        log.info("console: {}, availability: {} ", console, availability.isAvailable())));
            }
        };
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Availability {
    private boolean available;
    private String console;
}

@Component
@RequiredArgsConstructor
class AvailabilityClient {

    private final WebClient webClient;
    private static final String URI = "http://localhost:8083/availability/{console}";

    Mono<Availability> checkAvailability(String console) {
        return this.webClient
                .get()
                .uri(URI, console)
                .retrieve()
                .bodyToMono(Availability.class)
                .onErrorReturn(new Availability(false, console));
    }

}

启动 service 应用程序,然后启动 client 应用程序。client 应用程序会向服务产生大量需求,其中一些会导致请求失败。我们希望捕获所有这些信息。

换个名字,它还是那个数字

首先,我们希望获得所有数据的聚合视图,即*指标*,这些数据提供了关于所有请求的统计信息。指标是数字,是聚合。指标可以涵盖内存/线程使用、垃圾回收、进程指标等方面。它们通常也包含业务可能设定的关键绩效指标,例如完成的订单数量、通过认证的用户数量等。

Actuator starter 反过来引入了 Micrometer,它为最流行的监控系统的 instrumentation 客户端提供了一个简单的门面,让你无需供应商锁定即可 instrument 您的 JVM 应用代码。可以将其类比为 SLF4J,但用于指标。

Micrometer 最直接的用法是捕获指标并将它们保存在内存中,这是 Spring Boot Actuator 的功能。你可以配置你的应用程序,使其在 Actuator 管理端点 /actuator/metrics/ 下显示这些指标。然而,更常见的是,你会希望将这些指标发送到时间序列数据库,如 Graphite、Prometheus、Netflix Atlas、Datadog 或 InfluxDB。时间序列数据库存储指标随时间演变的值,以便你可以看到它的变化。

追踪数据

危险是真正的侦探的零食。——Mac Barnett

我们还希望获得单个请求的详细分解和追踪,以便为特定失败请求提供上下文。Sleuth starter 引入了 Spring Cloud Sleuth 分布式追踪抽象,它为 OpenZipkin、Google Cloud Stackdriver Trace 和 Wavefront 等分布式追踪系统提供了一个简单的门面。

Micrometer 和 Sleuth 让你在指标和追踪后端方面拥有选择权。我们*可以*使用这两种不同的抽象,并单独建立一个专门的集群用于追踪和指标聚合系统。人们确实这样做。更疯狂的事情也发生过。我们信奉“不经营不盈利的东西”的理念,所以让我们使用一个简单、开箱即用、托管的软件即服务 (SaaS) 产品,让别人来做那项工作。我们并不羡慕将如此高度相关的数据存储在两个不同、不相关的后端系统中的集成任务。

前往可观测性洞穴,数据侠!

我们将使用 VMware Tanzu 出色的 Wavefront 可观测性平台,它既理解指标也理解追踪,并且可以将它们关联起来。我们已经在构建中添加了 Wavefront starter。

启动 service 然后启动 clientclient 将产生大量流量。嗯,也不是*很多*。记住,Reddit 在其全球规模下成功使用了 Wavefront。所以,所有条件相同的情况下,我们的数据*微不足道*。但这足以看到一些核心概念的实际运作。当我们的 Spring Boot 应用程序启动时,会打印出一个 Wavefront URL。这是访问*免费增值* Wavefront 集群的 URL。你已经有了一个有效的 Wavefront 配置,甚至无需注册账户!指标发布到 Wavefront 需要一分钟。请等待一分钟,然后在浏览器中访问控制台输出中打印的 URL。

该 URL 会带你进入 Spring Boot 的 Wavefront 控制面板。这里有很多信息,我们将重点关注几个关键点。

你可以看到 Wavefront 预置了功能齐全的 Spring Boot 控制面板,位于屏幕顶部的 Dashboards 菜单中。控制面板顶部显示 Sourcemy-cloud-server,这来自于配置属性 management. .export.wavefront.source(或使用默认值,即机器的主机名)。我们关注的 Applicationconsole-availability,它来自于配置属性 wavefront.application.name。*Application* 指的是 Spring Boot 微服务的逻辑分组,而不是任何特定的一个。

点击它,你将一目了然地看到关于你应用程序的一切。你可以选择查看任一模块的信息——clientservice。点击 Jump To 跳转到特定的图表集。我们关注 HTTP 部分的数据。

你可以看到一些有用的信息,例如代码中遇到的 Top RequestsTop Failed RequestsTop Exceptions——将鼠标悬停在特定类型的请求上可以获取与每个条目相关的详细信息。你可以获取与失败请求相关的 HTTP 方法(GET)、服务(service)、状态码(500)和 URI(/availability/{console})等信息。

这些一目了然的数字就是指标。指标并非基于采样数据;它们是对每个单独请求的聚合。你应该使用指标进行告警,因为它们确保你看到*所有*请求(以及*所有*错误、慢请求等)。另一方面,追踪数据通常需要在高流量下进行采样,因为数据量与流量呈比例增加。

我们可以看到,指标收集在区分请求时忽略了 {console} 路径变量的值,这意味着——就我们的数据而言——只有一个 URI(/availability/{console})。这是有意为之。{console} 是我们用来指定主机的路径变量,但它也很可能是用户 ID、订单 ID 或其他可能有很多甚至无限个值的东西。指标系统默认记录高基数指标是危险的。有限基数指标很便宜!成本不会随流量增加。注意你的指标中的基数。

这有点不幸,因为即使我们知道 {console} 是一个低基数变量——可能值的集合是有限的——我们也无法进一步深入查看数据,从而一目了然地知道哪些路径正在失败。指标代表聚合统计数据,所以即使我们根据 {console} 变量对指标进行细分,指标仍然缺乏关于单个请求的上下文。

别忘了还有追踪数据!点击 Top Failed Requests 字样右边的小面包屑/汉堡图标,然后通过导航到 Traces > console-availability 来找到服务。

这里是为应用程序收集的所有追踪:无论好坏。

让我们通过向搜索添加 Error 过滤器来仅深入查看错误请求。然后点击 Search。现在我们可以详细检查单个错误请求。你可以看到每个服务调用花费了多长时间,服务之间的关系,以及错误发生的位置。

点击屏幕右下方标记为 client: GET 面板的 Expand 图标。你可以看到请求旅程中的每个跳跃:花费的时间、追踪 ID、URL 和路径。

展开追踪特定段落下的 Tags 分支,你就可以看到 Spring Cloud Sleuth 自动为你收集的元数据。一个追踪由称为*span* 的独立段组成,每个 span 描述了请求旅程中的一个跳跃。

使用业务/领域上下文丰富数据

我们从默认配置中获益良多。除了添加 Spring Boot Actuator starter、Wavefront starter 和 Sleuth starter 并启动应用程序之外,我们实际上没有对代码做任何改动就得到了刚才看到的结果。看到了吗?这很容易!非常容易。甚至比从以日志为中心的系统转移到真正的可观测性平台还要容易。我们获得了追踪信息、指标以及一个可以查看详细信息的控制面板。我们对 Java 代码完全没有做任何修改来支持这一切。

让我们更进一步,定制 Spring Cloud Sleuth 和 Micrometer 捕获的元数据,以便更容易地按领域特定的概念进行深入分析:即请求的主机类型。我们可以使用 {console} 路径变量来实现这一点。代码已经验证了主机的取值范围在已知主机集合内。在使用输入之前进行验证非常重要,这能确保主机类型的基数较低。你不应该使用任意可能具有高基数的输入(如路径变量或查询参数)作为指标标签——尽管你可以使用高基数数据作为追踪标签。现在,我们不必从追踪数据中的 HTTP 路径推断主机类型,而可以使用指标和追踪上的标签。

我们将更新服务,注入一个 SpanCustomizer 以定制追踪信息。我们还将更新服务,配置一个 WebFluxTagsContributor 以定制 Spring Boot 捕获并提供给 Micrometer 的标签。这里是新的更新后的代码

@Slf4j
@SpringBootApplication
public class ServiceApplication {

    @Bean
    WebFluxTagsContributor consoleTagContributor() {
        return (exchange, ex) -> {
            var console = "UNKNOWN";
            var consolePathVariable = ((Map<String,String>) exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)).get("console");
            if (AvailabilityController.validateConsole(consolePathVariable)) {
                console = consolePathVariable;
            }
            return Tags.of("console", console);
        };
    }

    public static void main(String[] args) {
        log.info("starting server");
        SpringApplication.run(ServiceApplication.class, args);
    }
}

@RestController
@AllArgsConstructor
class AvailabilityController {

    private final SpanCustomizer spanCustomizer;

    @GetMapping("/availability/{console}")
    Map<String, Object> getAvailability(@PathVariable String console) {
        Assert.state(validateConsole(console), () -> "the console specified, " + console + ", is not valid.");
        this.spanCustomizer.tag("console", console);
        return Map.of("console", console, "available", checkAvailability(console));
    }

    private boolean checkAvailability(String console) {
        return switch (console) {
            case "ps5" -> throw new RuntimeException("Service exception");
            case "xbox" -> true;
            default -> false;
        };
    }

    static boolean validateConsole(String console) {
        return StringUtils.hasText(console) &&
               Set.of("ps5", "ps4", "switch", "xbox").contains(console);
    }

}

使用上述更改重新运行服务,然后运行客户端(与之前相同),并等待一分钟以便指标发布。然后再次打开 Wavefront 控制台;使用控制台输出中打印的那个方便的链接!

你现在可以看到按主机细分的不同指标。点击 Dashboards > Spring Boot Dashboard,你会注意到 Top RequestsTop Failed Requests 有更多条目。这次,你可以根据每个主机来细分结果。将鼠标悬停在它们上面,你就会看到详细信息。

这里是成功的请求。

这里是失败的请求。

在我们看来,ps5 主机与失败请求高度关联。让我们看看追踪信息。点击 Applications > Traces,查看更新后的数据。

点击关键路径分解并展开面板。点击特定的段落(如图所示),然后展开 Tags 分支。你将看到与特定请求关联的所有标签,包括 console 标签。图示的失败请求是在有人请求 ps5 主机可用性之后发生的。如果能基于主机进行过滤,那就太好了,不是吗?点击 console 标签旁边的 + 图标,Wavefront 将其添加到搜索条件中。点击 Search 查看所有错误追踪并找出罪魁祸首。

我们的数据根据主机(我们的领域特定概念)细分了追踪和指标。

指标与追踪如鸡肉配泡菜水一样搭调

什么?你*从没*试过鸡肉配泡菜水?很好吃。真的很好吃。你能想象一旦你标准化使用 Spring 和 Wavefront,不再需要自己维护那么多无差别的基础设施后,你会有多少空闲时间吗?那将非常美好。你会有很多时间。你甚至会有时间去尝尝鸡肉配泡菜水。

你已经看到了一个将指标和追踪结合使用的具体示例。现在我们来回顾一下指标和追踪的一些用途和反模式。这希望能清楚地说明为什么你需要同时使用指标和追踪,以及如何针对不同目的使用它们。参考 Peter Bourgon 的博文《指标、追踪和日志》中建立的框架可能会很有帮助。

追踪和指标在提供关于服务中请求范围交互的洞察方面有所重叠。然而,指标和追踪提供的一些信息是相互独立的。追踪擅长展示服务之间的关系以及关于特定请求的高基数数据,例如与请求关联的用户 ID。分布式追踪有助于你快速定位分布式系统中的问题来源。权衡是,在高流量和严格性能要求下,需要对追踪进行采样以控制成本。这意味着你感兴趣的特定请求可能不在采样后的追踪数据中。

另一方面,指标聚合所有测量数据,并在时间间隔内导出聚合结果以定义时间序列数据。所有数据都包含在这个聚合中,并且只要遵循标签基数的最佳实践,成本就不会随流量增加。因此,衡量某事最大延迟的指标将包含最慢的请求,并且错误率的计算将是准确的,无论追踪数据是否经过采样。

指标可以用于请求范围之外的监控,例如监控内存、CPU 使用率、垃圾回收和缓存等。你会希望使用指标来配置告警、SLO(服务级别目标)和控制面板。在 console-availability 示例中,这将是一个关于 SLO 违规的告警,通知我们服务的高错误率。(你不会想整天盯着控制面板来检测问题吧?)

然后,通过指标和追踪,我们可以利用它们共有的元数据在两者之间进行跳转。指标和追踪信息都支持捕获任意的键值对,这些键值对称为标签。例如,给定一个关于基于 HTTP 的服务高延迟的告警通知(基于指标),你可以链接到一个匹配告警的 span(追踪数据)搜索。你会搜索具有相同服务、HTTP 方法、HTTP URI 且持续时间超过某个阈值的 span,以快速获得匹配告警的追踪样本。

总之,有数据总比没有数据好,集成的数据比非集成的数据更好。Micrometer 和 Spring Cloud Sleuth 开箱即用地提供了可靠的可观测性态势,但可以根据您的业务/领域的上下文进行配置和调整。最后,虽然您*可以*将 Micrometer 或 Spring Cloud Sleuth 与许多其他后端一起使用,但我们认为 Wavefront 是一个方便而强大的选择。示例中显示的代码可从这个 GitHub 仓库获取。

订阅 Spring 电子报

通过 Spring 电子报保持联系

订阅

领先一步

VMware 提供培训和认证,助你加速发展。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区即将举行的所有活动。

查看全部