指標與追蹤:協同增效

工程 | 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 = "https://: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 社群即將舉行的所有活動。

檢視全部