Spring Boot Actuator: health, metrics, mở endpoint an toàn

5 câu hỏi~30 phútTừ khóa: Actuator, /health, liveness, readiness, health group, HealthIndicator, Micrometer, Prometheus, /info, management.server.port

Tiếp đi: Actuator. Kubernetes cần biết shop-app còn sống hay không thì mình làm thế nào?

Đó đúng là việc của Spring Boot Actuator: một module thêm sẵn các endpoint vận hành (/actuator/health, /actuator/metrics, /actuator/info…) để hạ tầng bên ngoài hỏi "app có ổn không?".

1. Bài toán: shop-app chạy 3 pod trên Kubernetes

            Load balancer
          /       |        \
     pod-1      pod-2      pod-3
   (đang start) (DB mất)   (ổn)

2. Vấn đề

3. Giải pháp: Actuator + health groups + Micrometer

Thành phầnTrả lời câu hỏi
/actuator/healthApp và các phụ thuộc (DB, Redis, disk) có ổn không?
/actuator/health/livenessProcess có bị treo/hỏng không? Hỏng thì K8s restart.
/actuator/health/readinessĐã sẵn sàng nhận traffic chưa? Chưa thì K8s bỏ khỏi Service, không restart.
/actuator/metrics, /actuator/prometheusSố liệu: request, latency, JVM, pool… (qua Micrometer)
/actuator/infoVersion, git commit, build time đang chạy

4. Code từng bước

Bước 1: 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>

Bước 2: Config

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics   # chỉ mở cái cần
  endpoint:
    health:
      probes:
        enabled: true            # bật /health/liveness, /health/readiness
      show-details: when-authorized
      group:
        readiness:
          include: readinessState,db,redis
        liveness:
          include: livenessState  # KHÔNG đưa db vào liveness
  info:
    git:
      mode: simple
  metrics:
    tags:
      application: shop-app

Trên Kubernetes, Spring Boot tự bật probes. Chạy local thì cần probes.enabled=true như trên.

Bước 3: Custom HealthIndicator. shop-app gọi payment-service bên ngoài; muốn readiness phản ánh nó:

@Component("paymentGateway")
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentClient paymentClient;

    public PaymentGatewayHealthIndicator(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @Override
    public Health health() {
        try {
            long ms = paymentClient.ping();          // timeout ngắn, ví dụ 500ms
            return Health.up().withDetail("latencyMs", ms).build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

Tên bean paymentGateway thành tên component trong health. Thêm vào group: readiness.include: readinessState,db,redis,paymentGateway.

Bước 4: Test bằng curl

curl localhost:8080/actuator/health
{"status":"UP","groups":["liveness","readiness"]}

curl localhost:8080/actuator/health/readiness
{"status":"DOWN"}        # khi DB chết

curl localhost:8080/actuator/health/liveness
{"status":"UP"}          # process vẫn ổn → K8s không restart

curl localhost:8080/actuator/info
{"git":{"branch":"main","commit":{"id":"a1b2c3d","time":"..."}},"build":{"version":"1.4.2"}}

K8s manifest:

livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 8081 }
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 8081 }
  periodSeconds: 5
startupProbe:
  httpGet: { path: /actuator/health/liveness, port: 8081 }
  failureThreshold: 30     # cho 150s để start

5. Cách hoạt động

GET /actuator/health/readiness
        |
  HealthEndpoint
        |  group "readiness" = [readinessState, db, redis, paymentGateway]
        v
  +------------------+------------+----------------+------------------+
  | readinessState   | db         | redis          | paymentGateway   |
  | (AvailabilityBean)| SELECT 1   | PING           | ping()           |
  +--------UP--------+----DOWN----+------UP--------+-------UP---------+
        |
  StatusAggregator: có 1 DOWN → tổng DOWN → HTTP 503
        |
  K8s: readiness fail → gỡ pod khỏi Service (không restart)

Vòng đời readiness do Spring tự quản: khi start xong ApplicationReadyEvent → ACCEPTING_TRAFFIC; khi shutdown → REFUSING_TRAFFIC trước, rồi graceful shutdown.

6. So sánh liveness và readiness

LivenessReadiness
Câu hỏiProcess còn hoạt động đúng?Nhận traffic được chưa?
Fail thì K8sRestart containerGỡ khỏi load balancer
Nên chứaTrạng thái nội bộ (deadlock, broken state)DB, Redis, dịch vụ bắt buộc
Đặt DB vào?Không: DB chết thì restart cả 3 pod cũng vô íchCó

7. Khi nào nên dùng / không nên dùng

Mình muốn đếm số đơn tạo thành công và đo thời gian createOrder(), rồi vẽ lên Grafana. Viết thế nào?

Dùng Micrometer: nó giống SLF4J nhưng cho metrics. Code ghi vào MeterRegistry, còn đẩy đi đâu (Prometheus, Datadog…) là do registry bạn chọn.

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final Counter ordersCreated;
    private final Timer createTimer;

    public OrderService(OrderRepository orderRepository, MeterRegistry registry) {
        this.orderRepository = orderRepository;
        this.ordersCreated = Counter.builder("shop.orders.created")
                .description("Số đơn tạo thành công")
                .register(registry);
        this.createTimer = Timer.builder("shop.orders.create.time")
                .publishPercentiles(0.5, 0.95, 0.99)
                .register(registry);
    }

    public Order createOrder(CreateOrderRequest req) {
        return createTimer.record(() -> {
            Order order = orderRepository.save(Order.from(req));
            ordersCreated.increment();
            return order;
        });
    }
}

