0
0
Lập trình
Admin Team
Admin Teamtechmely

Sử dụng EventPipe để Phân Tích Lỗi Tăng RAM trong Ứng Dụng .NET trên Môi Trường Sản Xuất

Đăng vào 2 tuần trước

• 7 phút đọc

Tóm tắt cho những ai không có thời gian đọc: Nếu ứng dụng .NET của bạn trên môi trường sản xuất gặp vấn đề với việc sử dụng RAM, có một phương pháp hiệu quả mà không làm ảnh hưởng đến hiệu suất của server: hãy ghi lại stack trace mỗi khi Garbage Collection (GC) xảy ra. Khi lượng RAM tăng vọt, bạn có thể tra cứu log để xem stack trace nào thường xuất hiện, từ đó xác định vùng mã đang gây ra tình trạng tiêu tốn RAM. Dưới đây là phần Code mẫu cho bạn tham khảo.

Vấn đề cần giải quyết

Khi ứng dụng của bạn gặp phải tình trạng sử dụng nhiều RAM không cần thiết trên môi trường sản xuất, có một số vấn đề cần đến sự can thiệp:

  • Lượng RAM của ứng dụng đột ngột tăng cao và việc lấy dump của quá trình ứng dụng để phân tích có thể tốn thời gian, đồng thời có thể ảnh hưởng đến trạng thái hiện tại của RAM.
  • Các công cụ trace trên mã nguồn có thể làm chậm ứng dụng, do đó không thể bật tracing liên tục.

Phương pháp tiếp cận

Chúng ta cần một phương pháp nhẹ nhàng có thể hoạt động liên tục trong môi trường sản xuất. Ý tưởng chính là sử dụng cơ chế Garbage Collection (GC) trong .NET, thường kích hoạt khi mã cấp phát bộ nhớ. Bằng cách ghi lại stack trace vào log mỗi lần GC được thực hiện, ta có thể xác định các luồng mã (code flow) nào đang kích hoạt GC. Những luồng này nếu xuất hiện thường xuyên trong log, có khả năng đang tiêu tốn RAM.

Tóm tắt về Garbage Collection trong .NET

Khác với ngôn ngữ lập trình C, trong .NET, bộ nhớ được quản lý tự động qua Garbage Collector (GC). Khi bạn tạo một đối tượng bằng new, runtime sẽ cấp phát một vùng nhớ và bạn không cần phải xóa thủ công; runtime sẽ dọn dẹp các đối tượng không còn sử dụng khi cần thiết. Quá trình này gọi là Garbage Collection (GC).

Giới thiệu về Tracing trong .NET

Tracing là quá trình ghi lại mọi sự kiện trong ứng dụng của bạn, thường để phân tích hoặc theo dõi hiệu suất. Event Tracing for Windows (ETW) là một trong những hệ thống tracing tốt cho Windows, trong khi trên Linux, bạn có thể sử dụng LTTng. Nếu bạn muốn một giải pháp độc lập với hệ điều hành, bạn nên sử dụng EventPipe trong .NET, vì nó được tích hợp sẵn trong runtime

Cách áp dụng cho trường hợp của chúng ta

Chúng ta sẽ thực hiện theo các bước sau:

  • Ứng dụng sẽ đóng vai trò là provider phát ra các sự kiện cho consumer.
  • Provider sẽ phát ra các sự kiện về GC cho consumer.
  • Tạo một ứng dụng console để vừa đóng vai trò controller, vừa là consumer để:
    • Khởi tạo một session EventPipe để nhận các sự kiện GC từ provider.
    • Xử lý các sự kiện nhận được để hiển thị stack trace.

Hai lợi ích chính của phương pháp này:

  • Provider chỉ phát sự kiện khi session đang hoạt động, nghĩa là bạn không cần thay đổi mã nguồn đang chạy trên production và có thể tắt tracing mà không cần khởi động lại ứng dụng.
  • GC không được kích hoạt quá thường xuyên, do đó việc ghi log stack trace sẽ không ảnh hưởng đáng kể đến hiệu suất ứng dụng.

Sự kiện trong Session EventPipe

Ứng dụng cần phát ra sự kiện sau:

  • GCTriggered: được gửi mỗi khi .NET kích hoạt GC.

Lưu ý:

  • Dù có sự kiện GCAllocationTick_V3 có thể ghi lại mức RAM được cấp phát cho mỗi đối tượng, không nên sử dụng nó do có thể ảnh hưởng đến hiệu suất. Dù vậy, nó phù hợp cho việc theo dõi các cấp phát kutoka Large Object Heap.
  • Để nhận được stack trace dễ đọc, cần khởi động một session rundown trước khi bắt đầu session chính.

Code mẫu

Dưới đây là ví dụ mã cho ứng dụng console sẽ lấy ProcessId mà bạn muốn kiểm tra RAM và khởi động một EventPipeSession để ghi lại stack trace khi xảy ra sự kiện GCTriggered.

csharp Copy
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing.Etlx;
using Microsoft.Diagnostics.Tracing.Parsers;

namespace MonitorGC;
class GCTriggeredEventCollector(int processId)
{
    private EventPipeSession? _session;
    private readonly DiagnosticsClient _client = new(processId);

    public async Task StartCollectingAsync(CancellationToken cancellationToken)
    {
        var providers = new List<EventPipeProvider>()
        {
            new(
                ClrTraceEventParser.ProviderName,
                System.Diagnostics.Tracing.EventLevel.Informational,
                (long)(ClrTraceEventParser.Keywords.GC |
                       ClrTraceEventParser.Keywords.Stack))
        };

        var config = TraceLog.EventPipeRundownConfiguration.Enable(_client);

        _session = _client.StartEventPipeSession(providers);
        var source = TraceLog.CreateFromEventPipeSession(_session, config);
        source.Clr.GCTriggered += data =>
        {
            if (data.CallStack() == null) return;
            Console.WriteLine("\nStack Trace:");
            Console.WriteLine($"  {data.CallStack()}");
        };

        await Task.Run(() => { source.Process(); }, cancellationToken);
    }

    public void StopCollecting()
    {
        _session?.Stop();
        _session?.Dispose();
    }
}

class Program
{
    static async Task Main(string[] args)
    {
        if (args.Length != 1 || !int.TryParse(args[0], out int processId))
        {
            Console.WriteLine("Sử dụng: GCSegmentCollector.exe <ProcessId>");
            return;
        }

        var collector = new GCTriggeredEventCollector(processId);
        var cts = new CancellationTokenSource();

        Console.WriteLine($"Bắt đầu thu thập GC Segment cho process {processId}");
        Console.WriteLine("Nhấn Ctrl+C để dừng thu thập");

        Console.CancelKeyPress += (s, e) =>
        {
            e.Cancel = true;
            Console.WriteLine("yêu cầu thoát công việc");
            collector.StopCollecting();
            cts.Cancel();
        };

        try
        {
            await collector.StartCollectingAsync(cts.Token);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("\nViệc thu thập đã bị dừng bởi người dùng");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"\nLỗi: {ex.Message}");
        }
        finally
        {
            collector.StopCollecting();
        }
    }
}

Lưu ý: EventPipeSession được dùng để thu thập các sự kiện từ runtime. Trong khi TraceLog cung cấp công cụ để xử lý và phân tích các sự kiện. TraceLog là cần thiết để có được stack trace dễ đọc.

Nguồn tham khảo

  • Tracing allocations with EventPipe
  • PR hỗ trợ việc sử dụng TraceLog realtime trên một session EventPipe đang hoạt động mà không cần đọc từ file.
    source: viblo
Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào