Spring Cloud + Kubernetes: kiến trúc production

4 câu hỏi phỏng vấn~25 phútTừ khóa: Service DNS, ConfigMap, Secret, probes, Ingress, HPA, Spring Cloud Kubernetes, Service Mesh

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 đề

3. Giải pháp: chia vai trò

ViệcTrước (Spring Cloud)Trên Kubernetes
Service DiscoveryEurekaService + CoreDNS
Load balancingSpring Cloud LoadBalancerService (kube-proxy)
ConfigConfig ServerConfigMap / Secret (hoặc giữ Config Server)
Health / restartEureka heartbeatLiveness / Readiness probe
ScaleBật thêm JVM bằng tayDeployment replicas + HPA
Cửa vàoGatewayIngress, 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

KubernetesResilience4j
Restart container lỗiNgắt call tới dependency đang lỗi
Scale replicasGiới hạn concurrent calls (Bulkhead)
Readiness / LivenessTimeout, Retry có kiểm soát
Infrastructure resilienceApplication resilience
Kubernetes IngressSpring Cloud Gateway
K8s networking API, do Ingress Controller thực thiỨng dụng Spring chạy như service
Entry routing, TLSAPI routing, filter, auth, rate limit theo user
Cấu hình bằng YAML/annotationCode Java + Spring Security
Spring Cloud ConfigConfigMap / Secret
Config Server, Git backendK8s 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 K8sGắn với K8s

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

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ữ API DiscoveryClient.

🇬🇧 English:

Usually not. Kubernetes has built-in discovery through Services and DNS: order-service calls http://payment-service and 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 the DiscoveryClient API.

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/liveness và /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/liveness and /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

Tổng kết chương Spring Cloud

BàiThành phầnBài toán giải quyếtTrên Kubernetes
4.6.1Monolith → MicroservicesChia hệ thống, chọn thành phần Spring CloudVẫn áp dụng
4.6.2EurekaService tìm nhau khi IP thay đổiThường không cần (Service DNS)
4.6.3Spring Cloud ConfigCấu hình tập trung, đổi không cần build lạiTùy; ConfigMap/Secret thường đủ
4.6.4Spring Cloud GatewayMột cửa vào: route, auth, rate limitTùy nhu cầu, cân nhắc cùng Ingress
4.6.5OpenFeign + LoadBalancerGọi HTTP giữa service gọn, chia tải phía clientFeign vẫn hữu ích; LoadBalancer tùy
4.6.6Circuit Breaker + Resilience4jService chậm/lỗi không kéo sập cả hệVẫn cần
4.6.7Spring Cloud StreamGiao tiếp event qua RabbitMQ/KafkaVẫn cần nếu dùng broker
4.6.8Micrometer TracingRequest chậm ở service nàoVẫn cần
4.6.9Spring Cloud KubernetesDùng API Spring Cloud trên nền K8sTù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.