OpenFeign + Spring Cloud LoadBalancer: gọi service khác như gọi method

4 câu hỏi~20 phútTừ khóa: @FeignClient, @EnableFeignClients, LoadBalancer, round robin, timeout, ErrorDecoder, @HttpExchange, blocking

Tiếp đi, order-service gọi payment-service thế nào cho gọn?

Ở bài Eureka, các service đã đăng ký và tìm thấy nhau. Giờ ta lo phần gọi: order-service gọi payment-service sao cho gọn, có load balancing, có timeout.

1. Bài toán

Khi user tạo đơn hàng, order-service phải gọi payment-service để thanh toán:

Client → Gateway → Order Service ──POST /payments──> Payment Service (3 instance)

Cách quen thuộc là RestTemplate/RestClient:

@Service
public class OrderService {

    private final RestClient restClient;

    public OrderService(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("http://localhost:8082").build();
    }

    public PaymentResponse pay(PaymentRequest request) {
        return restClient.post()
            .uri("/payments")
            .body(request)
            .retrieve()
            .body(PaymentResponse.class);
    }
}

2. Vấn đề

3. Giải pháp

@FeignClient(name = "payment-service")   ← tên service trên Eureka
         |
   LoadBalancer chọn instance
         |
   HTTP call (blocking)

4. Code từng bước

Bước 1: Dependency (order-service):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Starter OpenFeign đã kéo theo spring-cloud-starter-loadbalancer.

Bước 2: Config (timeout luôn phải đặt):

spring:
  application:
    name: order-service
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 2000
            readTimeout: 5000
          payment-service:
            readTimeout: 3000
            loggerLevel: basic

Bước 3: Code Java

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
@FeignClient(name = "payment-service")
public interface PaymentClient {

    @PostMapping("/payments")
    PaymentResponse createPayment(@RequestBody PaymentRequest request);

    @GetMapping("/payments/{id}")
    PaymentResponse getPayment(@PathVariable("id") Long id);
}
@Service
public class OrderService {

    private final PaymentClient paymentClient;

    public OrderService(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    public PaymentResponse pay(PaymentRequest request) {
        return paymentClient.createPayment(request);   // như gọi method local
    }
}

Map lỗi HTTP sang exception nghiệp vụ bằng ErrorDecoder:

public class PaymentErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        return switch (response.status()) {
            case 400 -> new InvalidPaymentException("Payment request invalid");
            case 404 -> new PaymentNotFoundException("Payment not found");
            default  -> defaultDecoder.decode(methodKey, response); // 5xx → FeignException
        };
    }
}

@Configuration
class PaymentFeignConfig {
    @Bean
    ErrorDecoder paymentErrorDecoder() { return new PaymentErrorDecoder(); }
}
// @FeignClient(name = "payment-service", configuration = PaymentFeignConfig.class)

Lưu ý: không đánh @Configuration class này trong vùng component scan, nếu không nó áp cho mọi Feign client.

Bước 4: Test bằng curl. Chạy 2 instance payment-service (port 8082, 8092), mỗi instance trả về port của nó:

curl -X POST http://localhost:8080/orders/1/pay
# paid by payment-service:8082
curl -X POST http://localhost:8080/orders/1/pay
# paid by payment-service:8092   ← round robin

5. Cách hoạt động

orderService.pay()
   |
paymentClient.createPayment(req)      (JDK dynamic proxy do Feign sinh ra)
   |
1. Contract đọc @PostMapping → POST /payments
2. Encoder: req → JSON
3. URL = http://payment-service/payments
4. LoadBalancer: hỏi DiscoveryClient (cache từ Eureka)
      [10.0.0.6:8082, 10.0.0.7:8092] → chọn 1 (RoundRobin)
5. HTTP client gửi request — thread hiện tại CHỜ (blocking)
6. 2xx → Decoder: JSON → PaymentResponse
   4xx/5xx → ErrorDecoder → exception

Feign không tạo thread mới: nó chạy trên chính thread gọi (thường là Tomcat worker). Payment chậm 5 giây thì thread đó đứng 5 giây.

6. So sánh

Tiêu chíRestTemplateRestClientOpenFeignHTTP Interface (@HttpExchange)WebClient
KiểuImperativeFluent, imperativeDeclarative interfaceDeclarative interfaceReactive
BlockingCóCóCóTùy client bên dướiKhông
Load balancing@LoadBalanced@LoadBalanced builderSẵn qua nameQua client @LoadBalanced@LoadBalanced builder
ThuộcSpring Framework (maintenance)Spring Framework 6.1+Spring CloudSpring Framework 6+Spring WebFlux

HTTP Interface là "Feign của chính Spring Framework":

@HttpExchange("/payments")
public interface PaymentApi {
    @PostExchange
    PaymentResponse create(@RequestBody PaymentRequest request);
}

@Bean
PaymentApi paymentApi(@LoadBalanced RestClient.Builder builder) {
    RestClient client = builder.baseUrl("http://payment-service").build();
    return HttpServiceProxyFactory
        .builderFor(RestClientAdapter.create(client))
        .build()
        .createClient(PaymentApi.class);
}

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

OpenFeign có tạo thread mới không? Nó blocking thì mình bật virtual threads là xong nhỉ?

Bạn hiểu gần đúng, chỉ cần chỉnh một điểm. Feign không tạo thread mới, nó chạy trên thread gọi và block thread đó. Virtual threads (spring.threads.virtual.enabled=true, Java 21) làm cho việc block rẻ: thread ảo chờ I/O thì nhả carrier thread, nên Tomcat chịu được nhiều request chờ hơn.

