Xây dựng kiến trúc Microservices với Spring Cloud
Bài viết này sẽ hướng dẫn các bạn cách xây dựng một hệ thống Microservices cơ bản sử dụng Spring Cloud. Hệ thống bao gồm các thành phần: Eureka làm service registry, Ribbon và Feign để thực hiện gọi RPC giữa các service.
1. Tạo Registry Service với Eureka
Đầu tiên, chúng ta cần tạo một project Maven có hỗ trợ Web, sau đó tạo một Module mới dạng SpringBoot để làm Eureka Server.
1.1 Cấu hình file pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Lưu ý: Phiên bản cũ spring-cloud-starter-eureka-server đã bị deprecated, cần sử dụng spring-cloud-starter-netflix-eureka-server.
1.2 Cấu hình file application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
Cấu hình registerWithEureka và fetchRegistry设为false vì đây là Eureka Server, không phải client.
Tại sao nên dùng YML thay vì XML hoặc Properties? File YML (YAML) sử dụng cấu trúc dữ liệu theo cấp bậc với thụt đầu dòng bằng khoảng trắng, giúp:
- Tập trung vào dữ liệu thay vì cấu trúc thẻ
- Dễ đọc và bảo trì hơn
- Tiết kiệm tài nguyên hơn
1.3 Tạo class khởi chạy
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Annotation @EnableEurekaServer đánh dấu đây là Eureka Server.
1.4 Chạy ứng dụng
Truy cập địa chỉ http://localhost:8761/ để xem giao diện Eureka Dashboard.
2. Tạo Service Provider
Service Provider là service cung cấp dữ liệu, khi đăng ký vào Eureka Server sẽ gửi các metadata như host, port, URL...
2.1 Cấu hình Maven dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
</parent>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 Cấu hình application.yml
server:
port: 8762
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: user-service
2.3 Class khởi chạy
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
2.4 Tạo Controller
@RestController
public class UserController {
@Value("${server.port}")
String port;
@RequestMapping(value = "/user", method = RequestMethod.GET)
public String getUserInfo(@RequestParam String name) {
return "Hello " + name + ", I am from port: " + port;
}
}
2.5 Kiểm tra
Sau khi chạy service, truy cập http://localhost:8761 sẽ thấy service USER-SERVICE đã được đăng ký với port 8762. Truy cập http://localhost:8762/user?name=developer sẽ nhận được kết quả:
Hello developer, I am from port: 8762
3. Tạo Service Consumer với Feign
Feign là một HTTP client declarative, giúp việc gọi HTTP trở nên đơn giản hơn. Feign tích hợp sẵn với Ribbon để thực hiện load balancing.
3.1 Chuẩn bị
Khởi động Eureka Server ở port 8761 và chạy user-service ở 2 instance với port 8762 và 8773.
3.2 Cấu hình pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
</parent>
<groupId>com.example</groupId>
<artifactId>client-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.3 Cấu hình application.yml
server:
port: 8765
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: client-service
3.4 Tạo Feign Client Interface
@FeignClient(value = "user-service")
public interface UserServiceClient {
@RequestMapping(value = "/user", method = RequestMethod.GET)
String getUserInfo(@RequestParam(value = "name") String name);
}
3.5 Tạo Controller
@RestController
public class ClientController {
@Autowired
UserServiceClient userServiceClient;
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String sayHello(@RequestParam String name) {
return userServiceClient.getUserInfo(name);
}
}
3.6 Class khởi chạy
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ClientServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ClientServiceApplication.class, args);
}
}
3.7 Kết quả
Truy cập http://localhost:8765/hello?name=developer nhiều lần, kết quả sẽ luân phiên giữa 2 port:
Hello developer, I am from port: 8762
Hello developer, I am from port: 8773
4. Cấu hình Load Balancing với Ribbon
4.1 Tạo project service-ribbon
4.2 Cấu hình pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
</parent>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.3 Cấu hình application.yml
server:
port: 8764
spring:
application:
name: order-service
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
4.4 Class khởi chạy
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
4.5 Tạo Service và Controller
@RestController
public class OrderController {
@Autowired
OrderService orderService;
@RequestMapping(value = "/order", method = RequestMethod.GET)
public String getOrder(@RequestParam String name) {
return orderService.getOrderInfo(name);
}
}
@Service
public class OrderService {
@Autowired
RestTemplate restTemplate;
public String getOrderInfo(String name) {
return restTemplate.getForObject("http://user-service/" + "user?name=" + name, String.class);
}
}
4.6 Kiểm tra
Truy cập http://localhost:8764/order?name=developer
Lưu ý quan trọng: Thư viện spring-cloud-starter-netflix-eureka-client phiên bản 3.0 đã tích hợp sẵn Ribbon. Nếu thêm riêng spring-cloud-starter-netflix-ribbon sẽ gây xung đột và lỗi No instances available for user-service. Cần loại bỏ dependency ribbon riêng biệt.
5. Cấu hình Circuit Breaker với Hystrix
Trong kiến trúc Microservices, khi một service gọi service khác, nếu service bị lỗi và có nhiều request đồng thời, sẽ dẫn đến tình trạng quá tải và gây lỗi cascade cho toàn hệ thống (hiện tượng "Snowball Effect"). Hystrix giúp ngăn chặn điều này bằng cách mở circuit breaker khi số lỗi vượt ngưỡng (mặc định: 5 giây với 20 lần gọi thất bại).
5.1 Thêm Hystrix dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
5.2 Cập nhật Service class
@Service
public class OrderService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "orderError")
public String getOrderInfo(String name) {
return restTemplate.getForObject("http://user-service/" + "user?name=" + name, String.class);
}
public String orderError(String name) {
return "Sorry " + name + ", service is unavailable!";
}
}
5.3 Bật Hystrix trong class khởi chạy
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
5.4 Kiểm tra
Dừng user-service và truy cập http://localhost:8764/order?name=developer, kết quả sẽ trả về:
Sorry developer, service is unavailable!