Micrometer và Prometheus trong Spring Boot: Metric Actuator trên Production

Tích hợp Micrometer và Prometheus trên Spring Boot 4: mô hình truy cập Actuator, cấu hình scrape, metric HTTP, health probe và quy tắc cảnh báo.

Giám sát Spring Boot Actuator với Micrometer và Prometheus

Spring Boot Actuator cung cấp các endpoint health, metric và chẩn đoán, còn Micrometer là lớp facade metric đứng phía sau chúng. Thêm phụ thuộc micrometer-registry-prometheus sẽ tạo ra endpoint /actuator/prometheus để Prometheus scrape theo chu kỳ cố định. Các ví dụ dưới đây nhắm tới Spring Boot 4.1 (bản GA hiện tại, 4.2 đang ở giai đoạn release candidate), Micrometer 1.16 và Prometheus Java client 1.x.

Câu trả lời ngắn gọn

Tích hợp Micrometer của Spring Boot với Prometheus gồm ba bước: thêm spring-boot-starter-actuator và micrometer-registry-prometheus, thêm prometheus vào management.endpoints.web.exposure.include, rồi trỏ một scrape_config của Prometheus tới /actuator/prometheus. Các metric JVM, HTTP, HikariCP và hệ thống được công bố mà không cần viết thêm code.

Thiết Lập Micrometer trong Spring Boot: Actuator và Registry Prometheus

Các Phụ Thuộc Maven Cần Thiết

Starter Actuator kích hoạt các endpoint và kéo theo Micrometer. Registry Prometheus định dạng mọi meter theo định dạng exposition của Prometheus. Starter AspectJ chỉ cần thiết cho các annotation @Timed và @Counted.

xml
<!-- pom.xml -->
<!-- Actuator + Micrometer + Prometheus (Spring Boot 4.x) -->
<dependencies>
    <!-- Spring Boot Actuator: monitoring endpoints, includes Micrometer -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Registry Prometheus (Prometheus Java client 1.x) -->
    <!-- Exposes metrics in Prometheus format at /actuator/prometheus -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- AspectJ for @Timed and @Counted -->
    <!-- Spring Boot 4 renamed spring-boot-starter-aop to spring-boot-starter-aspectj -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>
</dependencies>

Hai cái bẫy về phiên bản vẫn xuất hiện trong các hướng dẫn cũ. Kể từ Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus được xây dựng trên Prometheus Java client 1.x; code 0.x cũ nằm trong module micrometer-registry-prometheus-simpleclient đã bị deprecated, và tài liệu Micrometer Prometheus chỉ xem đó là giải pháp tạm thời. Ngoài ra, trên Spring Boot 4, một pom.xml vẫn khai báo spring-boot-starter-aop sẽ không còn resolve được: hướng dẫn chuyển đổi Spring Boot 4.0 đổi tên nó thành spring-boot-starter-aspectj.

Cấu Hình Endpoint Actuator

Mặc định, chỉ endpoint health được phơi qua HTTP. Endpoint /actuator/prometheus tồn tại ngay khi registry có mặt trong classpath, nhưng sẽ trả về 404 cho tới khi được thêm vào danh sách exposure.

Spring Boot 3.4 đã thay các công tắc bật/tắt (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) bằng một mô hình truy cập, và các thuộc tính cũ đã bị deprecated. Mẫu opt-in đặt quyền truy cập mặc định là none rồi cấp read-only cho từng endpoint:

yaml
# application.yml
# Actuator configuration for production (Spring Boot 3.4+ access model)
management:
  endpoints:
    # Every endpoint is inaccessible unless granted below
    access:
      default: none
    web:
      exposure:
        # Endpoints exposed over HTTP
        # health, info, prometheus are the minimum for monitoring
        include: health,info,prometheus,metrics,env,loggers
      base-path: /actuator
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      show-components: when-authorized
    info:
      access: read-only
    prometheus:
      access: read-only
    metrics:
      access: read-only
    env:
      access: read-only
      # Mask sensitive values for anonymous callers
      show-values: when-authorized
    loggers:
      # unrestricted allows POST to change a log level at runtime
      access: unrestricted

