Trong microservice, bên cạnh API Gateway pattern rất phổ biến, còn có Service Discovery (khám phá dịch vụ). Service Discovery cũng tương tự như DNS (Domain Name Service). Thay vì phải sử dụng địa chỉ IP khó nhớ hoặc có thể biến động, các client, thiết bị, dịch vụ sẽ tìm thấy nhau bằng tên dễ nhớ và có một dịch vụ chung để chuyển tên dễ nhớ thành địa chỉ IP. Spring Cloud có thư viện Eureka Service Discovery để giúp các dịch vụ REST, Web đăng ký với nó sau đó giúp các dịch vụ này tìm kiếm nhau, kết nối với nhau đơn giản hơn. Eureka là thư viện do Netflix viết từ đầu, sau đó được team Spring thuộc Pivotal và cộng đồng tham gia phát triển tiếp. Mã nguồn minh hoạ cho bài viết này ở đây

0. Service Discovery là gì?

Tôi phát triển một web site thương mại điện tử trên kiến trúc microservice. Phía ngoài cùng sẽ là API Gateway cùng module Authentication/Authorization, phía sau là rất nhiều các service chuyên biệt gọi lẫn nhau chứ không dùng chung một database:

  • Catalog: danh mục mặt hàng

  • Sales: quản lý order, thanh toán, khuyến mại, giảm giá...

  • Merchant: quản lý các nhà cung cấp

  • Inventory: quản lý kho - nhập / xuất, hàng tồn kho

  • Guarantee: dịch vụ bảo hành sửa chữa

  • User: quản lý tất cả các user tham gia hệ thống

Mỗi service không chỉ phục vụ các request từ API Gateway điều hướng tới, mà còn phục vụ dữ liệu cho các service khác. Có rất nhiều vấn đề mà chúng ta phải đối mặt để đảm bảo hệ thống hoạt động thông suốt. Có những pattern mà bạn chắc đã nghe như:

  • Service Discovery: khám phá dịch vụ --> Đây là pattern bài viết này tập trung vào
  • Centralize Configuration: cấu hình tập trung
  • Load Balancing: căn bằng tải
  • Circuit Breaker: cầu trì ngắt lỗi
  • Tracing - Monitoring: theo dõi
  • Routing: định tuyến
  • Message Queue: trao đổi bằng thông điệp
  • Event Sourcing - CQRS

Service Discovery giúp các service có thể nói chuyện trực tiếp với nhau dễ dàng hơn. Giả sử hệ thống có 20 service hoặc nhiều hơn. Chúng ta không để gán từ tên miền cho mỗi service được. Một số service có thể chung địa chỉ IP nhưng khác cổng. Một số service lại phân tải ra nhiều node ở các địa chỉ IP khác nhau. Service cũng có thể bị lỗi, khởi động lại, lại được cấp một địa chỉ IP mới tinh.

Service Discovery hoạt động giống như một Domain Name Server: mỗi service bật lên, cần nối đến để đăng ký địa chỉ IP, cổng, tên servide, đồng thời lấy danh sách của service khác về. Như thế mọi service sẽ biết lẫn nhau kịp thời.

Netflix là một dịch vụ cho thuê phim số 1 thế giới. Để đáp ứng số lượng thuê bao khồng lồ, trên toàn thế giới, Netflix kiến trúc hệ thống mô hình microservice từ 2008. Eureka là một thư viện được Netflix phát triển phục vụ cho hệ thống microservice của mình. Sau một thời gian, Netflix mở mã nguồn, phiên bản Eureka 1.0. Trong lúc chuẩn bị nâng cấp lên phiên bản 2.0, thì Spring Cloud một dự án mã nguồn mở của Pivotal, đã tích hợp Netflix thành https://github.com/spring-cloud/spring-cloud-netflix. Do đó phiên bản Eureka 2.0 sẽ không phát triển tiếp.

Nói thêm về Pivotal một chút, đây là công ty bảo trợ cho rất nhiều phần mềm mã nguồn mở nổi tiếng như:

Pivotal bị VMWare mua lại. Sau đó VMWare lại bị Dell Technologies mua lại. Dell Technologies thuộc tập đoàn Dell, có người anh em Dell Computer chuyên sản xuất laptop, workstation mà các người anh em lập trình Việt nam hay dùng rồi cài Hackintosh đó.

Những gì tôi trình bày dưới đây sẽ về thư viện Service Discovery của Spring Cloud, đã học hỏi, vay mượn từ Netflix Eureka để phát triển lên.

1. Mô hình hệ thống demo

Để tối giản, tôi lập trình 3 dự án:

  • "Eureka Server" trong thư mục service_registry.

  • "Shop" REST API trả về danh sách sản phẩm.

  • "Web" Ứng dụng web cần gọi vào "Shop" để lấy dữ liệu.

    Cả "Shop" và "Web" sẽ là Eureka Client, cần kết nối vào Eureka Server.

2. Eureka service registry

