Distributed Tracing trong ASP.NET Core với Jaeger và Tye

Các ứng dụng microservices hiện đại bao gồm nhiều dịch vụ được triển khai trên các máy chủ khác nhau như Kubernetes, AWS ECS và Azure App Services hoặc các dịch vụ máy tính không máy chủ (serverless) như AWS Lambda và Azure Functions.

Một trong những thách thức chính của microservices là giảm khả năng hiển thị của các yêu cầu trải dài trên nhiều dịch vụ. Trong các hệ thống phân tán thực hiện các hoạt động khác nhau như truy vấn cơ sở dữ liệu, xuất bản và sử dụng thông báo cũng như kích hoạt các công việc, bạn sẽ nhanh chóng tìm ra các vấn đề và giám sát hành vi của các dịch vụ như thế nào? Câu trả lời cho vấn đề phức tạp này là Theo dõi phân tán (Distributed Tracing).

Distributed Tracing, Open Tracing và Jaeger

Theo dõi phân tán (Distributed Tracing) là khả năng của giải pháp theo dõi mà bạn có thể sử dụng để theo dõi một yêu cầu trên nhiều dịch vụ. Các giải pháp theo dõi sử dụng một hoặc nhiều Id tương quan (correlation Id) để đối chiếu các dấu vết request và lưu trữ các dấu vết, là các sự kiện nhật ký có cấu trúc trên các dịch vụ khác nhau, trong cơ sở dữ liệu trung tâm.

Open Tracing định nghĩa một API mở, trung lập với nhà cung cấp để theo dõi phân tán. Thông số kỹ thuật cho phép các nhà cung cấp như Jaeger và Zipkin triển khai chức năng theo dõi phân tán độc đáo của họ và cho phép người dùng tránh bị nhà cung cấp khóa thực hiện theo dõi cụ thể.

Jaeger là một hệ thống theo dõi mã nguồn mở dành cho các microservices và nó hỗ trợ tiêu chuẩn Open Tracing. Ban đầu nó được xây dựng và có nguồn mở bởi Uber Technologies và bây giờ là một dự án đã tốt nghiệp của CNCF. Một số trường hợp sử dụng cấp cao của Jaeger như sau:

  1. Tối ưu hóa hiệu suất và độ trễ.
  2. Giám sát giao dịch phân tán.
  3. Phân tích sự phụ thuộc của dịch vụ.
  4. Truyền bối cảnh phân tán.
  5. Phân tích nguyên nhân gốc rễ.

Jaeger bao gồm nhiều thành phần như sau:

  1. Thư viện ứng dụng client: Đây là những triển khai theo ngôn ngữ cụ thể của API Open Tracing.
  2. Agent: là một daemon mạng thu thập các khoảng thời gian từ ứng dụng và gửi chúng đến bộ thu thập.
  3. Collector: nhận dấu vết từ agent và chạy chúng qua một đường ống xử lý để xác nhận dấu vết, lập chỉ mục chúng, thực hiện bất kỳ chuyển đổi nào và cuối cùng là lưu trữ chúng.
  4. Query: Dịch vụ truy vấn lấy dấu vết từ bộ nhớ và kết xuất giao diện người dùng để hiển thị chúng.

Bạn có thể chạy Jaeger trên môi trường cục bộ của mình (và môi trường CI / CD) bằng cách sử dụng container image all-in-one của Jaeger chạy tất cả các thành phần của Jaeger trong một container. Trong môi trường sản xuất, bạn nên chạy từng thành phần một cách độc lập. Các hướng dẫn triển khai trên trang web của Jaeger bao gồm hướng dẫn và khuyến nghị về các mô hình triển khai.

Jaeger all-in-one

Thực thi lệnh sau để chạy một container Jaeger được hỗ trợ bởi thành phần lưu trữ trong bộ nhớ có tên là Badger.

docker run -d \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 16686:16686 \
  -p 5778:5778  \
  --name jaeger jaegertracing/all-in-one:1.22

Khi container đang chạy, bạn có thể kiểm tra bảng điều khiển Jaeger bằng cách truy cập http://localhost:16686/.

Ứng dụng demo: DCalculator

Để bắt chước một ứng dụng microservices, chúng ta sẽ tạo hai Web API viết bằng ASP.NET Core phụ thuộc lẫn nhau để mọi yêu cầu đều truyền qua hai API, do đó tạo ra các dấu vết phân tán.