Exposure và access là hai cơ chế kiểm soát riêng biệt. exposure.include quyết định endpoint có được ánh xạ qua HTTP hay không; access quyết định endpoint cho phép làm gì sau khi đã được ánh xạ. Một file thuộc tính vẫn dùng enabled: true vẫn chạy được kèm cảnh báo deprecation, và đó chính là lúc nên chuyển đổi.

Tùy chọn show-details: when-authorized hiển thị chi tiết health chỉ với người dùng đã xác thực và có vai trò phù hợp. Bản thân các endpoint được bảo vệ bằng một SecurityFilterChain riêng:

ActuatorSecurityConfig.javajava
// Securing Actuator endpoints (Spring Boot 4, Spring Security 7)
package com.example.monitoring.config;

import static org.springframework.security.web.access.IpAddressAuthorizationManager.hasIpAddress;

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {

    @Bean
    SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                // Health and info public for load balancers
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                // Prometheus accessible from the internal network only
                .requestMatchers(EndpointRequest.to("prometheus")).access(hasIpAddress("10.0.0.0/8"))
                // Other endpoints restricted to admins
                .anyRequest().hasRole("ACTUATOR_ADMIN")
            )
            .httpBasic(basic -> {})
            .build();
    }
}

Trong Spring Boot 4, EndpointRequest đã chuyển sang org.springframework.boot.security.autoconfigure.actuate.web.servlet, như được trình bày trong tài liệu tham khảo endpoint Actuator. DSL Java AuthorizedUrl của authorizeHttpRequests không có phương thức hasIpAddress: quy tắc IP đi qua IpAddressAuthorizationManager.hasIpAddress, có sẵn từ Spring Security 6.3.

Metric Tùy Chỉnh với Micrometer

Counter, Gauge và Timer cho Ứng Dụng

Micrometer cung cấp nhiều loại metric phù hợp với từng tình huống. Counter đo các sự kiện tích lũy, gauge đo giá trị tức thời và timer đo thời lượng của một thao tác.

OrderMetricsService.javajava
// Custom business metrics service
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

@Service
public class OrderMetricsService {

    // Counter for orders created
    private final Counter ordersCreatedCounter;
    // Timer to measure processing duration
    private final Timer orderProcessingTimer;
    // Atomic value for pending orders gauge
    private final AtomicInteger pendingOrdersCount = new AtomicInteger(0);

    public OrderMetricsService(MeterRegistry registry) {
        // Exported as orders_created_total: the _total suffix is added by the registry
        this.ordersCreatedCounter = Counter.builder("orders.created")
            .description("Total number of orders created")
            .tag("channel", "web")
            .register(registry);

        // Timer with histogram buckets: percentiles computed server-side in PromQL
        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .description("Order processing duration")
            .publishPercentileHistogram()
            .register(registry);

        // Gauge linked to atomic value
        // Read on each scrape
        Gauge.builder("orders.pending.count", pendingOrdersCount, AtomicInteger::get)
            .description("Number of orders pending processing")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreatedCounter.increment();
        pendingOrdersCount.incrementAndGet();
    }

    public void recordOrderProcessed(Runnable processingLogic) {
        // Measures execution duration
        orderProcessingTimer.record(processingLogic);
        pendingOrdersCount.decrementAndGet();
    }

    public <T> T recordOrderProcessedWithResult(Supplier<T> processingLogic) {
        return orderProcessingTimer.record(processingLogic);
    }
}

Có hai chi tiết đặt tên quan trọng khi dùng registry Prometheus. Tên meter của Micrometer dùng dấu chấm và được chuyển sang snake case (orders.processing.duration trở thành orders_processing_duration_seconds). Counter tự động nhận hậu tố _total, nên đặt tên counter là orders.created.total chỉ gây nhiễu.

publishPercentiles(0.5, 0.95, 0.99) tính percentile bên trong từng JVM, và các giá trị này không thể tổng hợp giữa các instance. publishPercentileHistogram() công bố các bucket mà histogram_quantile() có thể tổng hợp trên toàn bộ cụm instance, và đây là lựa chọn phù hợp khi dùng Prometheus.

