Xây dựng Microservices bằng ASP.NET Core - Khám phá dịch vụ với Eureka

Danh sách các bài viết:

Đây là bài viết thứ ba trong loạt bài của chúng tôi về cách xây dựng microservices trên .NET Core. Trong bài đầu tiên chúng tôi đã chuẩn bị kế hoạch: kiến ​​trúc giải pháp và tình huống kinh doanh. Trong bài viết thứ hai, chúng tôi đã mô tả cách bạn có thể cấu trúc kiến ​​trúc bên trong của một microservices bằng cách sử dụng mẫu CQRS và thư viện MediatR.

Trong bài viết này, chúng ta sẽ tập trung vào khám phá dịch vụ (service discovery), đây là một trong những khái niệm cơ bản của kiến ​​trúc dựa trên microservices.

Mã nguồn cho giải pháp hoàn chỉnh có thể được tìm thấy trên GitHub của chúng tôi.

Khám phá dịch vụ là gì và tôi có cần khám phá dịch vụ không?

Nếu bạn đang xây dựng giải pháp dựa trên microservices thì sớm hay muộn (nên là sớm hơn), bạn sẽ gặp phải tình huống khi một dịch vụ này cần phải nói chuyện với một dịch vụ khác. Để thực hiện điều này, người gọi phải biết vị trí chính xác của một microservices mục tiêu trong mạng mà họ hoạt động.

Bạn phải cung cấp bằng cách nào đó địa chỉ IP và cổng nơi microservices mục tiêu lắng nghe các yêu cầu. Bạn có thể thực hiện bằng cách sử dụng tệp cấu hình hoặc biến môi trường, nhưng cách tiếp cận này có một số nhược điểm và hạn chế.

Đầu tiên là bạn phải duy trì và triển khai đúng cách các tệp cấu hình cho tất cả các môi trường của bạn: phát triển cục bộ, thử nghiệm, tiền sản xuất và sản xuất. Việc quên cập nhật bất kỳ cấu hình nào trong số này khi thêm dịch vụ mới hoặc di chuyển dịch vụ hiện có sang một nút khác sẽ dẫn đến lỗi được phát hiện trong thời gian chạy.

Thứ hai, vấn đề quan trọng hơn, là nó chỉ hoạt động trong môi trường tĩnh, có nghĩa là bạn không thể thêm/xóa động các nút, do đó bạn sẽ không thể mở rộng hệ thống của mình một cách động. Khả năng mở rộng quy mô và triển khai microservices một cách tự động là một trong những lợi thế chính của kiến ​​trúc dựa trên microservices và chúng tôi không muốn đánh mất khả năng này.

Do đó chúng tôi giới thiệu khám phá dịch vụ (service discovery). Khám phá dịch vụ là một cơ chế cho phép các dịch vụ tìm thấy vị trí mạng của nhau. Có rất nhiều triển khai khác nhau của mô hình này, nhưng trong bài viết này chúng tôi sẽ tập trung vào việc triển khai bao gồm thành phần Service RegistryService Registry Clients.

Service Registry là thành phần trung tâm duy trì danh sách các phiên bản microservices hiện đang chạy với các vị trí mạng tương ứng của chúng.

Service Registry Clients được microservices của bạn sử dụng để: đăng ký chính nó trong sổ đăng ký, để truy vấn sổ đăng ký địa chỉ của một microservices nhất định mà chúng cần giao tiếp.

Có rất nhiều triển khai hiện tại của Service Registry có sẵn. Thật không may, tôi không biết về bất kỳ giải pháp .NET gốc nào. Trong thế giới Java, có hai giải pháp chúng tôi sử dụng trong nhiều dự án: Netflix EurekaHashiCorp Consul.

Với mục đích của bài viết này, chúng tôi sẽ sử dụng Eureka vì nó có một thư viện client rất tốt cho .NET Core được phát triển và duy trì bởi Pivotal (người tạo ra Spring Framework) - Steeltoe.

