OpenFeign + Spring Cloud LoadBalancer: gọi service khác như gọi method
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 đề
- Hardcode
localhost:8082: payment có 3 instance thì gọi cái nào? - Mỗi API phải viết lại URL, method, body, xử lý lỗi. Nhiều endpoint thì code lặp và dài.
- Timeout, header (trace id, token), mapping lỗi rải rác khắp nơi.
3. Giải pháp
- OpenFeign: declarative HTTP client. Bạn chỉ khai báo interface, Spring sinh proxy thực hiện HTTP call.
- Spring Cloud LoadBalancer: client-side load balancer. Lấy danh sách instance từ Eureka, chọn một instance (mặc định round robin). Thay thế Netflix Ribbon đã ngừng phát triển.
@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í | RestTemplate | RestClient | OpenFeign | HTTP Interface (@HttpExchange) | WebClient |
|---|---|---|---|---|---|
| Kiểu | Imperative | Fluent, imperative | Declarative interface | Declarative interface | Reactive |
| Blocking | Có | Có | Có | Tùy client bên dưới | Không |
| Load balancing | @LoadBalanced | @LoadBalanced builder | Sẵn qua name | Qua client @LoadBalanced | @LoadBalanced builder |
| Thuộc | Spring Framework (maintenance) | Spring Framework 6.1+ | Spring Cloud | Spring 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
- Nên: Spring MVC service gọi nhiều endpoint của service khác, đã dùng Spring Cloud + Eureka.
- Không nên: trong WebFlux / Gateway filter (Feign blocking, dùng WebClient); project mới không cần Spring Cloud thì cân nhắc
@HttpExchange+ RestClient (Spring Cloud OpenFeign đã chuyển sang feature-complete).
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:
- Đặt
connectTimeoutngắn để fail nhanh. - Bật retry của LoadBalancer sang instance khác, chỉ cho request idempotent (GET), không retry tự động
POST /payments:spring: cloud: loadbalancer: retry: enabled: true max-retries-on-next-service-instance: 1 - Bật health check của LoadBalancer hoặc dùng Kubernetes readiness probe.
- Bọc thêm Circuit Breaker.
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
@HttpExchangeare 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
- Feign = interface → proxy → HTTP.
name= service ID trên Eureka. - LoadBalancer = client-side, round robin, có cache → có thể trúng instance chết.
- Feign blocking, không tạo thread → luôn đặt timeout, tránh dùng trong WebFlux.
- Không retry tự động request không idempotent (thanh toá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.