Annotation @Timed và @Counted

Để tránh viết code lặp, Micrometer cung cấp các annotation đo đạc phương thức thông qua AspectJ.

PaymentService.javajava
// Automatic instrumentation with annotations
package com.example.monitoring.service;

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentValidator paymentValidator;

    public PaymentService(PaymentValidator paymentValidator) {
        this.paymentValidator = paymentValidator;
    }

    // @Timed creates a Timer
    // Measures each call and publishes count, sum, max and histogram buckets
    @Timed(
        value = "payment.process.duration",
        description = "Payment processing duration",
        histogram = true
    )
    public PaymentResult processPayment(PaymentRequest request) {
        // Call through another bean: the proxy intercepts it
        paymentValidator.validate(request);
        return executePayment(request);
    }

    // @Counted increments a counter on each call
    // Useful for discrete events
    @Counted(
        value = "payment.refunds",
        description = "Total number of refunds"
    )
    public void refundPayment(String transactionId) {
        // Refund logic
    }
}
yaml
# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
  observations:
    annotations:
      enabled: true

Kể từ Spring Boot 3.2, không còn cần khai báo thủ công các bean TimedAspect và CountedAspect: đặt management.observations.annotations.enabled=true cùng với spring-boot-starter-aspectj trong classpath sẽ tự động cấu hình chúng. Giá trị mặc định của thuộc tính này là false, điều giải thích phần lớn các báo cáo kiểu "@Timed của tôi không làm gì cả".

Hạn Chế của AOP

Annotation @Timed và @Counted chỉ hoạt động trên các phương thức public của bean Spring được gọi qua proxy. Lời gọi từ một phương thức sang phương thức khác trong cùng class sẽ bỏ qua proxy và không được đo đạc, còn annotation đặt trên phương thức private sẽ bị bỏ qua. Gắn annotation lên một controller Spring MVC vốn đã được đo đạc sẽ tạo ra observation trùng lặp.

Sẵn sàng chinh phục phỏng vấn Spring Boot?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Endpoint Health Tùy Chỉnh

Health Indicator cho Nghiệp Vụ

Health Indicator kiểm tra trạng thái của các phụ thuộc bên ngoài và các thành phần nghiệp vụ trọng yếu. Spring Boot cung cấp sẵn indicator cho cơ sở dữ liệu, Redis và nhiều dịch vụ phổ biến khác. Trong Spring Boot 4, API health nằm trong module riêng spring-boot-health, và Health cùng HealthIndicator đã chuyển từ org.springframework.boot.actuate.health sang org.springframework.boot.health.contributor.

PaymentGatewayHealthIndicator.javajava
// Health indicator for payment gateway (Spring Boot 4 packages)
package com.example.monitoring.health;

import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.time.Instant;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final String gatewayHealthUrl;

    public PaymentGatewayHealthIndicator(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
        this.gatewayHealthUrl = "https://api.payment-gateway.com/health";
    }

    @Override
    public Health health() {
        Instant start = Instant.now();

        try {
            // Call gateway health endpoint
            var response = restClient.get()
                .uri(gatewayHealthUrl)
                .retrieve()
                .toBodilessEntity();

            Duration responseTime = Duration.between(start, Instant.now());

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime.toMillis() + "ms")
                    .withDetail("statusCode", response.getStatusCode().value())
                    .build();
            } else {
                return Health.down()
                    .withDetail("statusCode", response.getStatusCode().value())
                    .withDetail("reason", "Unexpected status code")
                    .build();
            }
        } catch (Exception e) {
            Duration responseTime = Duration.between(start, Instant.now());

            return Health.down()
                .withDetail("error", e.getClass().getSimpleName())
                .withDetail("message", e.getMessage())
                .withDetail("responseTime", responseTime.toMillis() + "ms")
                .build();
        }
    }
}

Indicator này xuất hiện trong /actuator/health dưới tên paymentGateway, tức tên bean bỏ đi hậu tố HealthIndicator.

Health Group cho Kubernetes

