Service Discovery với Eureka
tiếp đi, microservices tìm nhau như thế nào?
1. Bài toán: Order gọi Payment bằng địa chỉ cứng
Trong spring-cloud-demo, khi user tạo đơn hàng:
Client
|
v
Order Service :8081
|
|---> Inventory Service :8083 (check stock)
|
|---> Payment Service :8082 (make payment)
Order đang gọi Payment như bài trước:
@Service
public class OrderService {
private final RestTemplate restTemplate;
public OrderService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String createOrder() {
return restTemplate.getForObject(
"http://localhost:8082/payments/status",
String.class
);
}
}
Code này chạy được. Nhưng vấn đề bắt đầu khi hệ thống scale.
2. Vấn đề: Payment Service có nhiều instance
Giả sử Payment được triển khai thành 3 instance: 8082, 8084, 8085. Lúc này:
- Order Service làm sao biết Payment có 3 instance?
- Nếu instance 8082 chết, Order phải làm gì?
- Thêm instance thứ 4 thì có phải sửa code không?
- Làm sao chia request đều cho các instance?
Hardcode địa chỉ trong code thì không quản lý nổi. Đây là bài toán Service Discovery.
3. Giải pháp: Spring Cloud Eureka
Eureka là một Service Registry: nơi lưu danh sách các instance đang sống. Gồm hai phần:
- Eureka Server: giữ danh sách instance (chính là
discovery-servertrong project). - Eureka Client: nằm trong mỗi service, tự đăng ký và tìm service khác.
Flow:
- Payment khởi động, đăng ký với Eureka:
PAYMENT-SERVICE, localhost, 8082, UP. - Payment gửi heartbeat định kỳ để báo "tôi còn sống".
- Order lấy danh sách instance của PAYMENT-SERVICE từ Eureka (và cache lại).
- Spring Cloud LoadBalancer chọn một instance, Order gọi HTTP trực tiếp tới instance đó.
Điểm cần nhớ: Eureka chỉ giúp tìm service, không chuyển tiếp HTTP request. Chọn instance là việc của LoadBalancer.
4. Code từng bước
spring-cloud-demo/
├── discovery-server/ <-- NEW :8761
├── order-service/ :8081
└── payment-service/ :8082
Bước 1 — Dependency. discovery-server:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
payment-service và order-service (cùng spring-boot-starter-web):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Eureka client starter đã kéo theo Spring Cloud LoadBalancer. Version lấy từ BOM ở bài 4.6.1.
Bước 2 — Config. discovery-server/application.yml:
server:
port: 8761
spring:
application:
name: discovery-server
eureka:
client:
register-with-eureka: false
fetch-registry: false
| Config | Ý nghĩa |
|---|---|
port: 8761 | Port quen thuộc của Eureka |
register-with-eureka: false | Server không tự đăng ký với chính nó |
fetch-registry: false | Server không lấy registry từ Eureka khác |
Hai giá trị false hợp với demo một Eureka đứng một mình. Chạy Eureka cluster thì cấu hình khác.
payment-service/application.yml:
server:
port: 8082
spring:
application:
name: payment-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
order-service tương tự, port: 8081, name: order-service.
Bước 3 — Code Java. Bật Eureka Server:
package com.example.discovery;
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
Payment giữ nguyên controller trả về port. Ở Order, tạo RestTemplate có @LoadBalanced:
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@LoadBalanced là điểm mấu chốt: nó cho phép LoadBalancer dịch tên service thành địa chỉ một instance cụ thể.
@Service
public class OrderService {
private final RestTemplate restTemplate;
public OrderService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String createOrder() {
return restTemplate.getForObject(
"http://payment-service/payments/status",
String.class
);
}
}
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public String createOrder() {
return orderService.createOrder();
}
}
Thấy khác biệt chưa? Trước: http://localhost:8082/.... Bây giờ: http://payment-service/.... Order không cần biết Payment chạy ở IP, port nào.
Bước 4 — Test bằng curl. Chạy discovery-server, mở http://localhost:8761 (ban đầu trống). Chạy Payment 3 instance cùng một JAR:
java -jar payment-service.jar # 8082
java -jar payment-service.jar --server.port=8084
java -jar payment-service.jar --server.port=8085
Application Status
---------------------------------
PAYMENT-SERVICE UP (3) - localhost:payment-service:8082, :8084, :8085
Gọi nhiều lần:
curl -X POST http://localhost:8081/orders
Request 1 → Payment Service running on port 8082
Request 2 → Payment Service running on port 8084
Request 3 → Payment Service running on port 8085
Request 4 → Payment Service running on port 8082
Đây là Round Robin mặc định của Spring Cloud LoadBalancer. Thứ tự thực tế có thể khác tùy cache và trạng thái instance.
5. Cách hoạt động
(1) register + heartbeat 30s
Payment :8082 ─────────┐
Payment :8084 ─────────┼──────> +--------------------+
Payment :8085 ─────────┘ | Eureka Server |
| :8761 |
| PAYMENT-SERVICE: |
| 8082, 8084, 8085 |
+--------------------+
^
(2) fetch registry | (cache, refresh ~30s)
|
Order Service ── RestTemplate @LoadBalanced
| "http://payment-service/payments/status"
| (3) LoadBalancer chọn 8084 (round robin)
v
Payment :8084 <── (4) HTTP gọi TRỰC TIẾP, không đi qua Eureka
Điều gì xảy ra nếu một instance chết? Bạn tắt 8084 đột ngột. Eureka không biết ngay:
- Client gửi heartbeat khoảng 30 giây một lần.
- Lease hết hạn sau khoảng 90 giây không có heartbeat.
- Eureka Server có self-preservation: nếu thấy quá nhiều instance mất heartbeat cùng lúc (nghi lỗi mạng), nó tạm không xóa instance nào.
- Order có registry cache, nên vẫn có thể gọi trúng instance đã chết.
t=0s 8084 crash
t=0-90s Eureka vẫn ghi 8084 UP
Order cache vẫn có 8084 -> ~1/3 request lỗi Connection refused
t=~90s Eureka xóa 8084 (nếu không bị self-preservation chặn)
t=~120s Order refresh cache -> hết gọi 8084
Service Discovery không đảm bảo mọi request thành công. Production vẫn cần timeout, retry có kiểm soát và Circuit Breaker.
6. So sánh: hardcode, Eureka, Kubernetes Service
| Tiêu chí | Hardcode URL | Eureka + LoadBalancer | Kubernetes Service |
|---|---|---|---|
| Thêm instance | Sửa code/config | Tự đăng ký | Tự thêm Pod endpoint |
| Ai chọn instance | Không ai | Client (LoadBalancer trong Order) | Hạ tầng (kube-proxy) |
| Phát hiện instance chết | Không | Heartbeat, chậm vài chục giây | Readiness probe |
| Thêm thành phần vận hành | Không | Eureka Server | Có sẵn trong cluster |
| Hợp với | Demo | VM, bare metal, hybrid | Chạy toàn bộ trên K8s |
7. Khi nào nên / không nên dùng Eureka
Nên: các service chạy trên VM hoặc bare metal, IP thay đổi, cần client-side load balancing và tích hợp sẵn với Spring.
Không nên: toàn bộ hệ thống chạy trên Kubernetes (đã có Service + DNS), hoặc chỉ có 2–3 service cố định sau một load balancer.
application.yml riêng. 20 service × 3 môi trường, muốn đổi payment.timeout thì sửa và deploy lại hết sao?Vậy Eureka có làm load balancing không? Request có đi qua Eureka không?
Không, và đây là chỗ hay bị hỏi bẫy. Eureka giống danh bạ điện thoại: bạn tra số, rồi tự gọi. Danh bạ không nối máy giúp bạn.
Eureka Server : giữ danh bạ (ai đang sống, ở đâu)
LoadBalancer : chọn 1 số trong danh bạ (round robin...)
RestTemplate/Feign: tự bấm gọi trực tiếp tới instance đó
Vì thế khi Eureka Server chết, các service vẫn gọi nhau được một thời gian nhờ registry đã cache. Chỉ là instance mới đăng ký sẽ không được biết tới, và instance chết không bị loại.
Kubernetes rồi có cần Eureka không?
Đúng như bạn đoán, thường là không cần. Kubernetes đã có Service Discovery sẵn qua Service + DNS:
Order Pod
| http://payment-service/payments
v
payment-service (K8s Service, DNS: payment-service.default.svc.cluster.local)
|
+---- Payment Pod 1
+---- Payment Pod 2
+---- Payment Pod 3
Khi đó Feign chỉ cần gọi URL cố định là tên Service, không cần Eureka. Eureka vẫn có chỗ dùng nếu hệ thống hybrid: một phần trên VM, một phần trên K8s. Phần này mình làm kỹ ở bài 4.6.9 (Spring Cloud + Kubernetes).
Chạy 3 instance trên cùng máy thì Eureka có bị nhầm thành 1 không?
Bạn hỏi đúng chỗ. Eureka phân biệt instance bằng instance ID. Mặc định Spring Cloud Netflix tạo ID dạng host:application-name:port, nên 3 port khác nhau là 3 instance khác nhau. Nếu chạy với port ngẫu nhiên (server.port=0) thì nên tự đặt ID duy nhất:
eureka:
instance:
instance-id: ${spring.application.name}:${random.value}
Phỏng vấn hỏi về Service Discovery và Eureka thì trả lời sao?
Q1. Service Discovery là gì? Eureka là gì?
🇻🇳 Tiếng Việt:
Service Discovery là cơ chế để các service tự đăng ký và tìm nhau một cách động, không cần hardcode IP và port. Eureka là service registry của Netflix, tích hợp vào Spring Cloud. Mỗi service khi khởi động đăng ký với Eureka Server, gửi heartbeat định kỳ, và lấy danh sách instance của service khác để gọi.
🇬🇧 English:
Service Discovery is a mechanism that lets services register and find each other dynamically without hardcoding IPs and ports. Eureka is a service registry from Netflix, integrated into Spring Cloud. Each service registers with the Eureka Server on startup, sends periodic heartbeats, and fetches the instance list of other services to call them.
Q2. Eureka có làm load balancing không?
🇻🇳 Tiếng Việt:
Không. Eureka chỉ cung cấp danh sách instance. Spring Cloud LoadBalancer ở phía client chọn một instance, mặc định round robin, rồi client gọi HTTP trực tiếp tới instance đó. Request không đi qua Eureka.
🇬🇧 English:
No. Eureka only provides the list of instances. Spring Cloud LoadBalancer on the client side picks one, round robin by default, and the client calls that instance directly. Requests never pass through Eureka.
Q3. Một instance crash đột ngột thì sao? Eureka Server chết thì sao?
🇻🇳 Tiếng Việt:
Eureka không phát hiện ngay. Heartbeat khoảng 30 giây, lease hết hạn khoảng 90 giây, cộng thêm cache ở client, nên trong khoảng đó request vẫn có thể gọi trúng instance chết. Vì vậy vẫn cần timeout, retry và circuit breaker. Nếu Eureka Server chết, client vẫn dùng registry đã cache để gọi các instance đã biết, nhưng không nhận được thay đổi mới. Production nên chạy Eureka cluster.
🇬🇧 English:
Eureka doesn't detect it immediately. Heartbeats are about every 30 seconds, the lease expires after about 90 seconds, and clients cache the registry, so requests can still hit the dead instance for a while. That's why we still need timeouts, retries, and circuit breakers. If the Eureka Server goes down, clients keep using their cached registry for known instances but won't see new changes. In production we run Eureka as a cluster.
Q4. Dùng Kubernetes thì có cần Eureka không?
🇻🇳 Tiếng Việt:
Thường là không. Kubernetes có Service và DNS cho discovery và load balancing. Em chỉ giữ Eureka khi hệ thống hybrid, có service chạy ngoài cluster.
🇬🇧 English:
Usually not. Kubernetes provides Services and DNS for discovery and load balancing. I would keep Eureka only in a hybrid setup where some services run outside the cluster.
Cách nhớ ngắn gọn
- Eureka = danh bạ. LoadBalancer = chọn số. RestTemplate/Feign = tự gọi.
@LoadBalanced+http://payment-service= gọi bằng tên, không bằng IP.- Heartbeat 30s, lease 90s, có cache: instance chết vẫn có thể bị gọi trúng.
- Trên Kubernetes: Service + DNS thay Eureka.
Tự kiểm tra
Eureka Server và Eureka Client làm gì?
Server giữ danh sách instance. Client nằm trong mỗi service, đăng ký, gửi heartbeat và lấy danh sách service khác.
Vì sao Eureka Server cấu hình register-with-eureka: false?
Để server đứng một mình không tự đăng ký với chính nó và không đi lấy registry từ Eureka khác.
@LoadBalanced làm gì?
Cho RestTemplate dùng Spring Cloud LoadBalancer để dịch tên service (payment-service) thành địa chỉ một instance cụ thể.
Payment 3 instance, 8082 crash. Request tiếp theo chắc chắn được chuyển sang instance khác?
Không. Eureka phát hiện chậm và client còn cache, request vẫn có thể gọi trúng 8082 và lỗi. Cần timeout, retry, circuit breaker.
Self-preservation của Eureka là gì?
Khi quá nhiều instance mất heartbeat cùng lúc, Eureka nghi lỗi mạng và tạm ngừng xóa instance để không xóa nhầm cả loạt.