Backend

99 Ngày Spring — Ngày 73: Service discovery với Eureka

SSite Admin
8 tháng 10, 2026 6 phút đọc 6 lượt xem
99 Ngày Spring — Ngày 73: Service discovery với Eureka

Ngày 72 cấu hình đã về một chỗ. Nhưng trong cấu hình đó, payment-url trỏ vào đâu? Container khởi động lại đổi IP, scale từ hai lên năm instance, một instance chết giữa chừng: địa chỉ ghi cứng là thứ hỏng đầu tiên. Service discovery cho mỗi service tự đăng ký vào một danh bạ khi khởi động, và cho phép service khác tra tên để lấy địa chỉ còn sống. Bài này dùng Eureka của Spring Cloud Netflix trên Spring Boot 3.5, Spring Cloud 2025.0 và Java 21.

Service discovery với Eureka: đăng ký, heartbeat, tra danh bạ và client-side load balancing

1. Danh bạ hoạt động thế nào

  • Đăng ký: lúc khởi động, mỗi instance gửi tên ứng dụng, host, port và URL health lên Eureka server.

  • Heartbeat: mỗi 30 giây (mặc định) instance gia hạn lease. Không gia hạn quá 90 giây thì server evict instance khỏi danh bạ. Instance chết đột ngột không kịp hủy đăng ký, nên khoảng chờ này là bắt buộc.

  • Tra danh bạ: client tải toàn bộ danh bạ về và cache cục bộ, làm mới mỗi 30 giây. Nhờ cache, Eureka server tạm chết không làm hệ thống dừng: client vẫn biết địa chỉ cũ.

  • Client-side load balancing: chọn instance là việc của người gọi (Spring Cloud LoadBalancer, round-robin mặc định), không cần bộ cân bằng tải đứng giữa.

  • Self-preservation: nếu số heartbeat rơi dưới ngưỡng (nghi mạng có vấn đề chứ không phải instance chết hàng loạt), server ngừng evict để không xóa nhầm cả danh bạ. Trên máy dev một instance, chế độ này khiến bạn thấy instance “ma” rất lâu.

2. Dựng Eureka server

Một dự án Spring Boot riêng, thêm BOM Spring Cloud như Ngày 72 và dependency spring-cloud-starter-netflix-eureka-server. Dashboard ở http://localhost:8761 liệt kê mọi instance đã đăng ký.

package vn.motdev.registry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer   // dependency: spring-cloud-starter-netflix-eureka-server
public class RegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(RegistryApplication.class, args);
    }
}
# registry/src/main/resources/application.yml
server:
  port: 8761
spring:
  application:
    name: registry
eureka:
  client:
    register-with-eureka: false   # server không tự đăng ký chính nó
    fetch-registry: false         # và không cần tải danh bạ
  server:
    enable-self-preservation: true   # production: giữ · dev một máy: có thể tắt để thấy eviction ngay
  • Production cần ít nhất hai server peer: mỗi server đăng ký với server kia (register-with-eureka: true và defaultZone trỏ chéo). Danh bạ được sao chép giữa các peer; client khai báo tất cả URL, cách nhau bằng dấu phẩy.

  • Bảo vệ danh bạ bằng Basic Auth (spring-boot-starter-security) và đưa thông tin đăng nhập vào URL defaultZone của client, lấy từ Config Server. Danh bạ lộ ra ngoài là bản đồ nội bộ hệ thống của bạn.

3. Client: đăng ký và gọi bằng tên

# payment-service/src/main/resources/application.yml
server:
  port: 0                          # cổng ngẫu nhiên: chạy nhiều instance trên một máy
spring:
  application:
    name: payment-service          # = tên trong danh bạ, luôn viết thường
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    healthcheck:
      enabled: true                # báo trạng thái theo /actuator/health, không chỉ "còn sống"
  instance:
    prefer-ip-address: true        # trong Docker, hostname của container không resolve được từ nơi khác
    instance-id: ${spring.application.name}:${random.uuid}
    lease-renewal-interval-in-seconds: 30      # mặc định; dev có thể hạ xuống 5
    lease-expiration-duration-in-seconds: 90   # mặc định; dev có thể hạ xuống 15
package vn.motdev.order;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
class HttpClients {
    @Bean
    @LoadBalanced   // Spring Cloud LoadBalancer sẽ thay tên service bằng host:port thật
    RestClient.Builder loadBalancedBuilder() {
        return RestClient.builder();
    }
}

@org.springframework.stereotype.Service
class PaymentGateway {
    private final RestClient http;
    PaymentGateway(RestClient.Builder builder) { this.http = builder.build(); }