Spring Boot 4 bật sẵn probe liveness và readiness trên mọi nền tảng, không chỉ khi phát hiện Kubernetes. Các group /actuator/health/liveness và /actuator/health/readiness tồn tại mà không cần cấu hình gì, và không cần khai báo thủ công bean LivenessStateHealthIndicator hay ReadinessStateHealthIndicator. Cấu hình chỉ cần thiết khi muốn thêm thành phần vào một group hoặc định nghĩa group tùy chỉnh:

yaml
# application.yml
# Health groups configuration for Kubernetes
management:
  endpoint:
    health:
      probes:
        # true by default in Spring Boot 4
        enabled: true
        # Also serve the groups at /livez and /readyz on the main server port
        add-additional-paths: true
      group:
        # Readiness: can the application receive traffic?
        readiness:
          include: readinessState,db
        # Custom group for critical dependencies
        critical:
          include: db,paymentGateway
          show-details: when-authorized

Group liveness nên chỉ giới hạn ở livenessState. Đưa cơ sở dữ liệu hoặc một gateway bên ngoài vào liveness sẽ khiến Kubernetes khởi động lại mọi pod khi phụ thuộc đó gặp sự cố, biến một sự cố cục bộ thành sự cố toàn phần. Các phụ thuộc bên ngoài thuộc về readiness, và chỉ khi ứng dụng không thể phục vụ bất kỳ yêu cầu nào nếu thiếu chúng.

Probe Kubernetes sau đó trỏ tới các endpoint chuyên biệt:

yaml
# kubernetes-deployment.yml
# Kubernetes probes configuration
spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /livez
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /readyz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 3

Với add-additional-paths: true, các probe vẫn nằm trên cổng ứng dụng ngay cả khi Actuator chạy trên một cổng quản trị riêng, nhờ vậy một server chính bị lỗi vẫn được probe phát hiện.

Tích Hợp Prometheus với Spring Boot và Grafana

Cấu Hình Scraping của Prometheus

Prometheus thu thập metric bằng cách truy vấn endpoint /actuator/prometheus định kỳ. Cấu hình scrape xác định các mục tiêu và đường dẫn metric, vì Prometheus mặc định dùng /metrics.

yaml
# prometheus.yml
# Prometheus configuration for Spring Boot
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    static_configs:
      - targets:
          - 'order-service:9090'
          - 'payment-service:9090'
          - 'inventory-service:9090'
    # Relabeling to strip the port from the instance label
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+):\d+'
        replacement: '${1}'

  # Kubernetes service discovery
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # Only scrape pods with annotation
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)

Các target dùng cổng 9090 vì cấu hình production ở phần sau chuyển Actuator sang một cổng quản trị riêng. Nếu không có management.server.port, target scrape là cổng của ứng dụng.

Metric Spring Boot Actuator Được Công Bố Mặc Định

Actuator kết hợp Micrometer phơi bày các metric JVM, process, HikariCP và HTTP mà không cần code. Các truy vấn PromQL sau bao quát những metric được theo dõi đầu tiên trong production:

promql
# PromQL queries for JVM monitoring

# Heap memory usage, summed across heap pools
sum by (instance) (jvm_memory_used_bytes{area="heap"})

# Heap usage percentage
sum by (instance) (jvm_memory_used_bytes{area="heap"})
  / sum by (instance) (jvm_memory_max_bytes{area="heap"}) * 100

# Live threads
jvm_threads_live_threads

# Garbage collection: seconds spent paused per second
rate(jvm_gc_pause_seconds_sum[5m])

# GC pauses per minute
rate(jvm_gc_pause_seconds_count[1m]) * 60

# CPU used by the JVM process
process_cpu_usage

# Active database connections
hikaricp_connections_active

# Connection pool utilization
hikaricp_connections_active / hikaricp_connections_max * 100

jvm_memory_used_bytes{area="heap"} trả về một chuỗi cho mỗi memory pool (Eden, Survivor, Old Gen với G1). Chia nó cho giá trị max mà không có sum by (instance) sẽ tạo ra tỷ lệ theo từng pool, trông sai lệch trên dashboard.

Một bean MeterBinder bổ sung các gauge cấp ứng dụng mà metric mặc định không bao gồm:

