Spring Boot Actuator: health, metrics, mở endpoint an toàn
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)
- pod-1 mới khởi động, chưa load xong cache sản phẩm, nhưng LB đã gửi request vào.
- pod-2 mất kết nối DB, request vào đều 500, nhưng process vẫn chạy nên K8s tưởng ổn.
2. Vấn đề
- K8s chỉ biết process còn hay chết. Process còn không có nghĩa là app phục vụ được.
- Tự viết
@GetMapping("/ping")trả "OK" thì không kiểm tra DB, Redis, disk. - Không có số liệu: bao nhiêu request/giây, latency p99, pool DB còn bao nhiêu connection?
3. Giải pháp: Actuator + health groups + Micrometer
| Thành phần | Trả lời câu hỏi |
|---|---|
/actuator/health | App và các phụ thuộc (DB, Redis, disk) có ổn không? |
/actuator/health/liveness | Process 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/prometheus | Số liệu: request, latency, JVM, pool… (qua Micrometer) |
/actuator/info | Version, 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
| Liveness | Readiness | |
|---|---|---|
| Câu hỏi | Process còn hoạt động đúng? | Nhận traffic được chưa? |
| Fail thì K8s | Restart container | Gỡ khỏi load balancer |
| Nên chứa | Trạ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ô ích | Có |
7. Khi nào nên dùng / không nên dùng
- Nên: mọi app chạy production. Actuator gần như bắt buộc.
- Không nên:
exposure.include: "*"trên port public./env,/heapdumplộ secret. - HealthIndicator gọi hệ thống ngoài phải có timeout ngắn, nếu không probe chậm làm K8s tưởng pod chết.
/orders chậm bao nhiêu thì cần metrics.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"}.
| Endpoint | Mức nguy hiểm | Gợi ý |
|---|---|---|
| health, info | Thấp | Mở (ẩn details) |
| prometheus, metrics | Trung bình (lộ tên uri, cấu trúc) | Chỉ nội bộ |
| loggers, env, beans, configprops | Cao | Port nội bộ + role OPS |
| heapdump, threaddump, shutdown | Rất cao | Tắ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).
- Đặt
failureThreshold(ví dụ 3 lần × 5s) để không gỡ pod chỉ vì một lần chập chờn. - Phụ thuộc không bắt buộc (payment chỉ dùng lúc checkout) thì không đưa vào readiness; xử lý bằng circuit breaker trong code, để shop vẫn xem sản phẩm được.
- Tuyệt đối không đưa phụ thuộc ngoài vào liveness: restart không chữa được DB.
Đư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/livenessvà/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/livenessand/health/readinessgroups. 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 quaMeterRegistryhoặ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 theMeterRegistryor 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
- Liveness fail → restart. Readiness fail → gỡ traffic.
- DB vào readiness, không vào liveness.
- Micrometer = SLF4J của metrics; tránh tag cardinality cao.
- Actuator an toàn = port riêng + Security + expose tối thiểu.
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.