Hoặc gọn hơn: @Timed("shop.orders.create.time") trên method (cần bean TimedAspect, tức là lại AOP proxy như bài 4.1.3).

curl localhost:8081/actuator/prometheus | grep shop_orders
shop_orders_created_total{application="shop-app"} 1532.0
shop_orders_create_time_seconds{quantile="0.99"} 0.412
http_server_requests_seconds_count{uri="/orders",method="POST",status="201"} 1532
shop-app (/actuator/prometheus) <--scrape 15s-- Prometheus --query--> Grafana
                                                    |
                                               Alertmanager (p99 > 1s → Slack)

Spring Boot đã tự có sẵn rất nhiều metric: http.server.requests (theo uri, status), jvm.memory.used, hikaricp.connections.active, cache.gets… Bạn chỉ thêm metric nghiệp vụ.

Bẫy cardinality: đừng đặt tag có giá trị vô hạn như userId, orderId. Mỗi giá trị tag là một time series, Prometheus sẽ phình bộ nhớ.

Vậy mở /actuator ra internet có sao không? Người ngoài gọi /actuator/env thì thấy gì?

Bạn lo đúng chỗ. /env có thể lộ cấu hình (Boot 3 mặc định che giá trị nhạy cảm, nhưng đừng dựa vào đó), /heapdump tải về cả heap chứa password và token, /loggers cho phép đổi log level. Cách an toàn gồm 3 lớp:

Lớp 1: Port riêng cho management

server:
  port: 8080            # traffic người dùng, qua Ingress