Chúng ta tạo dự án Spring Boot sử dụng https://start.spring.io/ sử dụng thư viện Eureka Server

.
├── main
│   ├── java
│   │   └── vn
│   │       └── techmaster
│   │           └── service_registry
│   │               └── ServiceRegistryApplication.java
│   └── resources
│       ├── application.properties
│       └── application.yml <--Khai báo cấu hình Eureka Server

Trong file ServiceRegistryApplication.java hãy bổ xung annotation @EnableEurekaServer

package vn.techmaster.service_registry;

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

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceRegistryApplication.class, args);
    }
}

Trong thư mục resource, tạo file cấu hình application.yml

server:
  port: 8761
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false

Eureka server lắng nghe ở cổng 8761. Vì đây là server nên phần Eureka client chúng ta không cần đăng ký registerWithEureka: false và cũng không cần lấy danh sách các service đăng ký về fetchRegistry: false

Hãy chạy dự án này, rồi dùng trình duyệt vào địa chỉ http://localhost:8761/

3. REST server 'shop'

Để tạo REST API, tôi cần một số thư viện:

  • spring-boot-starter-web: để tạo REST Controller

  • spring-cloud-starter-netflix-eureka-client: đăng ký Eureka server và lấy danh sách service về

  • spring-boot-starter-actuator: tạo dashboard để cung cấp thông tin tình trạng của server

  • lombok: giúp định nghĩa class model ngắn gọn, đơn giản hơn

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
</dependency>

Cấu trúc thư mục dự án như sau:

.
├── main
│   ├── java
│   │   └── vn
│   │       └── techmaster
│   │           └── shop
│   │               ├── controller
│   │               │   └── ProductController.java <-- REST Controller
│   │               ├── model
│   │               │   └── Product.java <-- định nghĩa model
│   │               └── ShopApplication.java
│   └── resources
│       ├── application.properties <-- cấu hình Actuator
│       └── application.yml <-- cấu hình Eureka Client

File application.yml cấu hình kết nối vào Eureka server.

spring:
  application:
    name: shop
server:
  port: 0
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
  instance:
    preferIpAddress: true

Giải thích:

  • server.port = 0 cho phép chọn ngẫu nhiên port mà REST server phục vụ.
  • eureka.client.serviceUrl.defaultZone khai báo đường dẫn đến Eureka Server. http://localhost:8761/eureka.
  • instance.preferIpAddress: true: yêu cầu Eureka server trả về danh sách service với địa chỉ IP của mỗi service thay vì tên miền nếu có. Việc này giúp kết nối sẽ nhanh hơn, bỏ qua giai đoạn dịch từ tên miền sang IP nhờ DNS.

File application.properties cấu hình cho Actuator

management.endpoints.enabled-by-default=true
management.endpoint.info.enabled=true
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
info.app.name=@project.name@
info.app.description=@project.description@
info.app.version=@project.version@
info.app.encoding=@project.build.sourceEncoding@
info.app.java.version=@java.version@

File ShopApplication.java bổ xung annotation @EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient
public class ShopApplication {
    ...
}

File ProductController.java chỉ có một phương thức trả về danh sách sản phẩm

@RestController
@RequestMapping("/")
public class ProductController {
    @GetMapping("products")
    public List<Product> getProducts() {
        return new ArrayList<>(List.of(new Product("Nike 360", 100), new Product("Sony WXMH4", 200)));
    }
}

Hãy biên dịch và chạy, rồi quay lại đường dẫn http://localhost:8761 refresh sẽ thấy dịch vụ "shop" đã đăng ký với Eureka

Ấn vào link sẽ mở ra Actuator info của dịch vụ "shop"

Gõ đường dẫn đến /products thì có dữ liệu trả về

Vậy chúng ta sẽ dựng được Eureka server. Tiếp đến dựng REST server có tên là shop, biến nó thành Eureka client, kết nối vào Eureka server.

4. Web App

Bước cuối cùng, tôi sẽ tạo một Spring Boot Web app sử dụng Thymeleaf để render HTML sau khi lấy dữ liệu từ REST server "shop". Nếu không có Eureka server, tôi cần gán cứng cho REST server "shop" một địa chỉ IP tĩnh, rồi từ dịch vụ khác dùng địa chỉ IP này gọi vào.

Cách này không hay ở chỗ: dải IP tĩnh không còn nhiều, nếu số lượng dịch vụ tăng nhanh thì sẽ rất tốn địa chỉ IP tĩnh cấp phát. Việc ghi nhớ dịch vụ nào nằm ở IP nào cũng không phù hợp với hệ thống lớn, phức tạp. Do đó, tôi tiếp tục biến Web App thành Eureka client. Khi nó khởi động sẽ đăng ký địa chỉ IP, các thông tin cần thiết với Eureka và nhận về danh sách các dịch vụ khác.