CustomJvmMetrics.javajava
// Additional JVM metrics
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;

@Component
public class CustomJvmMetrics implements MeterBinder {

    @Override
    public void bindTo(MeterRegistry registry) {
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

        // System load average
        Gauge.builder("system.load.average", osBean, OperatingSystemMXBean::getSystemLoadAverage)
            .description("System load average over 1 minute")
            .register(registry);

        // Available processors count
        Gauge.builder("system.cpu.count", osBean, OperatingSystemMXBean::getAvailableProcessors)
            .description("Number of available processors")
            .register(registry);

        // Application uptime
        Gauge.builder("application.uptime.seconds",
                ManagementFactory.getRuntimeMXBean(),
                bean -> bean.getUptime() / 1000.0)
            .description("Application uptime in seconds")
            .register(registry);
    }
}

system.cpu.count và process.uptime đã được các binder có sẵn của Micrometer công bố. Ví dụ trên minh họa mẫu triển khai, còn một binder thực tế chỉ nên đăng ký những gì metric mặc định còn thiếu.

Dashboard Grafana Sẵn Có

Grafana có các dashboard cộng đồng cho Spring Boot hoạt động trên metric chuẩn của Actuator. Một dashboard viết tay dùng các loại panel timeseries và stat:

json
{
  "annotations": {
    "list": []
  },
  "panels": [
    {
      "title": "Request Rate",
      "type": "timeseries",
      "targets": [
        {
          "expr": "sum by (method, uri, status) (rate(http_server_requests_seconds_count{application=\"$application\"}[5m]))",
          "legendFormat": "{{method}} {{uri}} - {{status}}"
        }
      ]
    },
    {
      "title": "Response Time P99",
      "type": "timeseries",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, sum by (le, method, uri) (rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m])))",
          "legendFormat": "{{method}} {{uri}}"
        }
      ]
    },
    {
      "title": "Error Rate",
      "type": "stat",
      "targets": [
        {
          "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) * 100"
        }
      ]
    }
  ]
}

Nhiều dashboard sao chép từ các hướng dẫn cũ vẫn dùng loại panel graph và singlestat. Singlestat đã bị loại bỏ trong Grafana 8, và Grafana 12 đã gỡ bỏ hoàn toàn hỗ trợ AngularJS, vì vậy các panel đó chỉ hiển thị sau khi được chuyển sang timeseries và stat.

Nhập Vào Grafana

Để nhập một dashboard cộng đồng: Grafana > Dashboards > New > Import, rồi nhập ID 4701 (JVM Micrometer) hoặc 12900 (SpringBoot APM). Kiểm tra loại panel sau khi nhập: các bản sửa đổi cũ của dashboard cộng đồng có thể vẫn chứa panel Angular mà Grafana 12 phải chuyển đổi.

Cảnh Báo với Prometheus

Quy Tắc Cảnh Báo Cốt Lõi

Quy tắc cảnh báo của Prometheus kích hoạt thông báo khi metric vượt qua các ngưỡng tới hạn.