management:
  server:
    port: 8081          # chỉ trong cluster, không expose qua Ingress
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
Internet ──→ Ingress ──→ :8080  /orders, /products
                          :8081  /actuator/*  ←── chỉ Prometheus, K8s probe (nội bộ)

Lớp 2: Spring Security

@Bean
@Order(1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
    return http
        .securityMatcher(EndpointRequest.toAnyEndpoint())
        .authorizeHttpRequests(a -> a
            .requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll()
            .anyRequest().hasRole("OPS"))
        .httpBasic(Customizer.withDefaults())
        .build();
}

Lớp 3: Chỉ expose cái cần, và show-details: when-authorized để người ngoài chỉ thấy {"status":"UP"}.

EndpointMức nguy hiểmGợi ý
health, infoThấpMở (ẩn details)
prometheus, metricsTrung bình (lộ tên uri, cấu trúc)Chỉ nội bộ
loggers, env, beans, configpropsCaoPort nội bộ + role OPS
heapdump, threaddump, shutdownRất caoTắt, bật khi cần debug

Readiness có db mà DB chập chờn vài giây thì cả 3 pod cùng bị gỡ khỏi load balancer à? Vậy còn tệ hơn?

Bạn hiểu đúng, và đây là bẫy thật. Nếu DB dùng chung cho cả 3 pod, DB chết thì cả 3 cùng DOWN, user nhận lỗi 503 từ LB thay vì 500 từ app; kết quả gần như nhau. Cái lợi của readiness là ở chỗ một pod hỏng riêng (mất kết nối, pool cạn).

Đưa vào readiness?   DB chính → có   |  Redis cache → tùy (có fallback thì không)
                     payment   → không (circuit breaker)

Phỏng vấn hỏi Actuator và monitoring thì trả lời sao?

Q1. Liveness và readiness probe khác nhau thế nào?

🇻🇳 Tiếng Việt:

Liveness hỏi process còn hoạt động đúng không; fail thì Kubernetes restart container. Readiness hỏi app sẵn sàng nhận traffic chưa; fail thì pod bị gỡ khỏi Service nhưng không restart. Spring Boot Actuator có sẵn hai group /health/liveness và /health/readiness. Không đưa DB vào liveness vì restart không chữa được DB.

🇬🇧 English:

Liveness asks whether the process is still working; if it fails, Kubernetes restarts the container. Readiness asks whether the app can accept traffic; if it fails, the pod is removed from the Service but not restarted. Spring Boot Actuator provides /health/liveness and /health/readiness groups. External dependencies like the database should not be in liveness, because restarting does not fix the database.

Q2. Bạn thu thập metrics của ứng dụng Spring Boot như thế nào?

🇻🇳 Tiếng Việt:

Dùng Micrometer qua Actuator, với registry Prometheus. Spring Boot tự có metric HTTP, JVM, Hikari, cache. Metric nghiệp vụ thì tạo Counter, Timer qua MeterRegistry hoặc @Timed. Prometheus scrape /actuator/prometheus, Grafana vẽ, Alertmanager cảnh báo. Tránh tag có cardinality cao như userId.

🇬🇧 English:

I use Micrometer through Actuator with the Prometheus registry. Spring Boot already publishes HTTP, JVM, Hikari and cache metrics. For business metrics I register Counters and Timers on the MeterRegistry or use @Timed. Prometheus scrapes /actuator/prometheus, Grafana visualizes it, and Alertmanager sends alerts. I avoid high-cardinality tags like user IDs.

Q3. Làm sao expose Actuator an toàn?

🇻🇳 Tiếng Việt:

Chỉ expose endpoint cần thiết, chạy management trên port riêng không đi qua Ingress, bảo vệ bằng Spring Security với EndpointRequest, ẩn chi tiết health với người chưa xác thực, và tắt các endpoint nguy hiểm như heapdump, env, shutdown.

🇬🇧 English:

Expose only the endpoints you need, run management on a separate port that is not routed through the ingress, secure it with Spring Security using EndpointRequest, hide health details from unauthenticated users, and disable dangerous endpoints such as heapdump, env and shutdown.

Cách nhớ ngắn gọn


Tự kiểm tra

Readiness fail thì Kubernetes làm gì? Liveness fail thì sao?

Readiness fail: gỡ pod khỏi Service, không restart. Liveness fail: restart container.

Vì sao không đưa health của DB vào liveness?

DB chết thì mọi pod đều fail, K8s restart hàng loạt mà không chữa được gì.

Tên component của một HealthIndicator lấy từ đâu?

Từ tên bean, bỏ hậu tố HealthIndicator nếu có.

Vì sao không gắn tag orderId vào metric?

Mỗi giá trị tag tạo một time series mới, gây bùng nổ cardinality trong Prometheus.

Ba lớp để mở Actuator an toàn?

Port management riêng, Spring Security, chỉ expose endpoint cần thiết.