Thiết lập Service Registry với Eureka

Cách đơn giản nhất để khởi động Máy chủ Eureka là sao chép kho lưu trữ GitHub và chạy với trình bao bọc Maven được đính kèm với dự án này. Điều này cho phép bạn chạy dự án Maven mà không cần cài đặt và trình bày Maven trên đường dẫn.

Chúng tôi đã sao chép kho lưu trữ vào thư mục eureka, vì vậy trong trường hợp của chúng tôi, chỉ cần thực hiện các lệnh sau:

cd eureka
mvnw spring-boot:run

Cấu hình Máy chủ Eureka được lưu trữ trong một tệp application.yml.

server:
  port: 8761

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
  server:
    waitTimeInMsWhenSyncEmpty: 0

Nếu bạn muốn kiểm tra xem Eureka đã khởi động chính xác hay chưa, hãy truy cập địa chỉ localhost:8761. Bạn sẽ thấy một cái gì đó như thế này:

Spring Eureka
Spring Eureka

Đăng ký microservices trong Eureka / Đăng ký Eureka client

Chúng tôi đã sử dụng triển khai ứng dụng khách Eureka của Steeltoe để đăng ký và tìm nạp các dịch vụ từ Máy chủ Eureka. Trong phần này, chúng tôi tập trung vào đăng ký trong sổ đăng ký dịch vụ. Chúng tôi làm ví dụ về một trong những microservices của chúng tôi - PricingService.

Bước đầu tiên là thêm các gói NuGet bắt buộc.

dotnet add package Steeltoe.Discovery.ClientCore --version 2.1.1

Bước thứ hai là thêm ứng dụng client khám phá dịch vụ trong lớp Startup.cs. Ở đây chúng ta phải thêm hai dòng, một dòng cho phương thức ConfigureServices và dòng thứ hai cho phương thức Configure.

using Steeltoe.Discovery.Client;

namespace PricingService
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDiscoveryClient(Configuration);
            //[...]
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
           //[...]
            app.UseDiscoveryClient();
        }
    }
}

Bước cuối cùng và cũng là bước quan trọng nhất là thiết lập cấu hình trong file appsettings.json

"spring" : {
    "application" : {
      "name" : "PricingService"
    }
  },
  "eureka" : {
    "client" : {
      "shouldRegisterWithEureka" : true,
      "serviceUrl" : "http://localhost:8761/eureka",
      "ValidateCertificates":  false
    },
    "instance" : {
      "appName" : "PricingService",
      "hostName" : "localhost",
      "port" : "5040”
    }
  }

Cấu hình bao gồm các yếu tố sau:

spring.application.name: chứa tên dịch vụ của chúng ta.

eureka.client chứa:

  • shouldRegisterWithEureka: cho biết liệu dịch vụ của chúng ta có nên đăng ký chính nó trong Eureka hay không, nếu chúng ta chỉ muốn gọi các dịch vụ khác, chúng ta có thể đặt nó thành false, nếu chúng ta muốn các dịch vụ khác có thể gọi dịch vụ của chúng ta thì chúng ta phải đặt nó thành true,
  • serviceUrl: địa chỉ của dịch vụ Eureka

eureka.instance: cho biết cách dịch vụ của chúng ta được đăng ký tại Eureka, chúng tôi chỉ định:

  • appName: tên dịch vụ, các dịch vụ khác sẽ có thể truy vấn địa chỉ của dịch vụ của chúng ta.
  • hostName: tên máy chủ mà dịch vụ của chúng ta đang chạy.
  • port: cổng mà dịch vụ của chúng ta đang sử dụng.

Lúc đầu, tôi hơi thất vọng vì tôi phải chỉ định tên máy chủ và cổng mà dịch vụ của tôi đang sử dụng thay vì ứng dụng khách Eureka có thể tự động phát hiện thông tin này trong thời gian chạy, nhưng nó lại là một tính năng rất hữu ích.