yaml
# alerting-rules.yml
# Alert rules for Spring Boot applications
groups:
  - name: spring-boot-alerts
    rules:
      # Alert if application is down
      - alert: ApplicationDown
        expr: up{job="spring-boot-apps"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Application {{ $labels.instance }} is down"
          description: "{{ $labels.instance }} has been down for more than 1 minute"

      # Alert on HTTP error rate
      - alert: HighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
          /
          sum(rate(http_server_requests_seconds_count[5m])) by (application)
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High error rate on {{ $labels.application }}"
          description: "Error rate is {{ $value | humanizePercentage }}"

      # Alert on P99 latency, aggregated per application
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum by (le, application) (rate(http_server_requests_seconds_bucket[5m]))
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High latency on {{ $labels.application }}"
          description: "P99 latency is {{ $value | humanizeDuration }}"

      # Heap memory alert
      - alert: HighHeapUsage
        expr: |
          sum by (instance) (jvm_memory_used_bytes{area="heap"})
          / sum by (instance) (jvm_memory_max_bytes{area="heap"})
          > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High heap memory usage on {{ $labels.instance }}"
          description: "Heap usage is at {{ $value | humanizePercentage }}"

      # Database connection pool exhausted alert
      - alert: DatabaseConnectionPoolExhausted
        expr: |
          hikaricp_connections_active
          / hikaricp_connections_max
          > 0.9
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Database connection pool nearly exhausted"
          description: "{{ $value | humanizePercentage }} of connections in use"

      # Excessive GC alert
      - alert: HighGCPause
        expr: |
          rate(jvm_gc_pause_seconds_sum[5m])
          / rate(jvm_gc_pause_seconds_count[5m])
          > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High GC pause time"
          description: "Average GC pause is {{ $value | humanizeDuration }}"

Các cảnh báo trên bao quát những vấn đề sản xuất phổ biến nhất: tính sẵn sàng, hiệu năng và tài nguyên. Quy tắc HighLatency cần sum by (le, ...): thiếu nó, histogram_quantile tính một P99 cho mỗi chuỗi (theo URI, method và status), và chỉ một endpoint chậm với ba lời gọi cũng đủ kích hoạt cảnh báo.

Metric HTTP và Metric Cơ Sở Dữ Liệu trong Spring Boot

Đo Đạc Tự Động Yêu Cầu HTTP

Spring Boot đo đạc mọi yêu cầu Spring MVC và WebFlux đầu vào thông qua Observation API và công bố timer http.server.requests, gắn các tag method, uri, status, outcome và exception.

yaml
# application.yml
# HTTP metrics configuration
management:
  metrics:
    distribution:
      # Histogram buckets for server-side percentiles
      percentiles-histogram:
        http.server.requests: true
      # SLO buckets: count requests faster than each boundary
      slo:
        http.server.requests: 100ms, 500ms, 1s, 2s
    tags:
      # Common tags added to every meter
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}

Các interface WebMvcTagsContributor và WebMvcTagsProvider xuất hiện trong nhiều ví dụ đã bị loại bỏ từ Spring Boot 3.0. Tag tùy chỉnh giờ được thêm bằng cách kế thừa DefaultServerRequestObservationConvention, như tài liệu tham khảo metric của Spring Boot mô tả:

TenantObservationConvention.javajava
// Adds custom tags to http.server.requests (Spring Boot 3.0+)
package com.example.monitoring.config;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;

@Component
public class TenantObservationConvention extends DefaultServerRequestObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        HttpServletRequest request = context.getCarrier();
        String userId = request.getHeader("X-User-Id");
        String tenantId = request.getHeader("X-Tenant-Id");

        // Keep the default tags and add two bounded ones
        return super.getLowCardinalityKeyValues(context).and(
            KeyValue.of("user.type", userId != null ? "authenticated" : "anonymous"),
            KeyValue.of("tenant", tenantId != null ? tenantId : "default")
        );
    }
}

Chỉ các key-value có cardinality thấp mới trở thành label của Prometheus. Một tag tenant chấp nhận được với vài chục tenant nhưng trở thành vấn đề cardinality khi có hàng nghìn tenant. Các giá trị có cardinality cao (ID người dùng, ID đơn hàng) thuộc về getHighCardinalityKeyValues, vốn chỉ đưa vào trace. Phần tracing của Observation API được trình bày chi tiết trong observability Spring Boot với OpenTelemetry.

Metric HikariCP và Truy Vấn SQL

Metric của pool HikariCP được phơi bày tự động với tiền tố hikaricp, gắn tag theo tên pool, bên cạnh các gauge chung jdbc.connections. Đặt tên pool giúp tag dễ đọc:

yaml
# application.yml
# HikariCP pool configuration
spring:
  datasource:
    hikari:
      # Becomes the "pool" tag on hikaricp_* metrics
      pool-name: OrderServicePool
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Spring Boot không đo thời gian ở cấp truy vấn. Dự án datasource-micrometer bọc DataSource và tạo một observation cho mỗi kết nối, truy vấn và result set, cho ra cả metric lẫn span trace. Dòng 2.x nhắm tới Spring Boot 4, dòng 1.x nhắm tới Spring Boot 3:

