Spring Cloud + Kubernetes: kiến trúc production
Tiếp đi: đưa Spring Cloud lên Kubernetes.
Đây là bài cuối của chương Spring Cloud. Chúng ta đưa spring-cloud-demo lên Kubernetes. Điểm quan trọng nhất của bài:
Khi dùng Kubernetes, không phải mọi thành phần Spring Cloud đều còn cần.
1. Bài toán
Trên máy local / VM, hệ thống chạy thế này:
api-gateway
|
v
discovery-server (Eureka) config-server (Git)
|
+---- order-service
+---- payment-service
+---- inventory-service
+---- notification-service
Kubernetes đã có sẵn: Service Discovery qua Service + DNS, Load Balancing qua Service, cấu hình qua ConfigMap/Secret, scaling qua Deployment + HPA, health check qua probes. Vậy Spring Cloud còn vai trò gì?
2. Vấn đề
- Chạy Eureka trong K8s = hai hệ discovery song song. Pod chết, K8s biết ngay, Eureka chờ heartbeat hết hạn (~90s) mới gỡ, gateway vẫn gọi vào pod chết.
- Config Server thêm một service phải vận hành, trong khi ConfigMap/Secret có sẵn.
- Ngược lại, nghĩ "lên K8s là hết lo" cũng sai: Payment pod vẫn Running nhưng mỗi request 20 giây thì K8s không cứu được.
3. Giải pháp: chia vai trò
| Việc | Trước (Spring Cloud) | Trên Kubernetes |
|---|---|---|
| Service Discovery | Eureka | Service + CoreDNS |
| Load balancing | Spring Cloud LoadBalancer | Service (kube-proxy) |
| Config | Config Server | ConfigMap / Secret (hoặc giữ Config Server) |
| Health / restart | Eureka heartbeat | Liveness / Readiness probe |
| Scale | Bật thêm JVM bằng tay | Deployment replicas + HPA |
| Cửa vào | Gateway | Ingress, có thể vẫn kèm Gateway |
Còn lại thuộc tầng ứng dụng, K8s không làm thay: Resilience4j (timeout, circuit breaker, bulkhead), OpenFeign, Spring Cloud Stream, tracing, Saga, idempotency.
Spring Cloud Kubernetes là cầu nối khi muốn giữ cách viết Spring Cloud: DiscoveryClient đọc từ K8s API thay Eureka, đọc ConfigMap/Secret thành PropertySource, reload khi ConfigMap đổi.
4. Code từng bước
Bước 1: Dependency. Bỏ spring-cloud-starter-netflix-eureka-client và spring-cloud-starter-config khỏi các service. Thêm Actuator (để có probe):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Tùy chọn: muốn DiscoveryClient / đọc ConfigMap kiểu Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-all</artifactId>
</dependency>
Bước 2: Deployment + Service cho payment-service
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 3
selector:
matchLabels: { app: payment-service }
template:
metadata:
labels: { app: payment-service }
spec:
containers:
- name: payment
image: myregistry/payment-service:1.0.0
ports: [ { containerPort: 8080 } ]
envFrom:
- configMapRef: { name: payment-config }
- secretRef: { name: payment-secret }
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 768Mi }
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
selector: { app: payment-service }
ports:
- port: 80
targetPort: 8080
# application.yml
management:
endpoint:
health:
probes:
enabled: true
server:
shutdown: graceful
Bước 3: ConfigMap / Secret thay Config Server
apiVersion: v1
kind: ConfigMap
metadata:
name: payment-config
data:
PAYMENT_TIMEOUT: "5000"
PAYMENT_RETRY: "2"
MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "http://zipkin:9411/api/v2/spans"
# Secret tạo từ CI/CD hoặc External Secrets, không commit giá trị thật vào Git
kubectl create secret generic payment-secret \
--from-literal=SPRING_DATASOURCE_PASSWORD="$DB_PASSWORD"
Spring Boot relaxed binding: PAYMENT_TIMEOUT map vào payment.timeout.
Bước 4: Code Java: Feign gọi Service DNS, không qua Eureka
@FeignClient(name = "payment-client", url = "${clients.payment.url}")
public interface PaymentClient {
@PostMapping("/payments")
PaymentResponse pay(@RequestBody PaymentRequest request);
}
clients:
payment:
url: http://payment-service # K8s DNS, cùng namespace
# khác namespace: http://payment-service.shop.svc.cluster.local
resilience4j: # vẫn giữ nguyên như bài 4.6.6
circuitbreaker:
instances:
payment: { failure-rate-threshold: 50, wait-duration-in-open-state: 10s }
timelimiter:
instances:
payment: { timeout-duration: 3s }
Gateway route cũng đổi từ lb://payment-service sang http://payment-service (hoặc giữ lb:// nếu dùng Spring Cloud Kubernetes discovery).
Bước 5: Test
kubectl apply -f k8s/
kubectl get pods -l app=payment-service
kubectl port-forward svc/api-gateway 8080:80
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"productId":"P01","quantity":2}'
kubectl delete pod payment-service-7d9f-abcde # K8s tự tạo pod mới, request vẫn chạy
5. Cách hoạt động: kiến trúc cuối cùng
Internet
|
v
Cloud Load Balancer + TLS
|
v
Ingress Controller (nginx)
|
v
api-gateway (Spring Cloud Gateway)
auth JWT, rate limit, route, traceId
|
+---------------------+----------------------+
v v v
order-service --Feign--> payment-service inventory-service
(HPA 2-10) Resilience4j (3 pods) (2 pods)
| | |
| Outbox v |
v PostgreSQL (managed) |
RabbitMQ / Kafka <-------------------------------+
| Spring Cloud Stream
v
notification-service
ConfigMap / Secret ---> mọi Pod (env)
Probes ----------------> kubelet restart / gỡ khỏi Service
Micrometer Tracing ----> Tempo/Zipkin; Prometheus; Loki --> Grafana
Database và broker trong production thường là managed service bên ngoài cluster.
Về probe: không đưa DB vào liveness. DB tạm lỗi mà liveness fail thì K8s restart toàn bộ Payment pods, hệ thống càng sập nặng. Liveness chỉ trả lời "JVM còn sống không", readiness trả lời "có nhận traffic được không".
6. So sánh
| Kubernetes | Resilience4j |
|---|---|
| Restart container lỗi | Ngắt call tới dependency đang lỗi |
| Scale replicas | Giới hạn concurrent calls (Bulkhead) |
| Readiness / Liveness | Timeout, Retry có kiểm soát |
| Infrastructure resilience | Application resilience |
| Kubernetes Ingress | Spring Cloud Gateway |
|---|---|
| K8s networking API, do Ingress Controller thực thi | Ứng dụng Spring chạy như service |
| Entry routing, TLS | API routing, filter, auth, rate limit theo user |
| Cấu hình bằng YAML/annotation | Code Java + Spring Security |
| Spring Cloud Config | ConfigMap / Secret |
|---|---|
| Config Server, Git backend | K8s API, quản lý YAML qua GitOps |
Có refresh (/actuator/refresh, Bus) | Env cần restart; mount file hoặc Spring Cloud Kubernetes thì reload được |
| Chạy được cả ngoài K8s | Gắn với K8s |
7. Khi nào nên dùng / không nên dùng
- Bỏ Eureka khi mọi service đều chạy trong K8s. Giữ khi còn service chạy ngoài cluster (VM cũ) cần tìm nhau.
- Bỏ Config Server khi ConfigMap/Secret + GitOps đủ. Giữ khi nhiều môi trường không phải K8s, hoặc cần refresh runtime và history config tập trung.
- Giữ Gateway khi cần logic API theo Spring (JWT, rate limit theo user, aggregate). Bỏ khi Ingress/API Gateway của cloud đã đủ, giảm một network hop.
- Luôn giữ Resilience4j, Stream, tracing, Outbox, idempotency.
- Service Mesh (Istio, Linkerd): khi hệ rất lớn, cần mTLS giữa service, traffic policy chung. Nó không thay idempotency, Saga hay xử lý thanh toán trạng thái UNKNOWN.
Vậy chỉ cần HPA tăng Order Service từ 2 lên 20 pod là chịu được hàng triệu user đúng không?
Chưa đúng hẳn. HPA chỉ scale tầng ứng dụng:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-hpa
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: order-service }
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
HPA cần metrics-server và resources.requests trên container. Nhưng tăng pod thì bottleneck dời chỗ:
20 order pods x HikariCP 10 connections = 200 connections
PostgreSQL max_connections = 100
=> DB thành nút thắt mới, request chờ connection, timeout
Còn phải xem: connection pool và index DB, read replica, cache Redis, xử lý bất đồng bộ qua queue, rate limiting ở gateway, consumer throughput của broker. Và quan trọng nhất: load test + tracing để tìm bottleneck trước, rồi mới scale đúng chỗ.
3 Payment Pod đều Running và Ready nhưng mỗi request mất 20 giây. K8s có tự xử lý không?
Không. Đây chính là chỗ phân biệt infrastructure resilience và application resilience. Pod vẫn sống, probe vẫn qua, nên K8s thấy mọi thứ ổn và tiếp tục gửi traffic. Order Service phải tự bảo vệ:
order-service
| TimeLimiter 3s -> không chờ 20s
| CircuitBreaker -> lỗi nhiều thì OPEN, fail fast + fallback "PAYMENT_PENDING"
| Bulkhead 20 calls -> payment chậm không ăn hết thread của order
| Retry 1 lần + idempotency key
v
payment-service (3 pods, chậm)
Song song: xem trace (bài 4.6.8) để biết chậm ở DB hay ở cổng thanh toán bên ngoài. Tăng lên 10 pod mà thủ phạm là cổng thanh toán thì chỉ tạo thêm request chờ.
Phỏng vấn hỏi về Spring Cloud trên Kubernetes thì trả lời sao?
Q1. Deploy lên Kubernetes thì còn cần Eureka không?
🇻🇳 Tiếng Việt:
Thường là không. Kubernetes có service discovery sẵn qua Service và DNS: order-service gọi
http://payment-service, K8s phân phối tới các pod đang Ready. Chạy thêm Eureka là hai hệ discovery song song và Eureka phát hiện pod chết chậm hơn. Em chỉ giữ Eureka nếu còn service chạy ngoài cluster cần tìm nhau, hoặc dùng Spring Cloud Kubernetes nếu muốn giữ APIDiscoveryClient.
🇬🇧 English:
Usually not. Kubernetes has built-in discovery through Services and DNS: order-service calls
http://payment-serviceand Kubernetes routes to ready pods. Running Eureka too means two discovery systems, and Eureka detects dead pods more slowly. I'd keep Eureka only if some services run outside the cluster, or use Spring Cloud Kubernetes to keep theDiscoveryClientAPI.
Q2. Kubernetes có thay thế Circuit Breaker không?
🇻🇳 Tiếng Việt:
Không. Kubernetes lo hạ tầng: restart container, scale, gỡ pod không Ready. Circuit breaker lo tầng ứng dụng: khi dependency chậm hay lỗi thì fail fast, có fallback, không để thread bị kẹt. Ví dụ payment pod vẫn Running nhưng mỗi request 20 giây thì K8s không làm gì, order-service vẫn cần timeout, circuit breaker và bulkhead.
🇬🇧 English:
No. Kubernetes handles infrastructure: restarting containers, scaling, removing unready pods. A circuit breaker handles application-level failures: when a dependency is slow or failing it fails fast with a fallback so threads don't pile up. If payment pods are running but take 20 seconds per request, Kubernetes does nothing, so order-service still needs timeouts, a circuit breaker and a bulkhead.
Q3. Ingress khác Spring Cloud Gateway thế nào? Có cần cả hai?
🇻🇳 Tiếng Việt:
Ingress là API của Kubernetes mô tả routing HTTP từ ngoài vào Service, do Ingress Controller như nginx thực thi, thường lo TLS và routing theo host/path. Spring Cloud Gateway là ứng dụng API gateway, làm được logic ở tầng ứng dụng: xác thực JWT bằng Spring Security, rate limit theo user, filter tùy biến. Không bắt buộc cả hai. Nếu Ingress hoặc API gateway của cloud đã đủ thì em bỏ Spring Cloud Gateway để bớt một hop.
🇬🇧 English:
Ingress is a Kubernetes API that defines external HTTP routing to Services, enforced by an Ingress Controller like nginx, usually for TLS and host or path routing. Spring Cloud Gateway is an application-level API gateway with JWT validation through Spring Security, per-user rate limiting and custom filters. You don't always need both; if Ingress or a cloud API gateway covers the requirements, I drop Spring Cloud Gateway to save a network hop.
Q4. Liveness và readiness probe khác nhau thế nào?
🇻🇳 Tiếng Việt:
Liveness hỏi "container có cần restart không", fail thì kubelet restart. Readiness hỏi "pod nhận traffic được chưa", fail thì pod bị gỡ khỏi Service nhưng không restart. Spring Boot Actuator có sẵn
/actuator/health/livenessvà/readiness. Em không đưa database vào liveness, vì DB lỗi tạm thời sẽ khiến K8s restart hết pod và làm sự cố nặng thêm.
🇬🇧 English:
Liveness asks whether the container must be restarted; if it fails, the kubelet restarts it. Readiness asks whether the pod can take traffic; if it fails, the pod is removed from the Service but not restarted. Spring Boot Actuator exposes
/actuator/health/livenessand/readiness. I never put the database in liveness, because a temporary DB outage would restart every pod and make the incident worse.
Q5. Bạn sẽ scale một hệ microservices Spring Boot thế nào?
🇻🇳 Tiếng Việt:
Đầu tiên em tìm bottleneck bằng monitoring, tracing và load test. Sau đó mới chọn cách: scale ngang bằng HPA, cache với Redis, tối ưu DB và connection pool, read replica, chuyển việc không gấp sang xử lý bất đồng bộ qua queue, rate limiting ở gateway. Tăng số pod mà không xem DB thì chỉ dời bottleneck sang database.
🇬🇧 English:
First I find the bottleneck with monitoring, tracing and load testing. Then I choose the fix: horizontal scaling with HPA, caching with Redis, database and connection pool tuning, read replicas, moving non-urgent work to async processing through a queue, and rate limiting at the gateway. Adding pods without looking at the database just moves the bottleneck there.
Cách nhớ ngắn gọn
- K8s thay: Eureka → Service DNS, LoadBalancer → Service, Config Server → ConfigMap/Secret.
- K8s không thay: Resilience4j, Stream, tracing, Outbox, idempotency.
- Ingress = cửa vào hạ tầng; Gateway = logic API. Có thể chỉ cần một.
- Liveness = restart; readiness = nhận traffic. DB không vào liveness.
- Scale: tìm bottleneck trước, HPA sau.
Tổng kết chương Spring Cloud
| Bài | Thành phần | Bài toán giải quyết | Trên Kubernetes |
|---|---|---|---|
| 4.6.1 | Monolith → Microservices | Chia hệ thống, chọn thành phần Spring Cloud | Vẫn áp dụng |
| 4.6.2 | Eureka | Service tìm nhau khi IP thay đổi | Thường không cần (Service DNS) |
| 4.6.3 | Spring Cloud Config | Cấu hình tập trung, đổi không cần build lại | Tùy; ConfigMap/Secret thường đủ |
| 4.6.4 | Spring Cloud Gateway | Một cửa vào: route, auth, rate limit | Tùy nhu cầu, cân nhắc cùng Ingress |
| 4.6.5 | OpenFeign + LoadBalancer | Gọi HTTP giữa service gọn, chia tải phía client | Feign vẫn hữu ích; LoadBalancer tùy |
| 4.6.6 | Circuit Breaker + Resilience4j | Service chậm/lỗi không kéo sập cả hệ | Vẫn cần |
| 4.6.7 | Spring Cloud Stream | Giao tiếp event qua RabbitMQ/Kafka | Vẫn cần nếu dùng broker |
| 4.6.8 | Micrometer Tracing | Request chậm ở service nào | Vẫn cần |
| 4.6.9 | Spring Cloud Kubernetes | Dùng API Spring Cloud trên nền K8s | Tùy nhu cầu tích hợp |
Kết luận: Kubernetes không thay thế toàn bộ Spring Cloud. Nó lo deployment, networking, config và scaling ở tầng hạ tầng. Spring Cloud vẫn lo phần ứng dụng: gọi nhau, chịu lỗi, messaging và quan sát.
Tự kiểm tra
Order Service gọi Payment ở cùng namespace bằng URL nào?
http://payment-service (đầy đủ: payment-service.<namespace>.svc.cluster.local).
3 Payment Pod Running + Ready nhưng chậm 20s. Nên làm gì?
Timeout, Circuit Breaker, Bulkhead ở order-service, rồi dùng tracing tìm bottleneck. Chỉ tăng pod là chưa đủ.
Vì sao không đưa DB check vào liveness?
DB lỗi tạm sẽ làm liveness fail, K8s restart mọi pod, sự cố lan rộng hơn.
HPA cần điều kiện gì để scale theo CPU?
Metrics pipeline (metrics-server) và resources.requests CPU trên container.
Khi nào vẫn giữ Eureka dù đã có Kubernetes?
Khi còn service chạy ngoài cluster (VM) cần đăng ký/tìm nhau với service trong cluster.