Chào mừng trở lại với series “Java Spring Roadmap”! Sau khi đã cùng nhau khám phá những viên gạch nền tảng của Spring Framework như lý do lựa chọn Spring, các thuật ngữ cốt lõi, Dependency Injection (DI), IoC Container, và đặc biệt là sự tiện lợi của Spring Boot Starters và Autoconfiguration. Gần đây, chúng ta đã bước chân vào thế giới của Microservices với bài giới thiệu Spring Cloud là gì và cách xây dựng API Gateway với Spring Cloud Gateway. Hôm nay, chúng ta sẽ đào sâu vào một thành phần cốt lõi khác không thể thiếu trong kiến trúc microservices: **Service Discovery**.
Tưởng tượng hệ thống của bạn giờ không còn là một khối monolith khổng lồ nữa, mà là tập hợp của hàng chục, thậm chí hàng trăm dịch vụ nhỏ, độc lập, giao tiếp với nhau. Làm thế nào để một dịch vụ A biết được địa chỉ (IP, port) của dịch vụ B khi dịch vụ B có thể được deploy trên nhiều máy chủ khác nhau, có thể scale up/down liên tục, và địa chỉ IP có thể thay đổi? Đây chính là vấn đề mà Service Discovery giải quyết. Và trong bài viết này, chúng ta sẽ tập trung vào một trong những giải pháp phổ biến nhất, đặc biệt trong “stack” của Netflix OSS: **Eureka**.
Hãy cùng tìm hiểu Service Discovery là gì, tại sao nó quan trọng, và cách chúng ta có thể sử dụng Netflix Eureka (qua Spring Cloud) để xây dựng hệ thống microservices của riêng mình nhé!
Mục lục
Vì Sao Chúng Ta Cần Service Discovery?
Trong kỷ nguyên của kiến trúc Monolith, mọi thứ khá đơn giản. Nếu cần gọi một module khác, bạn chỉ cần gọi phương thức hoặc hàm trong cùng một process. Khi chuyển sang Microservices, các dịch vụ là các tiến trình độc lập, thường chạy trên các máy chủ hoặc container khác nhau. Việc giao tiếp giữa chúng trở thành giao tiếp mạng (network communication), thường qua HTTP/REST hoặc gRPC.
Vấn đề nảy sinh khi:
- **Địa chỉ dịch vụ động:** Các dịch vụ có thể được triển khai trên môi trường đám mây (cloud) hoặc container orchestration (như Kubernetes), nơi địa chỉ IP và cổng của instance dịch vụ không cố định mà thay đổi liên tục.
- **Scale tự động:** Hệ thống cần scale up (thêm instance) khi tải tăng và scale down (bớt instance) khi tải giảm. Khi scale up, các instance mới xuất hiện với địa chỉ mới.
- **Tính sẵn sàng (Availability):** Nếu một instance dịch vụ bị lỗi hoặc ngừng hoạt động, client cần biết để không gửi request đến đó.
- **Load Balancing:** Khi có nhiều instance của cùng một dịch vụ đang chạy, client hoặc gateway cần biết tất cả các địa chỉ để phân phối tải (load balance) giữa chúng, đảm bảo hiệu suất và tính sẵn sàng.
Nếu không có Service Discovery, bạn sẽ phải làm gì?
- Cấu hình cứng địa chỉ IP/Port trong code hoặc file cấu hình: Cơn ác mộng! Mỗi khi dịch vụ thay đổi địa chỉ hoặc scale, bạn phải sửa đổi cấu hình ở hàng loạt dịch vụ khác và deploy lại.
- Sử dụng DNS tĩnh: Tốt hơn cấu hình cứng, nhưng vẫn không linh hoạt với việc scale up/down nhanh chóng hoặc xử lý các instance bị lỗi. DNS cache cũng có thể gây ra vấn đề.
Đây chính là lúc Service Discovery trở nên quan trọng. Nó cung cấp một cơ chế để các dịch vụ đăng ký (register) địa chỉ của mình khi khởi động và cho phép các dịch vụ khác tìm kiếm (discover) địa chỉ đó theo tên logic.
Service Discovery Hoạt Động Thế Nào?
Về cơ bản, Service Discovery là quá trình tự động phát hiện các instance dịch vụ trong mạng. Có hai mô hình chính:
- **Client-Side Discovery:**
- Các instance dịch vụ đăng ký với một **Service Registry** tập trung khi khởi động.
- Client (dịch vụ gọi) truy vấn Service Registry để lấy danh sách các instance khả dụng của dịch vụ mà nó muốn gọi.
- Client tự chọn một instance từ danh sách (thường dùng thuật toán load balancing đơn giản như Round Robin hoặc Random).
- Ví dụ: Netflix Eureka (phần Service Registry) kết hợp với Netflix Ribbon (phần Client-Side Load Balancer – hiện đã chuyển sang Spring Cloud LoadBalancer).
- **Server-Side Discovery:**
- Các instance dịch vụ đăng ký với Service Registry tập trung.
- Client gửi request đến một thành phần trung gian (thường là Load Balancer hoặc API Gateway).
- Thành phần trung gian này truy vấn Service Registry, chọn một instance khả dụng và chuyển tiếp request đến đó.
- Client không cần biết về Service Registry hay danh sách các instance.
- Ví dụ: AWS ELB (Elastic Load Balancer), Kubernetes Services.
Cả hai mô hình đều có ưu nhược điểm riêng. Trong hệ sinh thái Spring Cloud, mô hình Client-Side Discovery (với Eureka và Spring Cloud LoadBalancer) rất phổ biến, và đó là lý do chúng ta tập trung vào Eureka hôm nay.
Giới Thiệu Về Netflix Eureka
Eureka là một dịch vụ Service Discovery dựa trên REST được phát triển bởi Netflix trong quá trình xây dựng hệ thống microservices khổng lồ của họ. Nó là một phần quan trọng của bộ thư viện Netflix OSS và đã được tích hợp rất chặt chẽ với Spring Cloud.
Eureka hoạt động dựa trên hai thành phần chính:
- **Eureka Server:** Đây là Service Registry trung tâm. Nó nhận đăng ký từ các dịch vụ client và cung cấp API để các dịch vụ client khác truy vấn danh sách các instance dịch vụ đang hoạt động. Eureka Server được thiết kế để có tính sẵn sàng cao và có thể chạy nhiều instance để đồng bộ hóa dữ liệu đăng ký giữa chúng.
- **Eureka Client:** Đây là thành phần được tích hợp vào các microservices. Khi một microservice khởi động, Eureka Client sẽ đăng ký instance của dịch vụ đó với Eureka Server. Nó cũng có thể lấy danh sách các dịch vụ khác từ Eureka Server và lưu trữ cache cục bộ. Eureka Client gửi “heartbeat” định kỳ đến Server để báo hiệu rằng instance vẫn đang hoạt động.
Điểm đặc biệt của Eureka là nó ưu tiên **tính sẵn sàng (Availability)** hơn **tính nhất quán (Consistency)** theo mô hình CAP Theorem. Điều này có nghĩa là trong trường hợp mạng bị phân mảnh (network partition), Eureka Server vẫn tiếp tục phục vụ các request đọc (tìm kiếm dịch vụ) dựa trên dữ liệu cache gần nhất, ngay cả khi các Server không thể đồng bộ hóa hoàn toàn với nhau. Điều này giúp hệ thống microservices của bạn vẫn có thể tiếp tục hoạt động (tìm kiếm dịch vụ) ngay cả khi Service Registry gặp sự cố nhỏ về mạng, dù dữ liệu có thể hơi cũ. Các Server cuối cùng sẽ đồng bộ lại khi mạng ổn định.
Xây Dựng Eureka Server Với Spring Cloud
Bây giờ, hãy bắt tay vào xây dựng Eureka Server đầu tiên của chúng ta bằng Spring Boot và Spring Cloud.
Bạn cần tạo một dự án Spring Boot mới (sử dụng Spring Initializr là cách nhanh nhất).
- Truy cập start.spring.io.
- Chọn Maven hoặc Gradle, ngôn ngữ Java.
- Chọn phiên bản Spring Boot (ví dụ: 3.x).
- Thêm các Dependencies sau:
- **Eureka Server** (dưới mục Discovery & Registration)
- Spring Web (để chạy ứng dụng web cơ bản cho giao diện Eureka dashboard)
- Spring Boot Actuator (tùy chọn, nhưng tốt cho việc theo dõi sức khỏe server)
- Generate dự án và import vào IDE yêu thích của bạn.
Trong class ứng dụng chính (thường là class có annotation `@SpringBootApplication`), bạn cần thêm annotation `@EnableEurekaServer` để kích hoạt Eureka Server.
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Tiếp theo, cấu hình cho Eureka Server trong file `application.yml` (hoặc `application.properties`).
server:
port: 8761 # Cổng mặc định cho Eureka Server
eureka:
instance:
hostname: localhost # Tên host của Eureka Server (đổi khi deploy lên môi trường thật)
client:
register-with-eureka: false # Không đăng ký bản thân server này với chính nó
fetch-registry: false # Không lấy danh sách registry từ các server khác (vì đây là server đầu tiên)
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # URL để các client kết nối
Giải thích một chút về cấu hình:
- `server.port`: Cổng mà Eureka Server sẽ chạy. Theo quy ước, Eureka Server thường chạy trên cổng 8761.
- `eureka.instance.hostname`: Tên host của server.
- `eureka.client.register-with-eureka: false`: Thiết lập này cực kỳ quan trọng cho Eureka Server. Chúng ta không muốn Eureka Server tự đăng ký bản thân nó như một client vào registry của chính nó (trừ khi bạn có nhiều instance server chạy liên kết với nhau).
- `eureka.client.fetch-registry: false`: Tương tự, Eureka Server không cần lấy danh sách các dịch vụ từ các server khác (vì nó là nguồn dữ liệu). Nếu có nhiều Eureka Server, chỉ một số server nhất định cần thiết lập này là `false`, các server còn lại sẽ là `true` để đồng bộ.
- `eureka.client.serviceUrl.defaultZone`: Đây là URL mà Eureka Client sẽ sử dụng để kết nối và đăng ký/lấy thông tin registry.
Chạy ứng dụng `EurekaServerApplication`. Sau khi server khởi động, bạn có thể truy cập `http://localhost:8761/` trên trình duyệt để xem giao diện Dashboard của Eureka. Ban đầu, bạn sẽ thấy “Instances currently registered with Eureka” trống.
Xây Dựng Eureka Client (Microservice)
Bây giờ, hãy tạo một microservice mẫu đóng vai trò là Eureka Client.
- Tạo một dự án Spring Boot mới.
- Thêm các Dependencies sau:
- **Eureka Discovery Client** (dưới mục Discovery & Registration)
- Spring Web
- Spring Boot Actuator (tốt cho sức khỏe instance)
- Generate dự án và import.
Trong class ứng dụng chính của client, bạn cần thêm annotation `@EnableDiscoveryClient` (hoặc `@EnableEurekaClient` – `@EnableDiscoveryClient` là annotation chung của Spring Cloud, còn `@EnableEurekaClient` là cụ thể cho Eureka, thường dùng `@EnableDiscoveryClient` để linh hoạt hơn).
package com.example.microservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class MicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(MicroserviceApplication.class, args);
}
}
Cấu hình cho microservice client trong file `application.yml`:
spring:
application:
name: my-first-service # Tên định danh của dịch vụ này
server:
port: 8080 # Cổng của dịch vụ này (có thể chạy nhiều instance với cổng khác nhau)
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ # Địa chỉ của Eureka Server
instance:
hostname: localhost # Tên host của instance này
# instance-id: ${spring.application.name}:${random.value} # Tùy chọn: tạo ID instance độc nhất
Giải thích cấu hình client:
- `spring.application.name`: Tên logic của dịch vụ này. Đây là tên mà các dịch vụ khác sẽ sử dụng để tìm kiếm nó trong Eureka Registry. Tên này rất quan trọng và phải khớp với tên bạn muốn gọi dịch vụ.
- `server.port`: Cổng mà instance dịch vụ này chạy.
- `eureka.client.serviceUrl.defaultZone`: Địa chỉ của Eureka Server mà client này sẽ kết nối để đăng ký và lấy registry.
- `eureka.instance.hostname`: Tên host của instance dịch vụ.
Bây giờ, chạy ứng dụng microservice (`MicroserviceApplication`). Quan sát log, bạn sẽ thấy thông báo về việc đăng ký dịch vụ với Eureka Server. Sau đó, refresh trang Dashboard của Eureka Server (`http://localhost:8761/`), bạn sẽ thấy dịch vụ “MY-FIRST-SERVICE” xuất hiện dưới mục “Instances currently registered with Eureka” với địa chỉ IP và cổng của nó.
Bạn có thể chạy nhiều instance của cùng một dịch vụ bằng cách thay đổi cổng trong file cấu hình hoặc khi chạy bằng dòng lệnh (ví dụ: `java -jar target/microservice.jar –server.port=8081`). Mỗi instance sẽ đăng ký với Eureka Server và hiển thị trên Dashboard.
Eureka Hoạt Động Sâu Bên Trong
Để hiểu rõ hơn, hãy xem các cơ chế chính của Eureka:
- **Registration:** Khi một Eureka Client khởi động, nó gửi request `POST` đến Eureka Server chứa thông tin chi tiết về instance của nó (ID, hostname, IP Address, cổng, metadata…). Server lưu trữ thông tin này trong registry của nó.
- **Renewal (Heartbeat):** Sau khi đăng ký, mỗi client gửi request `PUT` (renewal/heartbeat) đến server khoảng mỗi 30 giây để báo hiệu rằng nó vẫn đang hoạt động và không bị lỗi. Nếu server không nhận được heartbeat từ một instance trong một khoảng thời gian nhất định (thường là 3 lần liên tiếp, tức khoảng 90 giây), nó sẽ đánh dấu instance đó là không khả dụng.
- **Fetching Registry:** Eureka Clients (và các Eureka Server khác) lấy registry thông tin từ server bằng cách gửi request `GET`. Client cache thông tin registry này cục bộ và cập nhật định kỳ (mặc định mỗi 30 giây). Việc cache này giúp client có thể tiếp tục hoạt động dựa trên dữ liệu cũ ngay cả khi server gặp sự cố (tính sẵn sàng).
- **Replication:** Nếu có nhiều Eureka Server, chúng sẽ đồng bộ hóa registry với nhau. Khi một client đăng ký hoặc gửi heartbeat đến một server, server đó sẽ cố gắng replicate thông tin đó đến các server ngang hàng (peer) khác.
- **Eviction:** Nếu một server không nhận được heartbeat từ một instance trong thời gian dài (ví dụ: 90 giây), nó sẽ loại bỏ instance đó khỏi registry của mình sau một thời gian chờ bổ sung. Điều này giúp loại bỏ các instance đã dừng hoặc bị lỗi.
- **Self-Preservation Mode:** Eureka Server có chế độ “tự bảo toàn”. Nếu server nhận thấy tỷ lệ heartbeats bị mất từ các client vượt quá một ngưỡng nhất định (ví dụ: trên 15% trong 15 phút), nó sẽ ngừng việc loại bỏ các instance bị đánh dấu hết hạn heartbeat. Chế độ này giúp ngăn chặn việc các instance bị loại bỏ hàng loạt do sự cố mạng giữa client và server, thay vì do bản thân instance bị lỗi thật. Khi mạng ổn định, server sẽ thoát khỏi chế độ này.
Mô hình client cache và chế độ tự bảo toàn là những lý do chính khiến Eureka ưu tiên tính sẵn sàng. Nó chấp nhận rủi ro cung cấp thông tin hơi cũ để đảm bảo các client vẫn có thể tìm kiếm dịch vụ ngay cả trong điều kiện mạng không lý tưởng.
Ưu và Nhược Điểm của Eureka
Không có công cụ nào là hoàn hảo cho mọi trường hợp. Dưới đây là bảng tổng hợp các điểm mạnh và yếu của Eureka:
Ưu Điểm (Pros) | Nhược Điểm (Cons) |
---|---|
Đơn giản để cài đặt và cấu hình với Spring Cloud. | Thiếu giao diện quản lý và giám sát mạnh mẽ (Dashboard cơ bản). |
Thiết kế ưu tiên tính sẵn sàng (Availability) theo CAP Theorem, phù hợp cho các hệ thống microservices cần hoạt động liên tục ngay cả khi mạng có sự cố nhỏ. | Có thể cung cấp dữ liệu hơi cũ (eventual consistency) cho các client do cơ chế cache và self-preservation mode. |
Tích hợp tốt với các thành phần Netflix OSS khác (Ribbon, Hystrix…) và Spring Cloud. | Không có tính năng Health Check nâng cao tích hợp sẵn ngoài Heartbeat cơ bản. Cần kết hợp với Spring Boot Actuator để có thông tin sức khỏe chi tiết hơn. |
Hỗ trợ replication giữa các server để tăng tính sẵn sàng của registry. | Yêu cầu cấu hình thủ công cho việc HA (High Availability) của server (chạy nhiều instance và cấu hình peer-awareness). |
Cộng đồng lớn và tài liệu phong phú (qua Spring Cloud). | Netflix đã chuyển Eureka sang chế độ bảo trì (maintenance mode) và sử dụng giải pháp khác nội bộ. Tuy nhiên, nó vẫn được duy trì và sử dụng rộng rãi trong cộng đồng Spring Cloud. |
Tích Hợp Với Spring Cloud LoadBalancer
Như đã đề cập, Eureka chỉ giúp các client *tìm thấy* danh sách địa chỉ của các instance dịch vụ. Nó không tự động làm nhiệm vụ Load Balancing. Để thực hiện Client-Side Load Balancing, Spring Cloud cung cấp module `spring-cloud-starter-loadbalancer` (thay thế cho Netflix Ribbon đã chuyển sang maintenance).
Khi bạn sử dụng `@EnableDiscoveryClient` và `spring-cloud-starter-loadbalancer` trong một client, Spring Cloud sẽ tự động tích hợp LoadBalancer với Eureka. Khi bạn muốn gọi một dịch vụ khác theo tên logic (ví dụ: `my-first-service`) thay vì địa chỉ IP/port cụ thể, LoadBalancer sẽ hỏi Eureka Registry (được cache trong client) để lấy danh sách các instance đang hoạt động của `my-first-service`, sau đó chọn một instance theo thuật toán load balancing (mặc định là Round Robin) và gửi request đến địa chỉ đã chọn.
Cách gọi dịch vụ khác qua tên logic thường sử dụng `RestTemplate` kết hợp với `@LoadBalanced` hoặc sử dụng Spring Cloud OpenFeign. Chúng ta sẽ đi sâu hơn vào Client-Side Load Balancing và OpenFeign trong các bài viết tiếp theo của series!
Việc kết hợp Eureka với OpenFeign và các pattern như Circuit Breaker (đã học với Hystrix/Resilience4j trong bài trước Xây dựng Ứng dụng Bền vững với Hystrix) chính là cách bạn bắt đầu xây dựng một “Stack Netflix OSS” thu nhỏ với Spring Cloud.
Lời Kết
Service Discovery là một mảnh ghép không thể thiếu trong bức tranh microservices hiện đại. Nó giải quyết vấn đề phức tạp của việc quản lý địa chỉ dịch vụ động, cho phép hệ thống của bạn scale linh hoạt và duy trì tính sẵn sàng cao. Netflix Eureka, với sự tích hợp mạnh mẽ trong Spring Cloud, cung cấp một giải pháp Service Registry tin cậy và dễ sử dụng, đặc biệt phù hợp với mô hình Client-Side Discovery.
Bằng cách thiết lập Eureka Server và cấu hình các microservices làm Eureka Client, bạn đã có một hệ thống nơi các dịch vụ có thể tìm thấy nhau một cách tự động theo tên. Đây là bước đệm quan trọng để triển khai các pattern microservices nâng cao hơn như Client-Side Load Balancing, API Gateway (chúng ta đã học về Spring Cloud Gateway), và Circuit Breaker.
Trong các bài viết tiếp theo của series “Java Spring Roadmap”, chúng ta sẽ tiếp tục khám phá cách các dịch vụ microservices giao tiếp với nhau hiệu quả và bền vững hơn, sử dụng các công cụ như Spring Cloud LoadBalancer và OpenFeign, và có thể tìm hiểu thêm về việc theo dõi request xuyên dịch vụ với Sleuth hay quản lý cấu hình tập trung với Spring Cloud Config.
Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về Service Discovery và cách bắt đầu với Eureka trong Spring Cloud. Nếu có bất kỳ câu hỏi nào, đừng ngần ngại để lại bình luận nhé!