Xây dựng kiến trúc Microservices với Spring Cloud

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 registerWithEurekafetchRegistry设为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!

Thẻ: spring-cloud eureka Microservices Feign ribbon

Đăng vào ngày 24 tháng 7 lúc 15:13