Được xây dựng trên nền tảng Spring Boot, Spring Cloud không phải là một framework mới mà là một tập hợp các “vũ khí” giúp đơn giản hóa việc xây dựng và triển khai ứng dụng theo kiến trúc microservices bằng cách cung cấp các giải pháp sẵn có cho những bài toán phổ biến.
Đọc bài viết này để biết thêm về:
- Spring Cloud là gì? Giúp lập trình viên giải quyết vấn đề gì?
- Spring Cloud quản lý cấu hình tập trung như thế nào?
- Tìm hiểu thêm về Dịch Vụ (Service Discovery & Registry)
- API Gateway
- Tăng cường khả năng chịu lỗi
Spring Cloud là gì và giải quyết vấn đề gì?
Spring Cloud là gì?
Spring Cloud là một framework cung cấp bộ công cụ toàn diện để xây dựng và vận hành các ứng dụng theo kiến trúc microservices. Mục tiêu chính của nó là đơn giản hóa sự phức tạp vốn có của các hệ thống phân tán bằng cách cung cấp các giải pháp đã được kiểm chứng cho những bài toán phổ biến. Cụ thể, Spring Cloud cung cấp các implementation cho những design pattern quan trọng như:
- Service Discovery (Khám phá dịch vụ)
- Centralized Configuration (Quản lý cấu hình tập trung)
- Circuit Breakers (Cơ chế ngắt mạch)
- Intelligent Routing (Định tuyến thông minh)
- Distributed Tracing (Truy vết phân tán)
Mối quan hệ với Spring Boot
Mối quan hệ và sự phân chia vai trò giữa Spring Boot và Spring Cloud rất rõ ràng:
- Spring Boot tập trung vào việc đơn giản hóa quá trình phát triển và đóng gói một service độc lập. Nó cung cấp cơ chế auto-configuration, quản lý dependency và máy chủ nhúng (embedded server) để bạn có thể nhanh chóng tạo ra một ứng dụng sẵn sàng chạy (production-ready). Phạm vi của Spring Boot là bên trong một microservice.
- Spring Cloud giải quyết các vấn đề phát sinh khi nhiều service được tạo bởi Spring Boot cần tương tác với nhau trong một môi trường phân tán. Nó cung cấp hạ tầng và các pattern để các service này có thể giao tiếp, chịu lỗi và phối hợp một cách hiệu quả. Phạm vi của Spring Cloud là giữa các microservice.
Nói một cách khác, Spring Boot cho phép bạn xây dựng các service, còn Spring Cloud cung cấp nền tảng để các service đó kết nối và vận hành một cách tin cậy như một hệ thống thống nhất.
Đọc chi tiết: Spring Boot là gì: Chi tiết cách xây dựng ứng dụng với Spring Boot
Tại sao Spring Cloud hỗ trợ đắc lực cho quá trình chuyển đổi sang microservice?
Kiến trúc microservices là một mô hình kiến trúc phần mềm, trong đó một ứng dụng được cấu thành từ một tập hợp các dịch vụ (services) nhỏ, độc lập và có khả-năng-triển-khai-độc-lập (independently deployable). Mỗi service thường được xây dựng xoay quanh một nghiệp vụ (business capability), chạy trong tiến trình riêng và giao tiếp với các service khác thông qua các API được định nghĩa rõ ràng, thường là qua giao thức HTTP/REST hoặc gRPC.
Mô hình này đối lập với kiến trúc Monolithic (nguyên khối), nơi mà tất cả các thành phần chức năng được đóng gói và triển khai như một đơn vị duy nhất. Trong khi Monolithic đơn giản hơn về mặt vận hành ban đầu, nó bộc lộ nhiều nhược điểm khi hệ thống phát triển về quy mô và độ phức tạp, chẳng hạn như:
- Khó mở rộng: Phải nhân bản toàn bộ ứng dụng thay vì chỉ mở rộng thành phần đang chịu tải cao.
- Ràng buộc công nghệ : Khó áp dụng công nghệ mới cho một phần của hệ thống.
- Thiếu khả năng phục hồi: Lỗi ở một module có thể làm sập toàn bộ ứng dụng.
- Chu trình phát triển chậm: Mọi thay đổi nhỏ đều yêu cầu build và triển khai lại toàn bộ khối ứng dụng lớn.
Các vấn đề phát sinh mà Spring Cloud giải quyết
Khi chia tách một ứng dụng nguyên khối thành các microservice, chúng ta phải giải quyết các vấn đề phức tạp về tương tác và vận hành giữa các service này.
- Service Discovery (Độ phân giải vị trí dịch vụ): Trong một môi trường linh hoạt như Cloud hoặc containerized (Docker/Kubernetes), các instance của service được tạo ra và hủy đi một cách tự động. Địa chỉ IP và port của chúng là động và không cố định. Vậy làm thế nào một service client có thể tìm ra được địa chỉ mạng (network location) chính xác của một service mà nó cần gọi tại một thời điểm bất kỳ?
- Externalized Configuration (Ngoại vi hóa cấu hình): Mỗi service cần các thông tin cấu hình như chuỗi kết nối database, credentials, địa chỉ của các dịch vụ khác, feature flags… Quản lý các file cấu hình này phân tán ở từng service là một cơn ác mộng. Khi một thuộc tính chung (ví dụ: mật khẩu database) thay đổi, làm thế nào để cập nhật cho hàng chục service một cách đồng bộ mà không cần phải restart lại toàn bộ?
- Fault Tolerance (Khả năng chịu lỗi và chống lỗi dây chuyền): Trong một hệ thống phân tán, lỗi mạng hoặc một service bị quá tải/sập là điều không thể tránh khỏi. Một lời gọi từ service A đến service B bị treo có thể làm cạn kiệt tài nguyên (thread pool) của A. Nếu nhiều service cùng gọi đến B, sự cố của B có thể lan truyền và gây ra lỗi dây chuyền (cascading failure), làm sập cả một phần hoặc toàn bộ hệ thống.
- API Gateway: Việc public hàng chục endpoint của các microservice ra bên ngoài cho client (web, mobile) là không thực tế và thiếu an toàn. Cần một điểm vào (entry point) duy nhất để xử lý các tác vụ chung (cross-cutting concerns) như xác thực (authentication), giới hạn truy cập (rate limiting), giám sát (monitoring), và định tuyến (routing) các request đến đúng service nội bộ.
- Cân bằng tải phía : Để đảm bảo tính sẵn sàng cao (high availability), mỗi service thường chạy nhiều instance. Khi service A gọi service B, làm thế nào nó có thể phân phối các yêu cầu một cách thông minh qua các instance của B để tránh tình trạng một instance bị quá tải trong khi các instance khác lại đang rảnh rỗi?
Spring Cloud giải quyết các vấn đề này như thế nào?
Spring Cloud cung cấp một bộ các project được xây dựng dựa trên hệ sinh thái Spring Boot để đưa ra các giải pháp đã được chuẩn hóa cho những thách thức trên.
Thách thức Kỹ thuật | Giải pháp của Spring Cloud | Cơ chế hoạt động |
Độ phân giải vị trí dịch vụ | Spring Cloud Netflix Eureka | Cung cấp một Service Registry. Mỗi microservice khi khởi động sẽ đăng ký (register) thông tin địa chỉ mạng của nó với Eureka Server và gửi các tín hiệu “heartbeat” định kỳ. Các service client sẽ truy vấn (query) Eureka Server để lấy danh sách các instance đang hoạt động của service mà nó cần gọi. |
Ngoại vi hóa cấu hình | Spring Cloud Config | Cung cấp một Config Server quản lý cấu hình tập trung, thường được sao lưu bởi một Git repository. Các microservice trở thành client, lấy cấu hình từ Config Server khi khởi động và có thể được cấu hình để tự động làm mới (refresh) các thuộc tính mà không cần khởi động lại. |
Khả năng chịu lỗi và chống lỗi dây chuyền | Spring Cloud Circuit Breaker (với Resilience4j) | Triển khai mô hình Circuit Breaker. Nó bao bọc các lời gọi mạng trong một state machine (CLOSED, OPEN, HALF-OPEN). Khi số lượng lỗi vượt ngưỡng, breaker sẽ “mở” (OPEN), các lời gọi sau đó sẽ thất bại ngay lập tức (fail-fast) thay vì cố gắng kết nối đến service đang lỗi, đồng thời cung cấp một phương án dự phòng (fallback). |
API Gateway | Spring Cloud Gateway | Đóng vai trò là một Reverse Proxy và API Gateway. Nó định nghĩa các routes để ánh xạ request từ client tới các microservice tương ứng dựa trên các điều kiện (predicates) như đường dẫn, header… Gateway cũng áp dụng các filters để xử lý các tác vụ cross-cutting concerns. |
Cân bằng tải | Spring Cloud LoadBalancer | Cung cấp một cơ chế Client-Side Load Balancing. Khi được tích hợp với service discovery (như Eureka), nó sẽ lấy danh sách các instance có sẵn và áp dụng một thuật toán (ví dụ: Round Robin) để chọn ra một instance cho mỗi request, phân phối đều tải trọng. |
Bây giờ hãy cùng tìm hiểu chi tiết về từng tính năng trong Spring Cloud nhé.
Quản lý cấu hình tập trung với Spring Cloud Config
Vấn đề:
Hãy tưởng tượng hệ thống của bạn có 20 microservices. Mỗi service lại có một file application.yml chứa các cấu hình như:
- Chuỗi kết nối database
- Địa chỉ của message broker (Kafka/RabbitMQ)
- Credentials của các dịch vụ bên thứ ba
- Các feature flag để bật/tắt tính năng
Khi một thông tin chung thay đổi, ví dụ như bạn cần đổi mật khẩu của database. Quy trình “thủ công” sẽ là:
- Mở project của từng service.
- Sửa lại giá trị trong file
application.yml
. - Build lại file
.jar
. - Triển khai (deploy) lại cả 20 service.
Quy trình này không chỉ tốn thời gian, công sức mà còn tiềm ẩn rủi ro sai sót. Việc quản lý các thông tin nhạy cảm (secrets) trong code cũng là một hành vi vi phạm bảo mật.
Giải pháp: Spring Cloud Config Server
Spring Cloud Config cung cấp một giải pháp thanh lịch cho bài toán này. Nó hoạt động theo mô hình client-server:
- Config Server: Là một service trung tâm, có nhiệm vụ duy nhất là đọc các file cấu hình từ một nguồn (backend) và cung cấp chúng qua API. Nguồn backend phổ biến nhất là một Git repository.
- Config Client: Là các microservice của bạn. Thay vì đọc file application.yml ở local, khi khởi động, chúng sẽ “hỏi” Config Server để lấy cấu hình của mình.
Mô hình này giúp tách biệt hoàn toàn cấu hình ra khỏi mã nguồn, cho phép bạn thay đổi cấu hình mà không cần phải build hay triển khai lại ứng dụng.
Hướng dẫn thực hành
Chúng ta sẽ xây dựng một ví dụ đơn giản gồm 1 Config Server và 1 Config Client.
Yêu cầu: Chuẩn bị một Git repository (trên GitHub, GitLab,…) chứa các file cấu hình. Ví dụ, tạo một repo có cấu trúc:
/my-configs
├── application.yml
└── order-service.yml
Nội dung file application.yml
(cấu hình chung):
common:
message: "Hello from default config!"
Nội dung file order-service.yml
(cấu hình riêng cho order-service):
order:
discount:
percentage: 15
Cài đặt Config Server
Đây là một Spring Boot application độc lập.
- Thêm Dependencies (
pom.xml
):
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- Kích hoạt Config Server: Thêm annotation
@EnableConfigServer
vào class Application chính.
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
- Cấu hình Server (
application.yml
): File này chỉ định port cho server và đường dẫn đến Git repository chứa cấu hình.
server:
port: 8888 # Port mặc định cho config server
spring:
cloud:
config:
server:
git:
uri: https://github.com/your-username/my-configs.git # Thay bằng URI repo của bạn
# clone-on-start: true # (Optional) Kéo repo về ngay khi khởi động
Bây giờ, hãy khởi chạy Config Server. Bạn có thể kiểm tra bằng cách truy cập http://localhost:8888/order-service/default
. Bạn sẽ thấy một JSON trả về chứa nội dung của cả 2 file application.yml
và order-service.yml
.
Cấu hình Config Client
Đây là microservice order-service
.
- Thêm Dependencies (
pom.xml
):
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- Cấu hình Bootstrap:
Để client biết địa chỉ Config Server ngay khi bắt đầu khởi động, chúng ta cần tạo một file src/main/resources/bootstrap.yml
(thay vì application.yml
).
spring:
application:
name: order-service # Tên này phải trùng với tên file cấu hình trong repo Git
cloud:
config:
uri: http://localhost:8888 # Địa chỉ của Config Server
Lưu ý: spring.application.name
rất quan trọng, Config Server sẽ dựa vào tên này để tìm file cấu hình tương ứng (order-service.yml
).
- Cấu hình Client:
Thêm dependency spring-boot-starter-actuator
.
Trong application.yml
của order-service
, thêm dòng sau để expose endpoint refresh
:
management:
endpoints:
web:
exposure:
include: "refresh"
Thêm annotation @RefreshScope
vào class Controller. Annotation này báo cho Spring biết rằng bean này có thể được tạo lại khi có sự kiện refresh.
@RestController
@RefreshScope // Rất quan trọng!
public class ConfigController {
// ... nội dung như cũ
}
- Quy trình Refresh:
- Chạy cả Config Server và order-service. Truy cập http://localhost:8080/config và xem giá trị ban đầu.
- Vào Git repository, sửa giá trị order.discount.percentage từ 15 thành 20 và commit.
- Truy cập lại http://localhost:8080/config, giá trị vẫn là 15.
- Mở một terminal hoặc Postman, gửi một request POST rỗng đến endpoint của Actuator: curl -X POST http://localhost:8080/actuator/refresh
- Truy cập lại http://localhost:8080/config. Bây giờ bạn sẽ thấy giá trị đã được cập nhật thành 20 mà không cần khởi động lại service!
Lưu ý: Trong môi trường thực tế, việc gọi API refresh thủ công không khả thi. Thay vào đó, người ta thường dùng Spring Cloud Bus kết hợp với RabbitMQ hoặc Kafka. Khi có commit mới trong Git, một webhook sẽ kích hoạt và gửi tin nhắn qua message bus, tất cả các instance của service sẽ tự động nhận được tín hiệu và refresh cấu hình.
Netflix Eureka – Service Registry Pattern của Spring Cloud
Vấn đề
Trong kiến trúc microservices, việc gọi từ service này đến service khác là hoạt động diễn ra liên tục. Ở môi trường phát triển, bạn có thể tạm “hard-code” địa chỉ http://localhost:8081 trong file cấu hình. Nhưng trên môi trường production, cách tiếp cận này hoàn toàn thất bại. Tại sao?
- Địa chỉ IP động: Trong các nền tảng cloud (AWS, Azure) hoặc container (Docker, Kubernetes), các service instance được gán địa chỉ IP một cách tự động và có thể thay đổi sau mỗi lần khởi động lại hoặc redeploy.
- Tự động co giãn (Auto-scaling): Để đáp ứng tải, hệ thống có thể tự động tạo thêm hoặc xóa bớt các instance của một service. Việc cập nhật danh sách địa chỉ IP này bằng tay là không thể.
- Tính sẵn sàng (Availability): Nếu một instance tại địa chỉ IP đã được hard-code gặp sự cố, hệ thống không có cách nào tự động chuyển hướng request sang một instance khác đang khỏe mạnh.
Bài toán đặt ra là: Cần một cơ chế tự động và linh hoạt để một service có thể tìm ra địa chỉ mạng (network location) hiện tại của service khác chỉ bằng cách sử dụng một cái tên logic.
Giải pháp: Service Registry Pattern và Netflix Eureka
Service Registry Pattern như một “danh bạ điện thoại” động cho toàn bộ hệ thống microservice của bạn. Nó hỗ trợ:
- Đăng ký (Registration): Khi một service instance khởi động, nó sẽ “gọi điện” cho danh bạ và nói: “Tôi là product-service, đang chạy tại địa chỉ
10.20.30.40:8081
“. - Khám phá (Discovery): Khi order-service muốn gọi product-service, nó sẽ hỏi danh bạ: “Cho tôi xin địa chỉ của product-service”. Danh bạ sẽ trả về một danh sách các địa chỉ đang hoạt động.
Spring Cloud Netflix Eureka là một project của Spring Cloud giúp triển khai mô hình này một cách hiệu quả.
Cách hoạt động của Eureka
- Eureka Server: Là service “danh bạ” trung tâm. Nó cung cấp một dashboard để theo dõi trạng thái của tất cả các microservice.
- Eureka Client: Là một thư viện được tích hợp vào mỗi microservice.
- Khi service khởi động, client sẽ tự động đăng ký (register) thông tin của service đó với Eureka Server.
- Định kỳ, client sẽ gửi một tín hiệu heartbeat (nhịp tim) đến Server để báo rằng “tôi vẫn còn sống”. Nếu Server không nhận được heartbeat trong một khoảng thời gian nhất định, nó sẽ coi như instance đó đã chết và loại bỏ khỏi danh bạ.
- Client cũng định kỳ kéo (fetch) bản sao của danh bạ từ Server về và lưu trữ ở local cache để tăng tốc độ truy vấn và khả năng chịu lỗi.
Hướng dẫn thực hành
Chúng ta sẽ xây dựng một hệ thống gồm:
- eureka-server: Service danh bạ.
- product-service: Service đăng ký với tên
PRODUCT-SERVICE
. - order-service: Service cũng đăng ký và sẽ gọi đến
PRODUCT-SERVICE
.
1. Thiết lập Eureka Server
a. Dependencies (pom.xml
):
XML
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
b. Kích hoạt Eureka Server: Trong class Application chính, thêm annotation @EnableEurekaServer
.
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Cấu hình Server (application.yml
):
server:
port: 8761 # Port mặc định của Eureka
eureka:
client:
register-with-eureka: false # Server không cần tự đăng ký với chính nó
fetch-registry: false # Server không cần lấy thông tin từ chính nó
Khởi chạy eureka-server
. Truy cập http://localhost:8761
, bạn sẽ thấy dashboard của Eureka.
2. Cấu hình Microservice Clients (product-service
& order-service
)
Cả hai service này sẽ có cấu hình tương tự nhau.
a. Dependencies (pom.xml
):
XML
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
b. Kích hoạt Eureka Client:
Trong class Application chính, thêm annotation @EnableDiscoveryClient
.
Java
@SpringBootApplication
@EnableDiscoveryClient
public class ProductServiceApplication {
// ...
}
c. Cấu hình Client (application.yml
của product-service
):
spring:
application:
name: product-service # Tên logic sẽ được đăng ký
server:
port: 8081 # Chạy ở port 8081
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # Địa chỉ Eureka Server
Tương tự, cấu hình cho order-service
với name: order-service và port: 8082
.
Bây giờ, hãy chạy product-service
và order-service
. Quay lại dashboard Eureka (http://localhost:8761
), bạn sẽ thấy PRODUCT-SERVICE
và ORDER-SERVICE
xuất hiện trong danh sách “Instances currently registered”.
3. Giao tiếp giữa các Service
Giờ là phần quan trọng nhất: làm sao order-service
gọi được product-service
mà không cần biết địa chỉ IP/port.
Cách làm được khuyến nghị: Sử dụng RestTemplate
với @LoadBalanced
a. Tạo một RestTemplate
Bean trong order-service
: Tạo một class config và khai báo bean. Annotation
@LoadBalanced
@Configuration
public class AppConfig {
@Bean
@LoadBalanced // Bật tính năng client-side load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
b. Tạo một API trong product-service
để order-service
gọi:
// Trong ProductController của product-service
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public String getProductById(@PathVariable String id) {
// Giả lập trả về thông tin sản phẩm
return "Product details for ID: " + id;
}
}
c. Gọi product-service
từ order-service
: Trong order-service
, inject RestTemplate
và gọi đến product-service
bằng tên logic đã đăng ký.
// Trong OrderController của order-service
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{orderId}")
public String getOrderDetails(@PathVariable String orderId) {
// Gọi đến product-service bằng tên logic "PRODUCT-SERVICE"
// Spring sẽ tự động phân giải tên này thành địa chỉ IP:port thực tế
String productId = "p123"; // Giả lập lấy product ID từ order
String productInfo = restTemplate.getForObject(
"http://PRODUCT-SERVICE/products/{id}", // <-- Chú ý: dùng tên service, không dùng IP
String.class,
productId
);
return "Order ID: " + orderId + " | Contains -> [ " + productInfo + " ]";
}
}
d. Kiểm tra: Chạy cả 3 service. Dùng Postman hoặc trình duyệt truy cập vào http://localhost:8082/orders/o456
. Kết quả trả về sẽ là: Order ID: o456 | Contains -> [ Product details for ID: p123 ]
.
Spring Cloud Gateway – giải pháp API Gateway của Spring
Khi các microservice đã có thể tự tìm thấy và giao tiếp với nhau, một câu hỏi lớn khác nảy sinh: Client (ứng dụng web, mobile) từ bên ngoài sẽ tương tác với hệ thống của chúng ta như thế nào?
Việc để client gọi trực tiếp đến từng microservice sẽ gây ra nhiều vấn đề nghiêm trọng:
- Tăng độ phức tạp phía Client: Client phải biết địa chỉ của hàng chục service, tạo ra sự phụ thuộc (coupling) chặt chẽ.
- Lặp lại logic: Các tác vụ chung như xác thực (authentication), ghi log (logging), giới hạn tần suất (rate limiting) sẽ phải được cài đặt ở mỗi service, dẫn đến trùng lặp code và khó bảo trì.
- Rủi ro bảo mật: Toàn bộ cấu trúc hệ thống bên trong bị lộ ra ngoài. Việc tái cấu trúc (refactoring) các service bên trong trở thành một cơn ác mộng vì có thể phá vỡ tích hợp với client.
Giải pháp: Spring Cloud Gateway
Mô hình API Gateway ra đời để giải quyết triệt để vấn đề này. Nó hoạt động như một lớp đệm, một “lối vào duy nhất” cho tất cả các yêu cầu từ bên ngoài. Mọi request từ client đều phải đi qua Gateway trước khi được chuyển đến các microservice tương ứng.
Spring Cloud Gateway là giải pháp API Gateway thế hệ mới của Spring, được xây dựng trên nền tảng non-blocking (không khóa), mang lại hiệu năng và khả năng mở rộng vượt trội so với Zuul 1.
Các chức năng chính của một API Gateway:
Định tuyến (Routing): Đây là nhiệm vụ cốt lõi. Gateway sẽ xác định xem một request nên được chuyển đến microservice nào dựa trên các quy tắc (ví dụ: đường dẫn URL, tên miền, header).
Xử lý các tác vụ chung (Cross-Cutting Concerns): Gateway là nơi lý tưởng để xử lý các logic chung một cách tập trung. Các logic này được triển khai dưới dạng các Filters:
- Bảo mật: Xác thực token (JWT), kiểm tra API key.
- Giám sát: Ghi log request/response.
- Tăng hiệu năng: Caching các response thường xuyên truy cập.
- Bảo vệ hệ thống: Giới hạn tần suất request (Rate Limiting).
Chuyển đổi giao thức (Protocol Translation): Có thể chuyển đổi yêu cầu từ giao thức này sang giao thức khác, ví dụ: từ REST sang gRPC.
Hướng dẫn thực hành
Chúng ta sẽ cài đặt một Gateway để định tuyến các request đến product-service
và order-service
đã tạo ở phần trước.
1. Cài đặt Spring Cloud Gateway
Tạo một Spring Boot project mới với dependency spring-cloud-starter-gateway
và spring-cloud-starter-netflix-eureka-client
(để Gateway có thể tìm thấy các service khác qua Eureka).
2. Cấu hình Routes và Filters
Phần quan trọng nhất nằm trong file application.yml
của Gateway. Chúng ta sẽ định nghĩa các quy tắc định tuyến.
server:
port: 9000 # Cổng của Gateway
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
# Route cho Product Service
- id: product-service-route
uri: lb://PRODUCT-SERVICE # lb:// -> Lấy service từ Eureka và cân bằng tải
predicates:
- Path=/api/products/** # Nếu path khớp với mẫu này...
filters:
- AddRequestHeader=X-Request-Source, api-gateway # ...thì thêm header này vào request
# Route cho Order Service
- id: order-service-route
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
Giải thích:
uri: lb://PRODUCT-SERVICE
: lb
là viết tắt của load-balancer. Dòng này yêu cầu Gateway: “Hãy tìm tất cả các instance của service có tên PRODUCT-SERVICE
trên Eureka và chuyển tiếp request đến một trong số chúng”.
predicates: - Path=/api/products/**
: Đây là điều kiện để kích hoạt route. Bất kỳ request nào có đường dẫn bắt đầu bằng /api/products/
sẽ được xử lý bởi route này.
filters: - AddRequestHeader=...
: Đây là một hành động được thực thi trước khi request được chuyển đi. Ở đây, chúng ta thêm một header X-Request-Source
với giá trị api-gateway
vào request.
3. Kiểm tra
Sau khi khởi chạy Gateway cùng với Eureka và 2 service kia, toàn bộ việc giao tiếp từ bên ngoài sẽ thay đổi:
Thay vì gọi: http://localhost:8081/products/{id}
Bây giờ Client sẽ gọi: http://localhost:9000/api/products/{id}
Request sẽ đi đến Gateway, Gateway khớp với route của product-service
, thêm header, tìm địa chỉ của product-service
qua Eureka và chuyển tiếp request đến đó. Client hoàn toàn không cần biết sự tồn tại của localhost:8081
.
Spring Cloud Circuit Breaker – Tăng cường khả năng chịu lỗi trong Microservices
Vấn đề: Hiệu ứng Domino (Cascading Failure)
Trong một hệ thống phân tán, “lỗi” không phải là một khả năng, mà là một điều chắc chắn sẽ xảy ra. Một service có thể bị sập, quá tải, hoặc phản hồi chậm do lỗi mạng. Vấn đề nghiêm trọng xảy ra khi một sự cố nhỏ lan truyền ra toàn hệ thống.
Hãy xem xét kịch bản sau: Order-Service
gọi đến Product-Service
để lấy thông tin sản phẩm. Đột nhiên, Product-Service
bị lỗi và phản hồi rất chậm. Các request từ Order-Service
sẽ bị treo lại để chờ đợi. Nếu có nhiều request đến cùng lúc, toàn bộ thread pool của Order-Service sẽ bị chiếm dụng hết bởi việc chờ đợi Product-Service
. Kết quả là Order-Service
cũng bị treo và không thể xử lý bất kỳ yêu cầu nào khác. Bất kỳ service nào gọi đến Order-Service
cũng sẽ bị ảnh hưởng theo. Đây chính là hiệu ứng domino (cascading failure), một sự cố nhỏ có thể kéo sập cả một hệ thống lớn.
Giải pháp: Mô hình Circuit Breaker
Để ngăn chặn thảm họa trên, chúng ta áp dụng mô hình Circuit Breaker. Đúng như tên gọi, nó hoạt động như một “cầu dao điện” tự động trong code của bạn.
Circuit Breaker bao bọc các lời gọi mạng và theo dõi trạng thái của chúng qua 3 trạng thái:
CLOSED
(Đóng): Trạng thái mặc định, mọi request đều được phép đi qua. Nếu số lượng lỗi vượt một ngưỡng cho phép, cầu dao sẽ “nhảy” sang trạng tháiOPEN
.OPEN
(Mở): Khi mạch đã mở, mọi request tiếp theo sẽ bị từ chối ngay lập tức mà không cần thực hiện lời gọi mạng. Thay vào đó, nó sẽ trả về lỗi hoặc thực thi một phương thức dự phòng (fallback). Điều này giúp bảo vệ hệ thống và cho service đang lỗi có thời gian để phục hồi.HALF-OPEN
(Nửa mở): Sau một khoảng thời gian chờ, cầu dao sẽ chuyển sang trạng thái này. Nó cho phép một vài request “thử nghiệm” đi qua. Nếu các request này thành công, cầu dao sẽ quay về trạng tháiCLOSED
. Nếu thất bại, nó sẽ quay lạiOPEN
.
Spring Cloud Circuit Breaker với Resilience4j
Spring Cloud Circuit Breaker là một module cung cấp một lớp trừu tượng (abstraction) cho việc tích hợp các thư viện Circuit Breaker. Resilience4j là thư viện hiện đại, nhẹ và được khuyến nghị sử dụng hiện nay (thay thế cho Netflix Hystrix đã cũ và đang trong chế độ bảo trì).
Hướng dẫn thực hành
Chúng ta sẽ tích hợp Resilience4j vào order-service
để bảo vệ lời gọi đến product-service
.
1. Cài đặt
Thêm dependency sau vào pom.xml
của order-service
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
2. Cấu hình Circuit Breaker
Trong application.yml
của order-service, ta có thể tinh chỉnh hành vi của Circuit Breaker.
resilience4j:
circuitbreaker:
instances:
productService: # Tên của Circuit Breaker, sẽ dùng trong code
slidingWindowSize: 10 # Theo dõi kết quả của 10 request gần nhất
failureRateThreshold: 50 # Nếu 50% request lỗi -> Mở mạch
waitDurationInOpenState: 10s # Thời gian chờ trước khi chuyển sang HALF-OPEN
3. Áp dụng @CircuitBreaker
và Fallback
Trong order-service
, chúng ta sẽ bọc lời gọi RestTemplate
bằng annotation @CircuitBreaker
.
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
// Annotation @CircuitBreaker sẽ bao bọc phương thức này
@CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
public String getProductInfo(String productId) {
// Lời gọi đến service có thể gây lỗi
return restTemplate.getForObject(
"http://PRODUCT-SERVICE/products/{id}",
String.class,
productId
);
}
// Phương thức Fallback, được gọi khi mạch mở (OPEN)
// Phải có cùng signature với phương thức gốc, có thể thêm Exception e
public String getProductFallback(String productId, Exception e) {
// Trả về dữ liệu cache, hoặc một thông báo mặc định
System.out.println("Circuit is open! Falling back for product: " + productId);
return "Default Product Information";
}
}
4. Kiểm tra
- Chạy tất cả các service (
eureka
,product-service
,order-service
), lời gọi API hoạt động bình thường. - Tắt
product-service
đi. - Gọi API của
order-service
vài lần. Ban đầu bạn sẽ nhận lỗi, nhưng sau khi đủ số lượng lỗi theo cấu hình, Circuit Breaker sẽ “nhảy”. - Các lời gọi tiếp theo sẽ không còn chờ đợi lâu nữa, thay vào đó chúng sẽ ngay lập tức trả về chuỗi
"Default Product Information"
từ phương thứcgetProductFallback
.
Các câu hỏi thường gặp về Spring Cloud
Các lỗi thường gặp với Spring Cloud là gì?
Làm việc với hệ thống phân tán luôn đi kèm với những thách thức gỡ lỗi mới. Dưới đây là một số lỗi kinh điển:
- Xung đột phiên bản (Version Conflict): Đây là lỗi phổ biến nhất. Spring Cloud có một phiên bản phát hành (release train) riêng (ví dụ: 2023.0.1, Hoxton) phải tương thích với một phiên bản Spring Boot cụ thể. Việc sử dụng các phiên bản không tương thích sẽ gây ra các lỗi khó hiểu.
- Giải pháp: Luôn sử dụng Spring Cloud BOM (Bill of Materials) trong file pom.xml để đảm bảo Spring quản lý tất cả các phiên bản của các module con một cách đồng bộ.
- Lỗi
No instances available for [service-name]
: Service A không thể tìm thấy Service B trên Eureka.
- Nguyên nhân: Thường do lỗi cấu hình. Có thể Service B đã không đăng ký thành công, hoặc tên ứng dụng (
spring.application.name
) trong fileapplication.yml
của Service B bị gõ sai, hoặc không khớp với tên logic mà Service A đang dùng để gọi. - Kiểm tra: Luôn kiểm tra Dashboard của Eureka Server để chắc chắn rằng service bạn cần gọi đã đăng ký và đang ở trạng thái
UP
.
- Cấu hình không được nạp từ Config Server: Service client khởi động nhưng không nhận được cấu hình từ Config Server.
- Nguyên nhân: Một lỗi kinh điển là đặt cấu hình kết nối tới Config Server trong file
application.yml
. Cấu hình này phải được đặt trong filebootstrap.yml
, vì nó cần được nạp trước khi context của ứng dụng chính được tạo. - Giải pháp: Chuyển các thuộc tính
spring.cloud.config.uri
sang filebootstrap.yml
.
- Quên các Annotation quan trọng:
Ví dụ như:
- Quên
@EnableEurekaServer
trên Eureka Server. - Quên
@RefreshScope
trên các Bean cần cập nhật cấu hình động.
Để khắc phục lỗi này, lập trình viên nên đi theo một quy trình gồm 3 bước logic: Hiểu chức năng -> Tìm đúng Annotation -> Bổ sung và kiểm tra lại.
Điều này dẫn đến việc bạn gọi /actuator/refresh
thành công nhưng giá trị trong code không thay đổi.
Khi nào nên (và không nên) dùng Spring Cloud?
Đây là một câu hỏi quan trọng về kiến trúc. Việc lựa chọn sai công cụ có thể dẫn đến sự phức tạp không cần thiết.
Bạn nên sử dụng Spring Cloud khi:
- Xây dựng kiến trúc Microservices: Đây là lý do tồn tại chính của Spring Cloud. Nếu bạn đang xây dựng một hệ thống gồm nhiều dịch vụ nhỏ, độc lập cần giao tiếp và phối hợp với nhau, Spring Cloud cung cấp một bộ giải pháp đã được chuẩn hóa.
- Hệ thống có yêu cầu phức tạp về vận hành: Khi bạn cần giải quyết các bài toán như khám phá dịch vụ (service discovery), quản lý cấu hình tập trung, khả năng chịu lỗi (fault tolerance), định tuyến thông minh… Spring Cloud là một lựa chọn tuyệt vời.
- Đội ngũ đã quen thuộc với Spring Boot: Spring Cloud tích hợp một cách tự nhiên và liền mạch với Spring Boot. Nếu team của bạn đã có kinh nghiệm với Spring, việc tiếp cận Spring Cloud sẽ rất nhanh chóng và hiệu quả.
Bạn nên cân nhắc lại hoặc không nên dùng Spring Cloud khi:
- Xây dựng ứng dụng nguyên khối (Monolithic): Nếu ứng dụng của bạn là một khối đơn giản, không có nhu cầu chia tách thành các service, việc thêm Spring Cloud vào chỉ làm tăng sự phức tạp mà không mang lại lợi ích rõ rệt.
- Dự án quá nhỏ hoặc đơn giản: Đối với các ứng dụng CRUD đơn giản hoặc các công cụ dòng lệnh (CLI tools), việc thiết lập cả một hệ sinh thái Spring Cloud (như Eureka Server, Config Server) là không cần thiết và tốn kém tài nguyên.
- Sử dụng nền tảng Cloud-Native đã có sẵn các giải pháp tương tự: Các nền tảng như Kubernetes cung cấp các cơ chế riêng cho service discovery, configuration (qua ConfigMaps/Secrets), và load balancing. Trong môi trường hoàn toàn là Kubernetes, bạn có thể cân nhắc sử dụng các giải pháp “thuần” của nền tảng đó thay vì triển khai thêm các thành phần của Spring Cloud. Tuy nhiên, việc kết hợp cả hai vẫn là một phương án phổ biến.
Làm thế nào để cài đặt Spring Cloud?
Một hiểu lầm phổ biến là xem Spring Cloud như một phần mềm cần “cài đặt”. Thực tế, bạn không “cài đặt” Spring Cloud. Thay vào đó, bạn thêm nó vào dự án Spring Boot của mình thông qua việc quản lý các dependency.
Quy trình chuẩn bao gồm 3 bước:
Bước 1: Khai báo Spring Cloud BOM để quản lý phiên bản
Đây là bước quan trọng nhất để tránh xung đột phiên bản. Trong file pom.xml
, thêm vào trong thẻ <dependencyManagement>
:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version> <type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Bước 2: Thêm các “Starters” cần thiết
Dựa trên nhu cầu của bạn, hãy thêm các dependency tương ứng vào thẻ <dependencies>
. Ví dụ:
Để dùng Config Server:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
Để một service trở thành Eureka Client:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Để dùng API Gateway:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Bước 3: Cấu hình và Kích hoạt
Sau khi đã thêm dependency, bạn chỉ cần cấu hình các thuộc tính cần thiết trong application.yml
(hoặc bootstrap.yml
) và thêm các annotation kích hoạt (@Enable...
) nếu cần, như đã trình bày trong các phần hướng dẫn thực hành.
Để bắt đầu một dự án mới, cách dễ nhất là sử dụng Spring Initializr (start.spring.io) và chọn các module Spring Cloud bạn cần từ danh sách.
Spring Cloud Bus dùng để làm gì?
Trong phần hướng dẫn về Config Server, chúng ta đã thực hiện refresh cấu hình động bằng cách gọi một API POST
đến endpoint /actuator/refresh
. Cách này chỉ hoạt động cho một instance duy nhất. Vậy nếu hệ thống có 10 instance của order-service
đang chạy, làm sao để tất cả chúng cùng cập nhật cấu hình?
Đây chính là lúc Spring Cloud Bus phát huy tác dụng.
Spring Cloud Bus là một project giúp liên kết các node (instance) của một hệ thống phân tán bằng một message broker nhẹ (như RabbitMQ hoặc Kafka). Nó hoạt động như một kênh giao tiếp chung.
Cơ chế hoạt động:
- Tất cả các instance của
order-service
đều kết nối vào một message broker. - Bạn chỉ cần gửi request
POST
đến endpoint/actuator/bus-refresh
của bất kỳ một instance nào. - Instance đó sẽ gửi một tin nhắn “refresh” lên message broker.
- Tất cả các instance khác (bao gồm cả chính nó) đang lắng nghe trên kênh này sẽ nhận được tin nhắn và đồng loạt kích hoạt quá trình làm mới cấu hình của mình.
Tóm lại, Spring Cloud Bus giúp quảng bá các thay đổi về trạng thái (phổ biến nhất là thay đổi cấu hình) đến tất-cả-các-instance của một service một cách tự động, thay vì phải thực hiện thủ công trên từng instance.
Tổng kết
Spring Cloud là bộ công cụ kỹ thuật giúp bạn chế ngự sự phức tạp của kiến trúc microservices. Nó cung cấp các giải pháp đã được chuẩn hóa cho những bài toán kinh điển như service discovery, quản lý cấu hình, định tuyến và khả năng chịu lỗi. Lợi ích lớn nhất mà Spring Cloud mang lại là cho phép bạn tập trung vào việc xây dựng logic nghiệp vụ, thay vì phải tự mày mò giải quyết các vấn đề hạ tầng phức tạp. Hy vọng bài viết này đã cung cấp một nền tảng vững chắc để bạn bắt đầu.