指标和追踪:更好的结合

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

这篇博文由我们自己撰写,始终对 Spring 的一切充满热情,Josh Long 共同创作。

您决定将自己的才能用于服务人类,并且——在疫情时代,除了软件之外,您没有其他真正的技能——您将构建一个 Web 服务,人们可以在您的新网站 www.ps5ownersarebetterpeople.com.net 上检查备受推崇的 Playstation 5 电子游戏机的可用性。

一切开始得如此顺利……

转到可靠的 Spring Initializr 并生成一个新项目(称为 service),使用最新版本的 Java(当然!)并将 Reactive WebWavefrontLombokSleuthActuator 依赖项添加到项目中。单击“生成”按钮下载包含项目代码的 .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;
        };
    }
}

对于特定类型控制台(ps5nintendoxboxps4)的请求,API 会返回控制台的可用性(大概是从本地电子产品商店获取)。但由于某些原因,对于我们的演示,这将不得不是神来之笔——没有 PlayStation 5 的可用性。更糟糕的是,每次有人胆敢询问 Playstation 5 时,服务本身都会遇到错误并崩溃!我们将使用此特定代码路径(特别是询问 Playstation 5 的可用性)来模拟系统中的错误。别评判。您可能也在某个时候犯过错误。也许吧。

我们希望尽可能多地了解各个微服务及其交互,并且在尝试消除系统中的错误时,我们最希望获得这些信息。让我们看看跟踪和指标如何协同工作,提供优于单独使用指标或跟踪的可观察性状态。

我们需要一个客户端来与服务通信并向其发送一些流量。返回 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 = "https://127.0.0.1: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 启动器反过来引入了 Micrometer,它为最流行的监控系统提供了仪器客户端的简单外观,使您能够在没有供应商锁定情况下为基于 JVM 的应用程序代码添加仪器。可以将其想象成 SLF4J,但用于指标。

Micrometer 最直接的用法是捕获指标并将它们保存在内存中,Spring Boot Actuator 将执行此操作。您可以将应用程序配置为在 Actuator 管理端点(/actuator/metrics/)下显示这些指标。但是,更常见的情况是,您希望将这些指标发送到时间序列数据库,例如 Graphite、Prometheus、Netflix Atlas、Datadog 或 InfluxDB。时间序列数据库会随着时间的推移存储指标的演变值,因此您可以查看其变化情况。

跟踪数据

危险是真正侦探的零食。——麦克·巴内特

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

Micrometer 和 Sleuth 使您能够在指标和跟踪后端中进行选择。我们可以使用这两个不同的抽象,并分别为我们的跟踪和指标聚合系统建立一个专用的集群。人们确实这么做。更疯狂的事情也发生过。我们都赞同这样的理念:您不应该运行您无法收费的东西,因此让我们使用一个简单、交钥匙、托管的软件即服务 (SaaS) 产品,并让其他人来完成这项工作。否则,让这些高度相关的数据驻留在两个不同的、不相关的后端系统中所带来的集成任务,我们并不羡慕。

前往可观察性洞穴,统计员!

我们将使用 VMware Tanzu 的 出色的 Wavefront 可观察性平台,它既了解指标又了解跟踪,并且可以将它们链接在一起供我们使用。我们已经将 Wavefront 启动器添加到我们的构建中。

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

该 URL 会将您转到 Spring Boot 的 Wavefront 仪表板。这里有很多内容,因此我们将重点关注一些关键内容。

您可以看到 Wavefront 在屏幕顶部的“仪表板”菜单中完全加载了 Spring Boot 仪表板。仪表板顶部显示 Sourcemy-cloud-server,它来自配置属性 management. .export.wavefront.source(或使用默认值,即机器的主机名)。我们感兴趣的 Applicationconsole-availability,它来自配置属性 wavefront.application.nameApplication 指的是 Spring Boot 微服务的逻辑组,而不是任何特定的微服务。

单击它,您将能够一目了然地看到有关应用程序的所有信息。您可以选择查看 clientservice 任何一个模块的信息。单击“跳转到”以导航到特定的一组图形。我们对 HTTP 部分中的数据感兴趣。

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

这些一目了然的信息就是指标。指标不是基于采样数据;它们是每个请求的聚合。您应该将指标用于警报,因为它们确保您看到所有请求(以及所有错误、缓慢请求等)。另一方面,跟踪数据通常需要在大量流量下进行采样,因为数据量会随着流量量的增加而增加。

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

这有点可惜,因为即使我们知道{console}是一个低基数变量——有一组有限的可能值——我们也无法进一步深入数据,一眼看出哪些路径正在失败。指标代表聚合的统计数据,因此,即使我们根据{console}变量细分指标,指标仍然缺乏关于单个请求的上下文。

当然还有跟踪数据!点击“顶级失败请求”右侧的小面包屑/三明治图标,然后通过转到“跟踪”>“console-availability”查找服务。

这里包含了为应用程序收集的所有跟踪数据:好的、坏的或其他。