.
├── main
│   ├── java
│   │   └── vn
│   │       └── techmaster
│   │           └── web
│   │               ├── controller
│   │               │   ├── APIController.java
│   │               │   └── WebController.java <-- Controller render web
│   │               ├── feign
│   │               │   └── ShopClient.java <-- OpenFeign interface định nghĩa kết nối đến REST
│   │               ├── model
│   │               │   └── Product.java
│   │               └── WebApplication.java <-- Thêm annotation @EnableFeignClients
│   └── resources
│       ├── static
│       │   └── diagram.jpg
│       ├── templates
│       │   └── products.html <-- File view template
│       ├── application.properties <-- cấu hình Actuator
│       └── application.yml <-- cấu hình Eureka client

Ứng dụng này cũng là Eureka client, nên file cấu hình application.yml cũng tương tự như dịch vụ REST. Điểm khác biệt là nó sẽ gọi sang REST server "shop" để lấy dự liệu. Do đó tôi tạo và dùng Open Feign client bằng 3 bước:

Bước 1: trong file pom.xml nhớ bổ xung dependency

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Bước 2: định nghĩa public interface ShopClient rồi đánh dấu nó bằng @FeignClient("shop")

package vn.techmaster.web.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import vn.techmaster.web.model.Product;

import java.util.List;

@FeignClient("shop")
public interface ShopClient {
    @GetMapping("/products")
    public List<Product> getProducts();
}

Bước 3: Sử dụng Open Feign client trong WebController.java

@Controller
public class WebController {
    @Autowired    private ShopClient shopClient;

    @GetMapping("/")
    public String showProducts(Model model) {
        List<Product> products = shopClient.getProducts();
        model.addAttribute("products", products);
        return "products.html";
    }
}

Khi chạy dịch vụ web này xong, rồi quay lại http://localhost:8761 sẽ thấy application "WEB" được bổ xung vào danh sách đăng ký.

Vào địa chỉ http://localhost:8080 sẽ thấy dữ liệu đã được xuất ra HTML

6. Mấy điểm cần lưu ý trong ví dụ demo này

  1. Tôi chưa có áp dụng các phương pháp bảo mật gì cả. Code chỉ dùng để minh hoạ Service Discovery

  2. Ở cả REST API và Web đều phải định nghĩa model Product. Nếu có nhiều dịch vụ cùng dùng chung một số model, bạn nên tạo ra một package riêng chứa tất cả các model này.

    public class Product {
            private String name;
            private int price;
    }
  3. Trong thực tế triển khai lên production, mỗi dịch vụ sẽ được đóng gói thành docker container. Sau đó chúng ta dùng Docker Swarm hay Kubernetes để vận hành các docker container, việc khai báo đường dẫn đến Eureka Server cần phải chỉnh lại cho phù hợp. Tôi sẽ viết ở bài sau.

7. Những lựa chọn khác thay thế Eureka Service Discovery

Netflix Eureka được tạo ra vào thời kỳ 2008-2010, khi Netflix chuyển từ mô hình Monolithic sang Microservice. Khi đó Docker, Kubernetes chưa có hay phổ biến như hiện nay. Thậm chí khái niệm Mesh service chưa có nốt. Giờ đây Kubernetes cũng đã có tính năng Service Discovery. Điểm khác biệt là Kubernetes Service Discovery áp dụng cho mọi ngôn ngữ lập trình, framework, và không cần phải tác động vào code của từng dịch vụ (thêm Eureka Client, rồi bổ xung annotation, @EnableEurekaClient, cấu hình application.yml..). Rồi service mesh ra đời, với khái niệm Side Car (thùng xe ghép vào xe motor), giúp lập trình không phải cấu hình, sửa đổi code ứng dụng mà vẫn có được tính năng service discovery thậm chí còn thu thập thông tin sức khoẻ từng service để báo cáo tập trung.

Tham khảo:

8. Kết luận

Eureka - Spring Cloud Service Discovery cũng có chút lạc hậu tại thời điểm hiện nay. Nó chỉ phù hợp với những dự án Spring Boot, Quarkus hay các ngôn ngữ lập trình có thư viện kết nối đến Eureka server. Thực ra cấu trúc API của Eureka server đơn giản, tuân thủ REST/JSON. Do đó bạn có thể biến mọi service dùng bất kỳ ngôn ngữ lập trình nào cũng có thể nối đến Eureka và được quản lý. Nếu dự án thuần Java Spring Boot, bạn dùng Eureka Server tốt vì nó đơn giản, có sẵn. Còn nếu dự án microservice có đủ loại ngôn ngữ lập trình, công nghệ khác nhau, hãy cân nhắc Kubernetes service discovery hay Istio service mesh. Một ngày rảnh rỗi, tôi sẽ dựng Kubernetes, Istio Service Mesh để quản lý các dịch vụ viết bằng Spring Boot, Golang, Node.js. Giờ tôi phải dừng ở đây. Hãy thường xuyên vào Blog Techmaster để đọc những bài viết mới nhé.