Platform threads: 200 thread, 200 request chờ Payment → hết thread
Virtual threads : 10.000 request chờ → vẫn ổn về thread
                  nhưng Payment vẫn chậm, connection pool vẫn có giới hạn

Virtual threads không sửa được: Payment chậm, timeout thiếu, lỗi lan truyền. Vẫn cần timeout và Circuit Breaker (bài sau). Ngoài ra, chỉ có lợi khi HTTP client bên dưới không pin thread.

Payment có 3 instance, instance 8082 vừa crash. Request tiếp theo LoadBalancer tự né luôn đúng không?

Chưa chắc. Eureka không phải HTTP proxy, và phát hiện instance chết không tức thì:

t=0s   8082 crash
       Eureka vẫn giữ 8082 tới khi hết lease (~90s mặc định)
       Order Service có cache registry (~30s) + cache LoadBalancer (~35s)
       → round robin vẫn có thể chọn 8082 → Connection refused
t=~2m  8082 bị xóa khỏi mọi cache

Cách giảm thiệt hại:

Phỏng vấn hỏi OpenFeign và LoadBalancer thì trả lời sao?

Q1. OpenFeign là gì, hoạt động thế nào?

🇻🇳 Tiếng Việt:

OpenFeign là declarative HTTP client. Mình khai báo interface với @FeignClient(name = "payment-service") và các annotation Spring MVC, Spring sinh proxy lúc runtime. Khi gọi method, proxy dựng request, encode body, nhờ Spring Cloud LoadBalancer chọn instance từ Eureka, gửi HTTP và decode response hoặc ném exception qua ErrorDecoder.

🇬🇧 English:

OpenFeign is a declarative HTTP client. We declare an interface with @FeignClient(name = "payment-service") and Spring MVC annotations, and Spring creates a proxy at runtime. When a method is called, the proxy builds the request, encodes the body, asks Spring Cloud LoadBalancer to pick an instance from the registry, sends the HTTP call, then decodes the response or throws an exception through the ErrorDecoder.

Q2. Client-side và server-side load balancing khác nhau thế nào?

🇻🇳 Tiếng Việt:

Server-side: client gọi một địa chỉ cố định (Nginx, cloud LB), LB quyết định instance. Client-side: chính caller lấy danh sách instance từ registry và tự chọn, như Spring Cloud LoadBalancer. Client-side bớt một hop, nhưng mỗi caller phải có logic và cache danh sách instance, nên có thể chọn trúng instance vừa chết.

🇬🇧 English:

With server-side load balancing, the client calls a fixed address such as Nginx or a cloud load balancer, and the balancer picks the instance. With client-side load balancing, the caller fetches the instance list from the registry and chooses itself, like Spring Cloud LoadBalancer. It removes one network hop, but every caller caches the list, so it may still pick an instance that just died.

Q3. Feign có blocking không? Dùng trong WebFlux được không?

🇻🇳 Tiếng Việt:

Có, Feign blocking và chạy trên thread gọi, không tạo thread mới. Vì vậy phải luôn đặt connect/read timeout. Không nên dùng trong WebFlux hay Gateway filter vì sẽ block event loop; ở đó dùng WebClient. Với Spring MVC trên Java 21 có thể bật virtual threads để việc chờ rẻ hơn, nhưng vẫn cần timeout và circuit breaker.

🇬🇧 English:

Yes, Feign is blocking and runs on the caller thread; it does not create a new thread. So we must always set connect and read timeouts. It should not be used in WebFlux or gateway filters because it would block the event loop; WebClient fits there. On Spring MVC with Java 21, virtual threads make waiting cheaper, but we still need timeouts and a circuit breaker.

Q4. OpenFeign khác RestClient / HTTP Interface?

🇻🇳 Tiếng Việt:

RestClient là fluent client, mình tự viết từng request. HTTP Interface (@HttpExchange) là kiểu declarative giống Feign nhưng có sẵn trong Spring Framework 6, chạy trên RestClient hoặc WebClient. Feign thuộc Spring Cloud, tích hợp sẵn LoadBalancer, hiện ở trạng thái feature-complete, nên project mới có thể chọn HTTP Interface.

🇬🇧 English:

RestClient is a fluent client where we write each request ourselves. HTTP Interfaces with @HttpExchange are declarative like Feign but built into Spring Framework 6, backed by RestClient or WebClient. Feign belongs to Spring Cloud and integrates LoadBalancer out of the box; it is now feature-complete, so new projects may prefer HTTP Interfaces.

Cách nhớ ngắn gọn


Tự kiểm tra

Thiếu @EnableFeignClients thì sao?

Spring không quét interface @FeignClient, không có bean PaymentClient, app lỗi khi inject.

Spring Cloud LoadBalancer mặc định chọn instance thế nào?

Round robin trên danh sách instance lấy từ DiscoveryClient (có cache).

ErrorDecoder dùng để làm gì?

Chuyển response 4xx/5xx thành exception nghiệp vụ phù hợp thay vì FeignException chung chung.

Vì sao không bật retry tự động cho POST /payments?

Request có thể đã thanh toán thành công nhưng mất response; retry sẽ trừ tiền hai lần nếu không có idempotency key.

Feign trong Gateway filter WebFlux có vấn đề gì?

Feign blocking, sẽ chặn event loop thread và làm nghẽn Gateway. Dùng WebClient.