Service Discovery với Eureka

5 câu hỏi~20 phútTừ khóa: Eureka Server, Eureka Client, heartbeat, @LoadBalanced, LoadBalancer, self-preservation

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:

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:

Flow:

  1. Payment khởi động, đăng ký với Eureka: PAYMENT-SERVICE, localhost, 8082, UP.
  2. Payment gửi heartbeat định kỳ để báo "tôi còn sống".
  3. Order lấy danh sách instance của PAYMENT-SERVICE từ Eureka (và cache lại).
  4. 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: 8761Port quen thuộc của Eureka
register-with-eureka: falseServer không tự đăng ký với chính nó
fetch-registry: falseServer 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:

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 URLEureka + LoadBalancerKubernetes Service
Thêm instanceSửa code/configTự đăng kýTự thêm Pod endpoint
Ai chọn instanceKhông aiClient (LoadBalancer trong Order)Hạ tầng (kube-proxy)
Phát hiện instance chếtKhôngHeartbeat, chậm vài chục giâyReadiness probe
Thêm thành phần vận hànhKhôngEureka ServerCó sẵn trong cluster
Hợp vớiDemoVM, bare metal, hybridChạ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.

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


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.