    String charge(String orderId) {
        // "payment-service" là TÊN trong danh bạ, không phải hostname.
        return http.post()
                .uri("http://payment-service/api/payments/{id}", orderId)
                .retrieve()
                .body(String.class);
    }
}
  • Chỉ cần spring-cloud-starter-netflix-eureka-client trên classpath và defaultZone: Boot tự đăng ký ứng dụng khi khởi động và hủy đăng ký khi tắt êm. spring-cloud-starter-loadbalancer đi kèm để thay tên bằng địa chỉ.

  • @LoadBalanced gắn interceptor vào RestClient.Builder: URL http://payment-service/... được tra danh bạ, chọn một instance và viết lại thành http://10.0.3.7:8083/.... Bean RestClient.Builder thường không được cân bằng tải; khai báo bean riêng như trên và tiêm builder vào nơi cần.

  • healthcheck.enabled: true gửi trạng thái từ /actuator/health thay vì chỉ “tiến trình còn chạy”. Instance mất kết nối DB sẽ tự báo DOWN và bị bỏ qua khi chọn.

  • server.port: 0 với instance-id ngẫu nhiên cho phép chạy ba instance payment-service trên cùng laptop để thấy round-robin thật.

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;

@org.springframework.web.bind.annotation.RestController
class WhoIsThere {
    private final DiscoveryClient discovery;   // interface chung: Eureka, Consul, Kubernetes đều cài đặt nó
    WhoIsThere(DiscoveryClient discovery) { this.discovery = discovery; }

    @org.springframework.web.bind.annotation.GetMapping("/instances/{name}")
    java.util.List<String> instances(@org.springframework.web.bind.annotation.PathVariable String name) {
        return discovery.getInstances(name).stream()
                .map(ServiceInstance::getUri)
                .map(Object::toString)
                .toList();
    }
}

DiscoveryClient là interface trừu tượng của Spring Cloud Commons; đổi Eureka sang Consul hay Kubernetes chỉ đổi dependency, mã nguồn giữ nguyên. Đó là lý do nên gọi qua @LoadBalanced và DiscoveryClient, không gọi API Eureka trực tiếp.

4. Dev một máy và production khác nhau ở thời gian

  • Với mặc định 30/90 giây và cache client 30 giây, một instance vừa chết có thể còn được chọn tới hai phút. Đó không phải lỗi mà là đánh đổi cho tải thấp trên registry với hàng nghìn instance. Retry sang instance khác (Ngày 75) và circuit breaker (Ngày 76) là phần bù bắt buộc.

  • Trên máy dev, hạ lease-renewal-interval-in-seconds xuống 5, lease-expiration-duration-in-seconds xuống 15, registry-fetch-interval-seconds xuống 5 và tắt self-preservation để thấy đăng ký/evict trong vài giây. Đừng mang các số này lên production.

  • Trong Docker Compose, hostname của container không resolve được từ máy host: prefer-ip-address: true hoặc đặt eureka.instance.hostname tường minh. Đây là nguyên nhân số một của “đăng ký thành công nhưng gọi thì Connection refused”.

  • Trên Kubernetes, nền tảng đã có Service và DNS: http://payment-service tự đến đúng pod. Khi đó Eureka là một thành phần thừa để vận hành; spring-cloud-kubernetes hoặc gọi thẳng tên Service là đủ. Eureka hợp với VM, bare metal hoặc môi trường hỗn hợp.

5. Bài tập thực hành

  • Chạy registry và ba instance payment-service với server.port: 0; gọi /instances/payment-service và xác nhận ba URI khác nhau.

  • Gọi charge mười lần, log server.port ở phía payment và quan sát round-robin. Tắt đột ngột một instance (kill -9) rồi đo bao lâu nó biến mất khỏi danh bạ với cấu hình mặc định và với cấu hình dev.

  • Bật healthcheck.enabled, cho /actuator/health của một instance trả DOWN (ví dụ tắt DB của nó) và xác nhận instance đó không còn được chọn dù vẫn heartbeat.

  • Dựng hai Eureka server peer, tắt một server và kiểm tra client vẫn gọi được nhờ cache và server còn lại.

Tóm lại: service tự đăng ký và heartbeat, client cache danh bạ và tự chọn instance, các mốc 30/90 giây quyết định độ trễ phát hiện lỗi, @LoadBalanced RestClient gọi bằng tên còn DiscoveryClient giữ mã độc lập với Eureka. Có danh bạ rồi, thế giới bên ngoài không nên phải biết đến nó: Ngày 74 dựng API Gateway làm cửa vào duy nhất, định tuyến theo tên service và xác thực tại cửa.

Tài liệu đối chiếu

Spring Cloud Netflix Eureka: https://docs.spring.io/spring-cloud-netflix/reference/ ; Spring Cloud LoadBalancer: https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/loadbalancer.html ; Spring Cloud Kubernetes discovery: https://docs.spring.io/spring-cloud-kubernetes/reference/

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 73: ClassLoader

Ba giai đoạn nạp, link, init; ba loader và mô hình ủy quyền cha trước; custom loader và định danh lớp theo loader; phân biệt ClassNotFoundException với NoClassDefFoundError.

8 thg 10, 20269 phút8
99 Ngày Spring — Ngày 72: Spring Cloud Config

Cấu hình tập trung trong Git, Config Server và spring.config.import, refresh khi đang chạy với @RefreshScope và Spring Cloud Bus, mã hóa secret bằng {cipher}.

7 thg 10, 20267 phút8
99 Ngày Java — Ngày 72: Garbage Collection

Reachability từ GC root, minor và full GC, G1 mặc định và ZGC thế hệ trong Java 21, cách đọc log GC bằng -Xlog và những thói quen nên bỏ.

7 thg 10, 20269 phút4