让我们通过向搜索中添加“错误”过滤器来深入研究仅有的错误请求。然后点击“搜索”。现在我们可以仔细检查各个错误请求。您可以看到每个服务调用花费了多长时间,服务之间的关系以及错误的来源。

点击屏幕右下角标记为“client: GET”的面板的“展开”图标。您可以看到请求旅程中的每个跳跃:花费的时间、跟踪 ID、URL 和路径。

展开跟踪特定段下的“标签”分支,您就可以看到 Spring Cloud Sleuth 自动代表您收集的元数据。跟踪由称为*跨度*的单个段组成,这些段描述了请求旅程中的一个跳跃。

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

我们从默认配置中获得了许多好处。除了添加 Spring Boot Actuator 启动器、Wavefront 启动器和 Sleuth 启动器并启动应用程序之外,我们实际上没有对代码进行任何操作来获得我们刚刚看到的这些结果。看到了吧?这很简单!非常简单。就像从日志记录中心系统掉到真正的可观察性平台上一样简单。我们获得了跟踪信息和指标以及一个仪表板,我们可以参考其详细信息。我们完全没有对 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 控制台;使用控制台输出中打印的方便链接!

您现在可以看到按控制台细分的不同指标。点击“仪表板”>“Spring Boot 仪表板”,您会注意到“顶级请求”和“顶级失败请求”有更多条目。这次,您可以根据每个控制台细分结果。将鼠标悬停在它们上面,您将看到详细信息。

以下是成功的请求。

以下是失败的请求。

在我们看来,ps5控制台与失败请求的重合度很高。让我们看看跟踪信息。点击“应用程序”>“跟踪”查看更新的数据。

点击关键路径细分并展开面板。点击特定段(如这里所示),并扩展“标签”分支。您将看到与特定请求关联的所有标签,包括console标签。显示的失败请求是在有人请求ps5控制台的可用性之后发生的。当然,能够根据控制台进行筛选会很不错,不是吗?点击console标签旁边的“+”图标,Wavefront 会将其添加到搜索条件中。点击“搜索”查看所有错误跟踪并找到罪魁祸首。

我们的数据根据控制台(我们特定于域的概念)细分跟踪和指标。

指标和跟踪像鸡肉和泡菜盐水一样完美搭配

什么?你从未尝试过鸡肉和泡菜盐水?它很好吃。它真的很好吃。你能想象一旦你标准化了 Spring 和 Wavefront,并且不必自己维护那么多的无差异基础设施,你将拥有多少空闲时间吗?这将会很棒。你将有如此多的时间。你会有足够的时间来尝试鸡肉和泡菜盐水。

您已经看到了一个使用指标和跟踪的具体示例。让我们回顾一下指标和跟踪的一些用法和反模式。希望这可以清楚地说明为什么您需要指标和跟踪,以及如何将每个用于什么。参考Peter Bourgon 的指标、跟踪和日志记录博客文章中设置的框架可能会有所帮助。

跟踪和指标在提供我们服务中请求范围交互的洞察力方面存在重叠。但是,指标和跟踪提供的一些信息是不连贯的。跟踪擅长显示服务之间的关系,以及关于特定请求的高基数数据,例如与请求关联的用户 ID。分布式跟踪可以帮助您快速查明分布式系统中问题根源。权衡的是,在高容量和严格的性能要求下,需要对跟踪进行采样以控制成本。这意味着您感兴趣的特定请求可能不在采样跟踪数据中。

另一方面,指标会聚合所有测量值,并在时间间隔内导出聚合值以定义时间序列数据。所有数据都包含在此聚合中,只要遵循标签基数的最佳实践,成本就不会随着流量增加而增加。因此,测量某事物最大延迟的指标将包含最慢的请求,并且错误率的计算将是准确的,而不管跟踪数据上的任何采样如何。

指标可用于请求范围之外,用于监控内存、CPU 使用率、垃圾回收和缓存,仅举几例。您需要将指标用于警报、SLO(服务级别目标)和仪表板。在console-availability示例中,它将是关于 SLO 违规的警报,通知我们服务的高错误率。(您不想 24/7 盯着仪表板来检测问题,对吧?)

然后,使用指标和跟踪,我们可以使用每个中可用的公共元数据从一个跳转到另一个。指标和跟踪信息都支持使用称为标签的数据捕获任意键值对。例如,给定关于基于 HTTP 的服务上的高延迟的警报通知(基于指标),您可以链接到与警报匹配的跨度(跟踪数据)搜索。您将搜索具有相同服务、HTTP 方法、HTTP URI 并且持续时间阈值以上的跨度,以快速获取与警报匹配的跟踪样本。

总之,数据胜于无数据,集成数据胜于非集成数据。Micrometer 和 Spring Cloud Sleuth 提供了开箱即用的可靠的可观察性姿势,但可以配置和调整以适应您的业务/域的上下文。最后,虽然您可以将 Micrometer 或 Spring Cloud Sleuth 与任意数量的其他后端一起使用,但我们发现 Wavefront 是一种方便且强大的选择。示例中显示的代码可从此 GitHub 存储库获取。

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获得支持

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

了解更多

即将举行的活动

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

查看全部