DCalculator là một ứng dụng máy tính phân tán bao gồm một số microservices, mỗi microservice thực hiện một phép toán duy nhất. Để giữ cho bản demo ngắn gọn, chúng ta sẽ tạo một microservice có tên là Calculator chấp nhận các tham số cần thiết để tính toán Log của một số cho một cơ sở cụ thể. Chúng ta cũng sẽ tạo một dịch vụ khác có tên LogService nhận yêu cầu tính giá trị Log từ dịch vụ Calculator và sử dụng công thức sau để tính Log của một số- N cho một cơ số x cho trước.

Logₓ(N) = Logₐ(N)/Logₐ(x)

Khởi chạy thiết bị đầu cuối của bạn và tạo một project Web API có tên Calculator bằng cách sử dụng lệnh sau:

dotnet new webapi -n Calculator -o calculator

Trong thiết bị đầu cuối của bạn, hãy chuyển sang thư mục Calculator và cài đặt các gói sau:

dotnet add package Jaeger
dotnet add package OpenTracing.Contrib.NetCore

Chỉnh sửa phương thức ConfigureServices trong lớp Startup như sau để ghi dấu vết vào cài đặt Jaeger localhost mặc định của chúng ta. Để biết chi tiết về cách sử dụng thư viện, vui lòng tham khảo tài liệu thư viện Jaeger C# Client.

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddOpenTracing();
    // Adds the Jaeger Tracer.
    services.AddSingleton<ITracer>(sp =>
    {
        var serviceName = sp.GetRequiredService<IWebHostEnvironment>().ApplicationName;
        var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
        var reporter = new RemoteReporter.Builder().WithLoggerFactory(loggerFactory).WithSender(new UdpSender())
            .Build();
        var tracer = new Tracer.Builder(serviceName)
            // The constant sampler reports every span.
            .WithSampler(new ConstSampler(true))
            // LoggingReporter prints every reported span to the logging framework.
            .WithReporter(reporter)
            .Build();
        return tracer;
    });

    services.Configure<HttpHandlerDiagnosticOptions>(options =>
        options.OperationNameResolver =
            request => $"{request.Method.Method}: {request?.RequestUri?.AbsoluteUri}");
}

Chúng ta đã đăng ký interface ITracer của Open Tracing với dependency injection để tạo thể hiện của Jaeger Tracer. Sau này chúng ta sẽ sử dụng interface ITracer để tạo các khoảng thời gian. Khoảng thời gian là một đơn vị công việc hoặc thời gian trong ứng dụng của bạn. Một khoảng có thể được lồng bên trong một khoảng khác và nó có thể chứa các thẻ và nhật ký.

Bạn có thể sử dụng mẫu tùy chọn trong ASP.NET Core để cấu hình HttpHandlerDiagnosticOptions, được sử dụng để tùy chỉnh các thuộc tính khoảng thời gian. Chúng ta đã cấu hình nó để tùy chỉnh tên hoạt động để hiển thị HTTP Verb và URL tuyệt đối của request. Bạn cũng có thể sử dụng nó để tùy chỉnh các tính năng khác như bỏ qua các request cụ thể, thêm thẻ vào các khoảng thời gian và sửa đổi các khoảng thời gian được tạo khi xảy ra lỗi.

Open Telemetry Trace là một Đồ thị Acyclic Có hướng (DAG) của các Khoảng thời gian. Một đỉnh nối hai khoảng thời gian được gọi là Tham chiếu.

Hãy thêm một controller có tên CalculatorController vào ứng dụng của chúng ta và thiết lập nó để gửi yêu cầu đến service Log. Chúng tôi cũng sẽ thêm một khoảng thời gian tùy chỉnh vào controller của chúng ta để ghi lại hoạt động của chúng ta như sau:

public class CalculatorController : ControllerBase
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly ITracer _tracer;

    public CalculatorController(IHttpClientFactory clientFactory, ITracer tracer)
    {
        _clientFactory = clientFactory;
        _tracer = tracer;
    }

    [HttpGet("log")]
    public async Task<ActionResult> ComputeLog(int n, int x)
    {
        var actionName = ControllerContext.ActionDescriptor.DisplayName;
        using var scope = _tracer.BuildSpan(actionName).StartActive(true);
        var client = _clientFactory.CreateClient("logService");
        var response = await client.GetAsync($"/log/compute?n={n}&x={x}");
        return response.IsSuccessStatusCode
            ? Ok(Convert.ToDouble(await response.Content.ReadAsStringAsync()))
            : Problem("Log service failed");
    }
}

Dịch vụ Máy tính của chúng tôi hiện đã sẵn sàng. Tạo ứng dụng WebAPI của service Log và chỉnh sửa lớp Startup để thêm Open Tracing và thiết lập bộ theo dõi Jaeger giống như bạn đã làm trước đó.