xml
<!-- pom.xml -->
<!-- JDBC observations: 2.x for Spring Boot 4, 1.x for Spring Boot 3 -->
<dependency>
    <groupId>net.ttddyy.observation</groupId>
    <artifactId>datasource-micrometer-spring-boot</artifactId>
    <version>2.0.1</version>
</dependency>

Cách này thay thế cách làm thủ công là bọc DataSource trong một ProxyDataSourceBuilder với listener tự viết, vốn bỏ qua auto-configuration DataSource của Spring Boot và phải cập nhật theo mỗi lần nâng cấp Boot. Truy vấn chậm thường là dấu hiệu của mẫu N+1, được trình bày trong giải pháp truy vấn N+1 với Spring Data JPA.

Thực Hành Tốt Nhất cho Sản Xuất

Cardinality của Metric

Cardinality quá cao làm giảm hiệu năng của Prometheus. Mỗi tổ hợp tag duy nhất tạo ra một chuỗi thời gian riêng biệt.

AntiPatternHighCardinality.javajava
// AVOID: unbounded cardinality
package com.example.monitoring.antipattern;

@Service
public class AntiPatternHighCardinality {

    private final MeterRegistry registry;

    // BAD: userId creates one series per user
    public void trackUserAction(String userId, String action) {
        Counter.builder("user.actions")
            .tag("userId", userId)  // Millions of possible values
            .tag("action", action)
            .register(registry)
            .increment();
    }
}
GoodPracticeCardinality.javajava
// Controlled cardinality
package com.example.monitoring.bestpractice;

@Service
public class GoodPracticeCardinality {

    private final MeterRegistry registry;

    // GOOD: user category instead of ID
    public void trackUserAction(User user, String action) {
        Counter.builder("user.actions")
            .tag("userType", user.getSubscriptionType())  // FREE, PREMIUM, ENTERPRISE
            .tag("action", action)
            .register(registry)
            .increment();
    }

    // GOOD: let a timer histogram bucket durations
    public void trackResponseTime(long responseTimeMs) {
        Timer.builder("response.time")
            .publishPercentileHistogram()
            .register(registry)
            .record(Duration.ofMillis(responseTimeMs));
    }
}

Một Timer có bucket histogram trả lời các câu hỏi về độ trễ tốt hơn một counter với tag bucket tự chế: PromQL có thể tính bất kỳ percentile nào từ các bucket, và ranh giới bucket luôn nhất quán giữa các service.

Cấu Hình Sẵn Sàng cho Sản Xuất

yaml
# application-production.yml
# Production configuration (Spring Boot 4)
management:
  server:
    # Separate port for management endpoints, not exposed publicly
    port: 9090
  endpoints:
    access:
      default: none
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      probes:
        add-additional-paths: true
    info:
      access: read-only
    prometheus:
      access: read-only
  observations:
    annotations:
      enabled: true
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      minimum-expected-value:
        http.server.requests: 1ms
      maximum-expected-value:
        http.server.requests: 30s
    tags:
      application: ${spring.application.name}
      environment: production
      version: ${app.version:unknown}

Với access.default: none, các endpoint như env, beans, configprops và mappings đều không truy cập được mà không cần liệt kê từng cái. management.prometheus.metrics.export.enabled mặc định là true, và thiết lập step không có tác dụng với Prometheus: đây là registry kiểu pull, và chu kỳ scrape được đặt trong prometheus.yml.

Lỗi Thường Gặp với Actuator và Prometheus

Triệu chứngNguyên nhânCách khắc phục
/actuator/prometheus trả về 404Endpoint không có trong danh sách exposure, hoặc thiếu registryThêm prometheus vào management.endpoints.web.exposure.include và kiểm tra micrometer-registry-prometheus có trong classpath
Target Prometheus hiển thị "server returned HTTP status 401"Spring Security bảo vệ mọi endpoint trừ healthMở prometheus cho mạng của scraper hoặc cấu hình basic auth trong cấu hình scrape
@Timed không tạo ra metricmanagement.observations.annotations.enabled mặc định là false, hoặc thiếu AspectJĐặt thuộc tính thành true và thêm spring-boot-starter-aspectj
Không tìm thấy spring-boot-starter-aop sau khi nâng cấpĐã đổi tên trong Spring Boot 4Dùng spring-boot-starter-aspectj
Import Health hoặc HealthIndicator lỗi trên Boot 4Package đã chuyển sang org.springframework.boot.health.contributorCập nhật các import
Cảnh báo deprecation với management.endpoint.*.enabledMô hình bật/tắt đã bị thay thế trong Spring Boot 3.4Dùng management.endpoints.access.default và management.endpoint.<id>.access
Percentile trên dashboard lệch xa thực tếGiá trị publishPercentiles bị lấy trung bình giữa các podDùng publishPercentileHistogram() và histogram_quantile()

Câu Hỏi Phỏng Vấn về Micrometer trong Spring Boot

Actuator xuất hiện trong hầu hết các buổi phỏng vấn Spring Boot cho vị trí mid-level và senior. Những câu trả lời phân biệt một hồ sơ có kinh nghiệm với một junior:

  • Khác biệt giữa liveness và readiness: liveness báo Kubernetes khởi động lại pod, readiness báo Kubernetes ngừng định tuyến lưu lượng. Ứng viên đưa cơ sở dữ liệu vào group liveness là người chưa từng vận hành nó qua một sự cố.
  • Vì sao dùng Micrometer thay vì client Prometheus trực tiếp: Micrometer là một facade. Cùng một đoạn code Counter và Timer có thể xuất sang Prometheus, Datadog hay OTLP chỉ bằng cách đổi phụ thuộc, và phần đo đạc sẵn có của Spring (HTTP, pool JDBC, cache) được viết dựa trên nó.
  • Percentile phía client so với histogram: percentile phía client không thể tổng hợp giữa các instance, còn histogram thì có. Đây là câu hỏi bẫy phổ biến nhất của chủ đề này.
  • Cardinality là gì và vì sao nó làm hỏng Prometheus: mỗi tổ hợp label duy nhất là một chuỗi thời gian; một tag ID người dùng nhân mức dùng bộ nhớ lên theo số lượng người dùng.
  • Cách bảo mật Actuator: giới hạn exposure, đặt mức truy cập, chạy phần quản trị trên một cổng riêng và bảo vệ bằng một SecurityFilterChain riêng.

Nhiều câu hỏi cùng loại được tập hợp trong module câu hỏi phỏng vấn Spring Boot Actuator.

Nguồn Tham Khảo

Những Điểm Cần Nhớ về Micrometer Spring Boot và Prometheus

  • Ba bước để scrape hoạt động: starter Actuator, micrometer-registry-prometheus, prometheus trong danh sách exposure
  • Mô hình truy cập từ 3.4: management.endpoints.access.default: none cộng read-only cho từng endpoint thay thế các cờ enabled đã deprecated
  • Thay đổi trong Spring Boot 4: spring-boot-starter-aspectj, API health trong org.springframework.boot.health.contributor, EndpointRequest trong org.springframework.boot.security.autoconfigure.actuate.web.servlet, probe bật mặc định
  • Annotation cần một công tắc: management.observations.annotations.enabled=true, không cần bean TimedAspect thủ công
  • Tag HTTP qua Observation API: kế thừa DefaultServerRequestObservationConvention, giữ label ở cardinality thấp
  • Histogram thay vì percentile phía client: publishPercentileHistogram() và sum by (le) trong mọi histogram_quantile
  • Liveness chỉ xét cục bộ: phụ thuộc bên ngoài đặt trong readiness hoặc group tùy chỉnh, không bao giờ trong liveness

Những thiết lập mặc định này mang lại cho một service Spring Boot 4 các metric tổng hợp chính xác giữa các pod, các endpoint không để lộ gì ra mạng công cộng, và các cảnh báo kích hoạt bởi sự cố thực sự thay vì bởi một URI chậm.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thử thách hôm nay

Bạn có tìm ra lỗi trong Spring Boot không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 26 tháng 10, 2026

Thẻ

#spring boot actuator
#micrometer
#prometheus
#monitoring
#observability

Chia sẻ

Bài viết liên quan