Kiến trúc vi dịch vụ Spring Cloud: Từ thiết lập môi trường đến Gateway và Circuit Breaker

Tổng quan về hệ sinh thái Spring Cloud

Spring Cloud cung cấp bộ công cụ tích hợp sẵn để xây dựng kiến trúc phân tán, bao gồm các thành phần cốt lõi như đăng ký dịch vụ (Eureka), cân bằng tải phía client (Ribbon), xử lý lỗi và mát cầu (Hystrix), cấu hình phân tán, cũng như cổng dịch vụ (Gateway). Kết hợp với Spring Boot, nhà phát triển có thể nhanh chóng triển khai các hệ thống microservices ổn định và dễ bảo trì.

Phiên bản đề xuất:

  • Spring Boot: 2.2.2.RELEASE
  • Spring Cloud: Hoxton.SR1
  • Java: 8 trở lên
  • Maven: 3.5+

Cấu hình dự án cha với Maven

Để quản lý phụ thuộc và phiên bản tập trung, ta tạo một dự án Maven cha đóng gói dưới dạng pom. Trong dependencyManagement, các phiên bản được khóa để các mô-đun con chỉ cần khai báo groupIdartifactId.

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>vn.techcloud</groupId>
    <artifactId>microservice-platform</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>4.12</junit.version>
        <lombok.version>1.18.12</lombok.version>
        <mysql.version>8.0.23</mysql.version>
        <druid.version>1.2.6</druid.version>
        <mybatis-springboot.version>2.1.4</mybatis-springboot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-springboot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Xây dựng dịch vụ cung cấp (Provider)

Tạo mô-đun payment-provider-8001 chứa logic xử lý giao dịch. Cấu hình kết nối cơ sở dữ liệu thông qua Druid và MyBatis.

<dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
    <dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency>
    <dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency>
    <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency>
    <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>

application.yml:

server:
  port: 8001
spring:
  application:
    name: payment-billing-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/microsvc_db?useSSL=false&serverTimezone=UTC
    username: root
    password: secret
mybatis:
  mapper-locations: classpath:mapping/**/*.xml
  type-aliases-package: vn.techcloud.payment.model

Lớp khởi chạy:

package vn.techcloud.payment;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PaymentBillingApp {
    public static void main(String[] args) {
        SpringApplication.run(PaymentBillingApp.class, args);
    }
}

Controller xử lý request:

package vn.techcloud.payment.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import vn.techcloud.payment.dto.ApiResponse;
import vn.techcloud.payment.model.Transaction;
import vn.techcloud.payment.service.TransactionProcessor;

@RestController
@Slf4j
@RequiredArgsConstructor
public class TransactionEndpoint {
    private final TransactionProcessor txProcessor;

    @PostMapping("/billing/register")
    public ApiResponse register(@RequestBody Transaction tx) {
        log.info("Nhận yêu cầu thanh toán: {}", tx);
        int status = txProcessor.insertTransaction(tx);
        return status > 0 
            ? new ApiResponse(200, "Ghi nhận thành công", status) 
            : new ApiResponse(500, "Lỗi ghi nhận", null);
    }

    @GetMapping("/billing/detail/{txId}")
    public ApiResponse findByCode(@PathVariable Long txId) {
        Transaction found = txProcessor.findByCode(txId);
        return found != null 
            ? new ApiResponse(200, "Tìm thấy", found) 
            : new ApiResponse(404, "Không có giao dịch này", null);
    }
}

Xây dựng dịch vụ tiêu thụ (Consumer) và RestTemplate

Mô-đun order-client-80 sẽ gọi remote đến dịch vụ billing thông qua RestTemplate. Đây là công cụ HTTP client đồng bộ của Spring.

package vn.techcloud.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class WebClientConfig {
    @Bean
    public RestTemplate httpCaller() {
        return new RestTemplate();
    }
}

Controller bên phía order:

package vn.techcloud.order.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import vn.techcloud.order.dto.ApiResponse;
import vn.techcloud.order.model.Transaction;

@RestController
@Slf4j
@RequiredArgsConstructor
public class OrderRequestHandler {
    private static final String BILLING_BASE_URL = "http://localhost:8001";
    private final RestTemplate httpCaller;

    @GetMapping("/client/billing/register")
    public ApiResponse submitOrder(Transaction tx) {
        return httpCaller.postForObject(BILLING_BASE_URL + "/billing/register", tx, ApiResponse.class);
    }

    @GetMapping("/client/billing/detail/{txId}")
    public ApiResponse fetchDetail(@PathVariable Long txId) {
        return httpCaller.getForObject(BILLING_BASE_URL + "/billing/detail/" + txId, ApiResponse.class);
    }
}

Tái cấu trúc: Tạo mô-đun API chung

Để tránh trùng lặp DTO và model, ta tách lớp ApiResponse, Transaction ra mô-đun api-contract. Sau đó thực hiện mvn clean install để publish vào local repository. Các mô-đun provider và consumer chỉ cần thêm dependency vào api-contract và xóa các class trùng lặp.

Đăng ký và Phát hiện dịch vụ với Eureka

Eureka hoạt động theo mô hình Client-Server. Provider và Consumer sẽ đăng ký thông tin mạng của mình lên Eureka Server. Khi cần gọi dịch vụ, client sẽ hỏi Server để lấy danh sách instance sống.

Cấu hình Eureka Server (7001 & 7002):

server:
  port: 7001
eureka:
  instance:
    hostname: eureka-node1.local
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://eureka-node2.local:7002/eureka/