Trong khi chạy các dịch vụ trong một container Docker, chúng ta không muốn chúng đăng ký các cổng và địa chỉ từ địa chỉ cục bộ trong container mà phải là địa chỉ và cổng hiển thị trên mạng docker.

Có thể tìm thấy thêm thông tin liên quan đến các tùy chọn cấu hình trong tài liệu của Steeltoe.

Bây giờ chúng ta có thể chạy microservices của mình bằng cách sử dụng dòng lệnh, ví dụ. Từ thư mục kho lưu trữ gốc:

dotnet run --project ./PricingService

và mở Eureka để xem dịch vụ của bạn có hiển thị hay không. Mở trình duyệt của bạn và truy cập localhost:8761

Eureka
Eureka

Nếu mọi thứ hoạt động như mong đợi, một phiên bản dịch vụ của bạn sẽ được liệt kê trong phần “Phiên bản hiện được đăng ký với Eureka”.

Bạn có thể tạo một điểm cuối kiểm tra sức khỏe để Eureka có thể kiểm tra xem dịch vụ của bạn có hoạt động hay không. Điều này sẽ cho phép giám sát gần như thời gian thực các dịch vụ có sẵn và trạng thái của chúng thông qua bảng điều khiển Eureka.

Nếu bạn triển khai điểm cuối như vậy, bạn có thể chỉ định nó bằng cách sử dụng thuộc tính cấu hình healthCheckUrl.

Gọi một microservices khác bằng cách sử dụng khám phá dịch vụ

Bước đầu tiên của chúng ta là thêm cấu hình ứng dụng khách Eureka theo cách giống như cách chúng ta đã làm ở dịch vụ PricingService.

Sau các bước trước đó, chúng ta đã sẵn sàng gọi dịch vụ của chúng ta từ một dịch vụ khác. Chúng ta muốn gọi dịch vụ PricingService  từ dịch vụ PolicyService, bởi vì trong một bước tạo chính sách, chúng ta cần một số dữ liệu về giá từ dịch vụ PricingService.

Chúng ta sẽ kết hợp dịch vụ khám phá client Steeltoe, RestEasePolly để tạo ra một client tốt, dễ khai báo và linh hoạt cho dịch vụ PricingService của  chúng ta.

using Microsoft.Extensions.Configuration;
using Polly;
using PricingService.Api.Commands;
using RestEase;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Steeltoe.Common.Discovery;

namespace PolicyService.RestClients
{
    public interface IPricingClient
    {
        [Post]
        Task CalculatePrice([Body] CalculatePriceCommand cmd);
    }

    public class PricingClient : IPricingClient
    {
        private readonly IPricingClient client;

        private static Policy retryPolicy = Policy
            .Handle()
            .WaitAndRetryAsync(retryCount: 3, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(3));

        public PricingClient(IConfiguration configuration, IDiscoveryClient discoveryClient)
        {
            var handler = new DiscoveryHttpClientHandler(discoveryClient);
            var httpClient = new HttpClient(handler, false)
            {
                BaseAddress = new Uri(configuration.GetValue("PricingServiceUri"))
            };
            client = RestClient.For(httpClient);
        }

        public Task CalculatePrice([Body] CalculatePriceCommand cmd)
        {
            return retryPolicy.ExecuteAsync(async () => await client.CalculatePrice(cmd));
        }
    }
}

Chúng ta đã khai báo một interface đại diện cho các hoạt động được hiển thị bởi PricingService.

Chúng ta đã triển khai giao diện này sử dụng ứng dụng client khám phá dịch vụ sẽ lấy địa chỉ PricingService từ sổ đăng ký dịch vụ trong Eureka, kết hợp với Polly và RestEase.

Các dòng quan trọng nhất từ ​​ví dụ này là trình xử lý và tạo HTTP client:

