Spring Boot 3 + Grafana实现可观察性
本文将教您如何为 Spring Boot 应用程序配置可观察性。我们假设可观察性被理解为指标、日志记录和分布式跟踪之间的互连。最后,它应该允许您监控系统状态以检测错误和延迟。
Spring Boot 2 和 3 之间的可观察性方法发生了一些重大变化。通过 Spring Cloud Sleuth 项目,跟踪将不再是 Spring Cloud 的一部分。该项目的核心已移至Micrometer Tracing。您可以在 Spring 博客上的这篇文章中阅读有关原因和未来计划的更多信息。
那篇文章的主要目的是给你一个简单的收据,告诉你如何使用一种新的方法为你用 Spring Boot 编写的微服务启用可观察性。为了简化我们的练习,我们将在他们的云中使用完全托管的 Grafana 实例。我们将构建一个非常基本的架构,其中包含两个在本地运行的微服务。让我们花点时间详细讨论一下我们的架构。
源代码
如果您想自己尝试一下,可以随时查看我的源代码。为此,您需要克隆我的 GitHub 存储库。它包含几个教程。您需要转到inter-communication目录。之后,您应该按照我的指示进行操作。
https://github.com/piomin/course-spring-microservices.git
Spring Boot 可观察性架构
有两个应用程序:inter-callme-service和inter-caller-service。应用程序调用应用程序公开的inter-caller-serviceHTTP 端点inter-callme-service。我们运行两个inter-callme-service. 我们还将使用 Spring Cloud Load Balancer 在这两个实例之间配置静态负载平衡。所有应用程序都将使用 Spring Boot Actuator 和 Micrometer 项目公开 Prometheus 指标。对于追踪,我们将使用 Open Telemetry with Micrometer Tracing 和 OpenZipkin。为了将包括日志、指标和跟踪在内的所有数据从本地 Spring Boot 实例发送到云端,我们必须使用 Grafana Agent。
另一方面,有一个堆栈负责收集和可视化所有数据。正如我之前提到的,我们将为此利用 Grafana Cloud。这是一种非常舒适的方式,因为我们不必安装和配置所有必需的工具。首先,Grafana Cloud 提供了一个现成的 Prometheus 实例,负责收集指标。我们还需要一个日志聚合工具来存储和查询我们应用程序的日志。Grafana Cloud 为此提供了一个预配置的 Loki 实例。最后,我们有一个通过 Grafana Tempo 的分布式跟踪后端。这是我们整个架构的可视化。
使用千分尺启用度量和跟踪
为了以 Prometheus 格式导出指标,我们需要包含
micrometer-registry-prometheus依赖项。为了跟踪上下文传播,我们应该添加
micrometer-tracing-bridge-otel模块。我们还应该使用 Grafana Tempo 支持的格式之一导出跟踪范围。它将通过
opentelemetry-exporter-zipkin依赖项成为 OpenZipkin。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>
我们需要使用最新可用的 Spring Boot 3 版本。目前,它是3.0.0-RC1. 作为候选版本,该版本在 Spring Milestone 存储库中可用。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-RC1</version>
<relativePath/>
</parent>
Spring Boot 3 中更有趣的新特性之一是对 Prometheus 示例的支持。示例是对应用程序发布的指标之外的数据的引用。它们允许将度量数据链接到分布式跟踪。在这种情况下,发布的指标包含对traceId. 为了启用特定指标的示例,我们需要公开百分位数直方图。我们将为http.server.requests度量(1)这样做。我们还将通过将采样概率设置为 1.0 (2)将所有跟踪发送到 Grafana Cloud 。最后,为了验证它是否正常工作,我们打印traceId并spanId在日志行(3)中。
spring:
application:
name: inter-callme-service
output.ansi.enabled: ALWAYS
management:
endpoints.web.exposure.include: *
metrics.distribution.percentiles-histogram.http.server.requests: true # (1)
tracing.sampling.probability: 1.0 # (2)
logging.pattern.console: "%clr(%d{HH:mm:ss.SSS}){blue} %clr(%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]){yellow} %clr(:){red} %clr(%m){faint}%n" # (3)
公开 POST 端点,该inter-callme-service端点仅以相反的顺序返回消息。我们不需要在这里添加任何东西,只需要标准的 Spring Web 注释。
@RestController
@RequestMapping("/callme")
class CallmeController(private val repository: ConversationRepository) {
private val logger: Logger = LoggerFactory
.getLogger(CallmeController::class.java)
@PostMapping("/call")
fun call(@RequestBody request: CallmeRequest) : CallmeResponse {
logger.info("In: {}", request)
val response = CallmeResponse(request.id, request.message.reversed())
repository.add(Conversation(request = request, response = response))
return response
}
}
使用 Spring Cloud 进行负载平衡
在由 暴露的端点中,inter-caller-service我们只是从 调用端点inter-callme-service。为此,我们使用 Spring RestTemplate。您也可以声明 Spring Cloud OpenFeign 客户端,但它目前似乎不支持开箱即用的 Micrometer Tracing。
@RestController
@RequestMapping("/caller")
class CallerController(private val template: RestTemplate) {
private val logger: Logger = LoggerFactory
.getLogger(CallerController::class.java)
private var id: Int = 0
@PostMapping("/send/{message}")
fun send(@PathVariable message: String): CallmeResponse? {
logger.info("In: {}", message)
val request = CallmeRequest(++id, message)
return template.postForObject("http://inter-callme-service/callme/call",
request, CallmeResponse::class.java)
}
}
在本练习中,我们将使用一个静态客户端负载均衡器,将流量分配给inter-callme-service. 通常,您会将 Spring Cloud Load Balancer 与基于 Eureka 的服务发现集成。但是,我不想让我们的演示与架构中的外部组件复杂化。假设我们正在运行inter-callme-service,这是文件55800中55900的负载均衡器配置application.yml:
spring:
application:
name: inter-caller-service
cloud:
loadbalancer:
cache:
ttl: 1s
instances:
- name: inter-callme-service
servers: localhost:55800, localhost:55900
由于没有内置的静态负载均衡器实现,我们需要添加一些代码。首先,我们必须将配置属性注入 Spring bean。
@Configuration
@ConfigurationProperties("spring.cloud.loadbalancer")
class LoadBalancerConfigurationProperties {
val instances: MutableList<ServiceConfig> = mutableListOf()
class ServiceConfig {
var name: String = ""
var servers: String = ""
}
}
然后我们需要创建一个实现
ServiceInstanceListSupplier接口的bean。它只返回ServiceInstance代表所有已定义静态地址的对象列表。
class StaticServiceInstanceListSupplier(private val properties: LoadBalancerConfigurationProperties,
private val environment: Environment):
ServiceInstanceListSupplier {
override fun getServiceId(): String = environment
.getProperty(LoadBalancerClientFactory.PROPERTY_NAME)!!
override fun get(): Flux<MutableList<ServiceInstance>> {
val serviceConfig: LoadBalancerConfigurationProperties.ServiceConfig? =
properties.instances.find { it.name == serviceId }
val list: MutableList<ServiceInstance> =
serviceConfig!!.servers.split(",", ignoreCase = false, limit = 0)
.map { StaticServiceInstance(serviceId, it) }.toMutableList()
return Flux.just(list)
}
}
最后,我们需要为应用启用 Spring Cloud Load Balancer 并RestTemplate使用@LoadBalanced.
@SpringBootApplication
@LoadBalancerClient(value = "inter-callme-service", configuration = [CustomCallmeClientLoadBalancerConfiguration::class])
class InterCallerServiceApplication {
@Bean
@LoadBalanced
fun template(builder: RestTemplateBuilder): RestTemplate =
builder.build()
}
这是客户端负载均衡器配置。我们提供我们的自定义
StaticServiceInstanceListSupplier实现作为默认值
ServiceInstanceListSupplier。然后我们将RandomLoadBalancer负载平衡算法设置为默认实现。
class CustomCallmeClientLoadBalancerConfiguration {
@Autowired
lateinit var properties: LoadBalancerConfigurationProperties
@Bean
fun clientServiceInstanceListSupplier(
environment: Environment, context: ApplicationContext):
ServiceInstanceListSupplier {
val delegate = StaticServiceInstanceListSupplier(properties, environment)
val cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager::class.java)
return if (cacheManagerProvider.ifAvailable != null) {
CachingServiceInstanceListSupplier(delegate, cacheManagerProvider.ifAvailable)
} else delegate
}
@Bean
fun loadBalancer(environment: Environment,
loadBalancerClientFactory: LoadBalancerClientFactory):
ReactorLoadBalancer<ServiceInstance> {
val name: String? = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME)
return RandomLoadBalancer(
loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceListSupplier::class.java),
name!!
)
}
}
使用 Spring Boot 测试可观察性
让我们看看它是如何工作的。第一步,我们将运行两个inter-callme-service. 由于我们设置了侦听端口的静态值,因此我们需要覆盖server.port每个实例的属性。我们可以使用 env 变量来做到这一点SERVER_PORT。转到
inter-communication/inter-callme-service目录并运行以下命令:
$ SERVER_PORT=55800 mvn spring-boot:run
$ SERVER_PORT=55900 mvn spring-boot:run
然后,转到
inter-communication/inter-caller-service目录并在默认端口上运行单个实例8080:
$ mvn spring-boot:run
POST /caller/send/{message}然后,让我们用参数多次调用我们的端点,例如:
$ curl -X POST http://localhost:8080/caller/call/hello1
以下是inter-caller-service带有突出显示的traceId参数值的日志:
让我们看一下来自 的日志inter-callme-service。如您所见,该参数与该请求的traceId参数相同。
traceIdinter-caller-service
以下是第二个实例的日志inter-callme-service:
您也可以使用 Spring Cloud OpenFeign 尝试相同的练习。它已配置并可以使用。但是,对我来说,它没有traceId正确传播参数。也许,当前非 GA 版本的 Spring Boot 和 Spring Cloud 就是这种情况。
让我们验证另一个特性——Prometheus 示例。为此,我们需要使用请求 OpenMetrics 格式/actuator/prometheus的标头调用端点。Accept这与 Prometheus 用于抓取指标的标头相同。
$ curl -H Accept: application/openmetrics-text; version=1.0.0 http://localhost:8080/actuator/prometheus
如您所见,结果包含几个指标traceId和spanId参数。这些是我们的榜样。
安装和配置 Grafana 代理
我们的示例应用程序已准备就绪。现在,主要目标是将所有收集到的 observables 发送到我们在 Grafana Cloud 上的帐户。有多种方法可以将指标、日志和跟踪发送到 Grafana 堆栈。在本文中,我将向您展示如何使用Grafana 代理。首先,我们需要安装它。根据您的操作系统,您可以在此处找到详细的安装说明。由于我使用的是 macOS,因此我可以使用 Homebrew 来完成:
$ brew install grafana-agent
在我们启动代理之前,我们需要准备一个配置文件。该文件的位置还取决于您的操作系统。对我来说是$(brew --prefix)
/etc/grafana-agent/config.yml。配置 YAML 清单包含有关我们希望如何收集和发送指标、跟踪和日志的信息。让我们从指标开始。在该scrape_configs部分中,我们需要设置用于抓取的端点列表(1)和默认路径(2)。在该remote_write部分中,我们必须传递 Grafana Cloud 实例身份验证凭据(3)和 URL (4)。默认情况下,Grafana Agent 不发送示例。因此我们需要使用send_exemplars属性(5)来启用它。
metrics:
configs:
- name: integrations
scrape_configs:
- job_name: agent
static_configs:
- targets: [127.0.0.1:55800,127.0.0.1:55900,127.0.0.1:8080] # (1)
metrics_path: /actuator/prometheus # (2)
remote_write:
- basic_auth: # (3)
password: <YOUR_GRAFANA_CLOUD_TOKEN>
username: <YOUR_GRAFANA_CLOUD_USER>
url: https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push # (4)
send_exemplars: true # (5)
global:
scrape_interval: 60s
您可以在 Grafana Cloud 仪表板中找到有关您的 Prometheus 实例的所有信息。
在下一步中,我们准备用于收集日志并将其发送到 Grafana Loki 的配置。和之前一样,我们需要设置 Loki 实例的身份验证凭据(1)和 URL (2)。这里最重要的是传递日志文件的位置(3)。
logs:
configs:
- clients:
- basic_auth: # (1)
password: <YOUR_GRAFANA_CLOUD_TOKEN>
username: <YOUR_GRAFANA_CLOUD_USER>
# (2)
url: https://logs-prod-eu-west-0.grafana.net/loki/api/v1/push
name: springboot
positions:
filename: /tmp/positions.yaml
scrape_configs:
- job_name: springboot-json
static_configs:
- labels:
__path__: ${HOME}/inter-*/spring.log # (3)
job: springboot-json
targets:
- localhost
target_config:
sync_period: 10s
默认情况下,Spring Boot 仅记录到控制台,不写入日志文件。在我们的例子中,Grafana 代理从输出文件中读取日志行。为了写入日志文件,我们需要设置一个 logging.file.name or logging.file.path 属性。由于有两个实例,inter-callme-service我们需要以某种方式区分它们的日志文件。我们将server.port为此使用该属性。文件内的日志以 JSON 格式存储。
logging:
pattern:
console: "%clr(%d{HH:mm:ss.SSS}){blue} %clr(%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]){yellow} %clr(:){red} %clr(%m){faint}%n"
file: "{\"timestamp\":\"%d{HH:mm:ss.SSS}\",\"level\":\"%p\",\"traceId\":\"%X{traceId:-}\",\"spanId\":\"%X{spanId:-}\",\"appName\":\"${spring.application.name}\",\"message\":\"%m\"}%n"
file:
path: ${HOME}/inter-callme-service-${server.port}
最后,我们将配置跟踪收集。除了 Grafana Tempo 实例的身份验证凭据和 URL,我们还需要启用 OpenZipkin 接收器(1)。
traces:
configs:
- name: default
remote_write:
- endpoint: tempo-eu-west-0.grafana.net:443
basic_auth:
username: <YOUR_GRAFANA_CLOUD_USER>
password: <YOUR_GRAFANA_CLOUD_TOKEN>
# (1)
receivers:
zipkin:
然后,我们可以使用以下命令启动代理:
$ brew services restart grafana-agent
Grafana 代理包含一个侦听默认端口的 Zipkin 收集器9411。还有一个 HTTP API 暴露在代理之外的端口上,12345用于验证代理状态。
例如,我们可以使用 Grafana Agent HTTP API 来验证它正在监控多少日志文件。为此,只需调用端点GET agent/api/v1/logs/targets。如您所见,对我来说,它正在监视三个文件。这正是我们想要实现的目标,因为有两个正在运行的实例inter-callme-service和一个inter-caller-service.
使用 Grafana Stack 可视化 Spring Boot 可观察性
Grafana Cloud 在我们的练习中最重要的优势之一是我们已经配置了所有必需的东西。导航到 Grafana 仪表板后,您可以显示可用数据源的列表。如您所见,已经配置了 Loki、Prometheus 和 Tempo。
默认情况下,Grafana Cloud 在 Prometheus 数据源中启用示例。如果您自己运行 Grafana,请不要忘记在您的 Prometheus 数据源上启用它。
让我们从日志开始。我们将分析与“使用 Spring Boot 测试可观察性”部分完全相同的日志。我们将获取 Grafana Agent 发送的所有日志。您可能还记得,我们将所有日志格式化为 JSON。因此,我们将使用Json服务器端的解析器来解析它们。多亏了这一点,我们将能够按所有日志字段进行过滤。例如,我们可以使用traceId标签过滤表达式:{job="springboot-json"} | json | traceId =
1bb1d7d78a5ac47e8ebc2da961955f87.
这是未经任何过滤的完整日志列表。突出显示的行包含两个分析轨迹的日志。
在下一步中,我们将配置 Prometheus 指标可视化。由于我们为指标启用了百分位直方图,因此我们有多个由值http.server.requests表示的桶。 一组带有标签
http_server_requests_seconds_bucket的多个桶 ,其中包含 值小于或等于 标签中包含的数值的所有样本的 计数 。我们需要统计 90% 和 60% 的请求的直方图。以下是我们的 Prometheus 查询:_bucketlele
这是我们的直方图。示例显示为绿色菱形。
当您将鼠标悬停在所选示例上时,您将看到更多详细信息。例如,它包括traceId值。
最后,我们练习的最后一部分。我们想用 Grafana Tempo 分析特定的痕迹。您唯一需要做的就是选择grafanacloud-*-traces数据源并设置 searched 的值traceId。这是一个示例结果。
最后的想法
Spring Boot 3 的第一个 GA 版本指日可待。在从 Spring Boot 2 迁移期间,您必须处理的最重要的事情之一可能是可观察性。在本文中,您可以找到当前 Spring Boot 方法的详细说明。如果你对 Spring Boot 感兴趣,那么值得一读这篇文章中关于它构建微服务的最佳实践。