Cấu hình tương tự cho node 7002 nhưng trỏ defaultZone ngược lại node 7001 để tạo cụm (cluster) đồng bộ.

Provider đăng ký lên Eureka:

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka-node1.local:7001/eureka,http://eureka-node2.local:7002/eureka
  instance:
    instance-id: billing-8001
    prefer-ip-address: true

Thêm annotation @EnableEurekaClient hoặc @EnableDiscoveryClient vào lớp main. Cơ chế tự bảo vệ (self-preservation) sẽ kích hoạt khi mất kết nối mạng đột ngột, ngăn Server xóa nhầm các instance còn sống nhưng chưa gửi được heartbeat.

Cân bằng tải phía client với Ribbon

Ribbon tích hợp sẵn trong Eureka client, cho phép phân phối request đến các instance đăng ký. Để kích hoạt gọi theo tên service thay vì cứng IP, ta thêm annotation @LoadBalanced lên bean RestTemplate:

@Bean
@LoadBalanced
public RestTemplate httpCaller() {
    return new RestTemplate();
}

Trong controller, đổi BILLING_BASE_URL thành http://payment-billing-service. Ribbon mặc định dùng thuật toán Round Robin. Có thể tùy chỉnh bằng cách khai báo bean IRule (ví dụ RandomRule) và trỏ qua @RibbonClient.

Gọi dịch vụ khai báo với OpenFeign

OpenFeign giúp ẩn đi các lệnh gọi HTTP thủ công. Ta chỉ cần định nghĩa interface và ánh xạ annotation giống như Controller bên provider:

package vn.techcloud.order.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import vn.techcloud.order.dto.ApiResponse;
import vn.techcloud.order.model.Transaction;

@FeignClient(name = "payment-billing-service")
public interface BillingRemoteClient {
    @PostMapping("/billing/register")
    ApiResponse register(@RequestBody Transaction tx);

    @GetMapping("/billing/detail/{txId}")
    ApiResponse findByCode(@PathVariable Long txId);
}

Kích hoạt bằng @EnableFeignClients. Feign tự động tích hợp Ribbon nên vẫn giữ được tính năng load balancing. Để tránh timeout, cấu hình thời gian chờ trong application.yml:

ribbon:
  ReadTimeout: 5000
  ConnectTimeout: 5000

Hỗ trợ logging chi tiết bằng cách thêm bean Logger.Level và bật log level DEBUG cho package Feign client.

Xử lý lỗi & Mát cầu (Circuit Breaker) với Hystrix

Trong hệ phân tán, lỗi thời gian chờ hoặc sập dịch vụ con dễ gây hiệu ứng tuyết lở. Hystrix giải quyết bằng cơ chế fallback và mạch điện tự động mở/đóng.

Provider định nghĩa fallback:

package vn.techcloud.payment.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

@Service
public class TransactionProcessor {
    @HystrixCommand(fallbackMethod = "handleTimeoutFallback",
            commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
            })
    public String processSlowTransaction(Long id) {
        Thread.sleep(5000); // Mô phỏng delay
        return "Xử lý thành công trên instance: " + id;
    }

    public String handleTimeoutFallback(Long id) {
        return "Hệ thống đang bận, vui lòng thử lại. ID: " + id;
    }
}

Active Hystrix bằng @EnableCircuitBreaker hoặc @EnableHystrix. Khi tỷ lệ lỗi vượt ngưỡng (mặc định 50% trong 10s với 20 request), mạch sẽ mở (OPEN) và tất cả request sau đó sẽ nhảy thẳng vào fallback mà không gọi dịch vụ thật. Sau khoảng thời gian ngủ (sleep window), mạch chuyển sang HALF-OPEN để thử lại một request đơn lẻ.

Có thể tách logic fallback ra class riêng bằng cách implement interface Feign và trỏ qua thuộc tính fallback = YourFallbackClass.class trong @FeignClient để code gọn gàng hơn.

Cổng dịch vụ API Gateway

Spring Cloud Gateway thay thế Zuul, được xây dựng trên Reactive (WebFlux) và Netty, hỗ trợ xử lý kết nối đồng thời cao. Gateway nhận request, khớp điều kiện (Predicate), áp dụng biến đổi (Filter), rồi chuyển tiếp (Route) đến dịch vụ đích.

Cấu hình routing tĩnh:

spring:
  cloud:
    gateway:
      routes:
        - id: billing-route
          uri: http://localhost:8001
          predicates:
            - Path=/billing/**
        - id: billing-lb-route
          uri: lb://payment-billing-service
          predicates:
            - Path=/client/billing/**

Thêm discovery.locator.enabled: true để Gateway tự động tạo route theo tên service đăng ký trên Eureka. Tiền tố lb:// báo hiệu kích hoạt load balancing.

Predicate thông dụng:

  • After/Before/Between: Lọc theo thời gian
  • Cookie, Header, Query: Kiểm tra tham số hoặc metadata
  • Method: Giới hạn HTTP method

Global Filter tùy chỉnh:

package vn.techcloud.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class AuthAndLogFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (token == null || !token.startsWith("valid_")) {
            log.warn("Từ chối request thiếu token hợp lệ");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1; // Ưu tiên cao
    }
}

Gateway hoạt động như một điểm tập trung duy nhất cho tất cả traffic đến hệ thống, giúp triển khai chính sách bảo mật, rate limiting, và logging tập trung mà không cần sửa đổi các microservices con.

Thẻ: spring-cloud Microservices eureka ribbon openfeign

Đăng vào ngày 13 tháng 6 lúc 02:24