var handler = new DiscoveryHttpClientHandler(discoveryClient);
var httpClient = new HttpClient(handler, false)
{
    BaseAddress = new Uri(configuration.GetValue("PricingServiceUri"))
};

Chúng tôi sử dụng URL PricingService được cấu hình trong file appsettings.json và trông giống như sau:

"PricingServiceUri" : "http://PricingService/api/pricing"

Như bạn có thể thấy địa chỉ trong cấu hình trỏ đến tên dịch vụ, không phải địa chỉ mạng thực.

Bước cuối cùng là đăng ký client của chúng ta vào IoC container. Với mục đích này, chúng ta tạo ra một lớp được gọi là RestClientsInstaller.

public static class RestClientsInstaller
{
    public static IServiceCollection AddPricingRestClient(this IServiceCollection services)
    {
        services.AddSingleton(typeof(IPricingClient), typeof(PricingClient));
        //...
        return services;
    }
}

và chúng ta sử dụng lớp này trong Startup.cs như sau:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDiscoveryClient(Configuration);
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddMediatR();
    services.AddPricingRestClient();
    services.AddNHibernate(Configuration.GetConnectionString("DefaultConnection"));
    services.AddRabbit();
}

Bây giờ bạn có thể inject IPricingClient và sử dụng nó để gọi PricingService.

Tóm lược

Khám phá dịch vụ là một trong những nguyên tắc cơ bản của kiến ​​trúc dựa trên microservices. Nếu không có nó, cần phải có rất nhiều công việc thủ công và dễ xảy ra lỗi để cấu hình và triển khai microservices đúng cách.

Việc thiết lập tính năng khám phá dịch vụ với Eureka và Steeltoe khá dễ dàng và cung cấp cho bạn khả năng thêm và xóa động các dịch vụ. Ngoài ra, nó cũng loại bỏ nhu cầu về địa chỉ mã cứng của các dịch vụ cần giao tiếp.

Tất nhiên, giải pháp này chỉ là một trong những cách khả thi của cơ chế khám phá dịch vụ. Bạn cũng có thể thử các cách tiếp cận thay thế với Kubernetes, NGINX hoặc Consul.

Bài viết này được dịch từ bài viết gốc ở đây.

MicroservicesASP.NET CoreASP.NET Core Web APIApache EurekaPollyService Discovery
Bài Viết Liên Quan:
Xây dựng Microservices bằng ASP.NET Core - Định hình kiến trúc Microservices với CQRS và MediatR
Trung Nguyen 03/05/2021
Xây dựng Microservices bằng ASP.NET Core - Định hình kiến trúc Microservices với CQRS và MediatR

Trong bài viết này, chúng ta sẽ thiết kế kiến trúc bên trong của Microservices với mẫu CQRS và MediatR trong ASP.NET Core.

Xây dựng Microservices bằng ASP.NET Core - Lập kế hoạch
Trung Nguyen 03/05/2021
Xây dựng Microservices bằng ASP.NET Core - Lập kế hoạch

Trong loạt bài viết này, chúng tôi sẽ giới thiệu cho các bạn các tác vụ điển hình cần thiết để xây dựng giải pháp dựa trên microservices bằng ASP.NET Core.

Sử dụng Envoy Proxy để cải thiện độ tin cậy, bảo mật và khả năng giám sát của Microservices
Trung Nguyen 28/11/2020
Sử dụng Envoy Proxy để cải thiện độ tin cậy, bảo mật và khả năng giám sát của Microservices

Vì sao Envoy proxy được sử dụng trong nhiều dự án thương mại và mã nguồn mở để cải thiện tính bảo mật, độ tin cậy và khả năng giám sát.

Vai trò của Service Mesh và API Gateway trong Microservices
Trung Nguyen 28/11/2020
Vai trò của Service Mesh và API Gateway trong Microservices

Trong bài viết này, tôi sẽ nói chi tiết về Service Mesh (lưới dịch vụ), API Gateway trong Microservices và thảo luận khi nào thì sử dụng chúng.