Để chứng minh cách bạn có thể ghi nhật ký với các khoảng thời gian, hãy tạo một controller có tên LogController và thêm một điểm cuối có tên Compute. Điểm cuối Compute sẽ tính toán Log của số cho cơ số đã cho và trả về kết quả trong phản hồi. Chúng ta sẽ ghi lại hoạt động này trong một khoảng thời gian tùy chỉnh và cũng thêm nhật ký vào khoảng thời gian như sau:

public class LogController : ControllerBase
{
    private readonly ITracer _tracer;

    public LogController(ITracer tracer)
    {
        _tracer = tracer;
    }

    [HttpGet("compute")]
    public ActionResult<double> Compute(int n, int x)
    {
        var actionName = ControllerContext.ActionDescriptor.DisplayName;
        using var scope = _tracer.BuildSpan(actionName).StartActive(true);
        scope.Span.Log($"Requested log compute of #{n}, base {x}");
        return Ok(Math.Log(n) / Math.Log(x));
    }
}

Hãy khởi chạy cả hai dịch vụ và gọi điểm cuối Log của dịch vụ Calculator một vài lần để tạo ra một vài dấu vết.

Truy cập endpoint Log của dịch vụ Calculator

Vui lòng truy cập giao diện người dùng Jaeger tại http://localhost:16686 để xem các dấu vết mà chúng ta đã tạo. Trong bảng Find Traces, nhấp vào menu thả xuống, chọn dịch vụ Calculator từ danh sách và nhấp vào nút Find Traces.

Truy cập giao diện người dùng Jaeger để xem các dấu vết

Màn hình theo dõi trình bày một số thông tin hữu ích về yêu cầu (request), chẳng hạn như:

  1. Thời hạn của yêu cầu.
  2. Tên của các dịch vụ và các khoảng thời gian được các dịch vụ ghi lại trong dấu vết.
  3. Thời gian của yêu cầu.

Nhấp vào một dấu vết để xem các khoảng thời gian có trong đó.

Kiểm tra dấu vết

Trong màn hình theo dõi mở rộng, bạn có thể kiểm tra chi tiết từng khoảng thời gian. Bạn có thể sử dụng màn hình này để xác định độ trễ do các phần phụ thuộc thêm vào một yêu cầu và tối ưu hóa ứng dụng của bạn. Bạn cũng có thể xem chi tiết của một khoảng bằng cách nhấp vào nó. Hãy xem câu lệnh Log mà dịch vụ Log đã thêm vào khoảng thời gian.

Xem khoảng thời gian

Bạn có thể xem các dịch vụ phụ thuộc trong tab Dependencies. Ví dụ: màn hình sau cho thấy rằng dịch vụ Calculator phụ thuộc vào dịch vụ Log:

Xem dịch vụ phụ thuộc

Tóm lược

Trong bài viết ngắn này, chúng ta đã thảo luận về những lợi ích của việc theo dõi phân tán (distributed tracing). Chúng ta đã học cách thiết lập Jaeger trong môi trường cục bộ và tích hợp của nó với các ứng dụng ASP.NET Core.

Trong phần sau của loạt bài này, chúng ta sẽ sử dụng ứng dụng này để tìm hiểu Microsoft Tye. Tye là một công cụ dành cho nhà phát triển thử nghiệm của Microsoft giúp việc xây dựng, thử nghiệm và triển khai microservices trở nên dễ dàng hơn. Hãy cùng tìm hiểu xem nó ảnh hưởng như thế nào đến quá trình triển khai và phát triển ứng dụng của chúng ta.

ASP.NET Core Web APIASP.NET CoreJaegerDistributed Tracing
Bài Viết Liên Quan:
Cách xây dựng REST API sử dụng ASP.NET Core, Entity Framework Core và JWT
Trung Nguyen 08/04/2021
Cách xây dựng REST API sử dụng ASP.NET Core, Entity Framework Core và JWT

Trong hướng dẫn này, chúng ta sẽ học cách tạo CRUD REST API bằng ASP.NET Core, Entity Framework Core và bảo mật API bằng JWT.

Triển khai microservices với gRPC và ASP.NET Core 3.1
Trung Nguyen 16/03/2021
Triển khai microservices với gRPC và ASP.NET Core 3.1

Tìm hiểu cách xây dựng các dịch vụ hiệu suất cao trong microservices với .NET Core 3.1 và gRPC.

Hướng dẫn tuyệt vời về cách xây dựng API RESTful với ASP.NET Core
Trung Nguyen 04/10/2020
Hướng dẫn tuyệt vời về cách xây dựng API RESTful với ASP.NET Core

Hướng dẫn từng bước về cách triển khai các API RESTful sạch, có thể bảo trì sử dụng ASP.NET Core.