Dùng EventPipe để dò chỗ nào trong code .NET đang cắn RAM trên production
nếu anh em lười đọc phần dưới: Khi code .NET trên production của anh em ăn quá nhiều ram, hoặc là ram tăng giảm liên tục, thì có một cách rất nhanh rất nhẹ giúp anh em khoanh vùng được code nào ăn RAM mà không làm chậm server: lưu lại stacktrace mỗi lần .NET chạy garbage collection. Khi RAM tăng vọt, anh em có thể check log xem stacktrace nào xuất hiện thường xuyên thì khả năng rất cao code chỗ đấy đang cắn RAM của anh em. Bạn nào đang gấp thì nhảy qua phần Code mẫu cũng được.
Vấn đề cần xử lí là gì
Code trên production gặp các vấn đề này:
- RAM server tự nhiên tăng vọt lên, khi mình cần đi tìm vấn đề thì mình lấy file dump, nhưng lấy file dump tốn thời gian và có khi dump được xuất trễ quá không thấy được gì.
- Các tool tracing trên code đang chạy thì lại làm chậm site nên không thể bật tracing 24/24 được.
tiếp cận ra sao
Mình cần một phương pháp siêu nhẹ có thể chạy liên tục trong production. Ý tưởng then chốt dựa vào cách .NET quản lí memory : garbage collection thường được kích hoạt khi nào trong code mình đi cấp bộ nhớ. Bằng cách lưu lại stack trace vào log mỗi lần .NET đi dọn rác, mình có thể xác định sơ các flow code nào kích hoạt garbage collection. flow nào xuất hiện càng nhiều trong file log thì flow đấy đang cắn RAM của anh em.
Tóm tắt .NET GC
Khác với C, mình đi cấp phát bộ nhớ như là gọi malloc
rồi sau đấy phải tự đi free , thì .NET runtime quản lý bộ nhớ hộ mình luôn thông qua Garbage Collector. Khi anh em tạo object bằng new
, runtime cấp 1 vùng bộ nhớ cho anh em, dùng xong không cần xóa gì, khi nào runtime thấy cần dọn thì nó sẽ tự động đi dọn mấy objects mà không ai dùng nữa , quy trình đi dọn rác đó gọi là garbage collection (GC).
Tóm tắt về Tracing trong hệ sinh thái của .NET
Tracing đơn giản là ghi lại tất cả mọi event mà app của bạn phát ra vào file để phân tích sau hoặc stream ra đâu đấy để sử dụng ngay lập tức, thường người ta dùng tracing để đi profile app của mình xem tại sao chỗ này lại chậm
ETW (Event Tracing for Windows) là hệ thống tracing ngon nhất cho Windows, còn Linux thì có LTTng (Linux Trace Toolkit: next generation) tương đương. Nếu anh em không muốn phụ thuộc vào hệ điều hành nào, và không cần mấy cái event ở tầng OS, thì cứ dùng hệ thống EventPipe để trace, vì nó được tích hợp sẵn trong runtime .NET. Kiến trúc EventPipe cũng tương tự như ETW, như trong hình sau.
Điều này áp dụng như thế nào cho trường hợp của chúng ta?
- Ứng dụng chúng ta muốn kiểm tra RAM sẽ là provider.
- Provider sẽ phát GC events cho consumer.
- Chúng ta sẽ tạo một app console vừa làm controller và consumer (để demo) để:
- Tạo một session EventPipe để đòi GC events từ provider.
- Lấy event nhận được từ session để xử lí (bằng cách ghi vào console trong ví dụ này)
Hai cái hay ở đây là:
- provider chỉ bắn event khi session chạy và đòi events, nên là mình không cần phải chỉnh code production gì cả , và muốn thì mình có thể tắt tracing đi mà không cần restart site.
- GC thường không xảy ra quá nhiều nên không lo site bị làm chậm nếu mình log stacktrace.
Session EventPipe của mình cần lấy event gì?
App của mình cần bắn event sau:
GCTriggered
: được gửi mỗi lần .NET kích hoạt GC, kích hoạt thông qua cấp bộ nhớ hay bị bắt kich hoạt thì cũng sẽ gửi event này.
Lưu ý:
- Mặc dù có 1 cái event
GCAllocationTick_V3
(được gửi mỗi lần runtime cấp hơn 100KB) có thể check được luôn là mình cấp phát bao nhiêu mem cho object nào, mình không sử dụng nó do có thể ảnh hưởng hiệu năng. Nhưng mà nó phù hợp để theo dõi các cấp phát Large Object Heap. - Để thu được stack trace dễ đọc thì phải tạo một session rundown trước khi bắt đầu session chính để code đi resolve symbol đúng cách.
- Controllers không thể yêu cầu là tao cần chính xác event A event B được. Thay vào đó, controllers chỉ có thể yêu cầu 1 combo keywords và log levels. App sẽ bắn events nào có chứa keyword này mà cùng log level. Ví dụ:
- Một session yêu cầu event với keyword GC ở level 4 sẽ nhận được events
GCTriggered
(keyword GC, level 4 - informational) - NHƯNG Nó sẽ không nhận được event
GCAllocationTick_V3
(level 5 - verbose), mặc dù tài liệu ETW nói nó chỉ ở level 4
- Một session yêu cầu event với keyword GC ở level 4 sẽ nhận được events
Code mẫu
Show cho ae code console lấy 1 cái processID làm đầu vào, bắt đầu một EventPipeSession
và ghi lại stack trace cho mỗi event GCTriggered
.
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)
{
// Cấu hình providers cho EventPipe
var providers = new List<EventPipeProvider>()
{
new(
ClrTraceEventParser.ProviderName,
System.Diagnostics.Tracing.EventLevel.Informational,
(long)(ClrTraceEventParser.Keywords.GC |
ClrTraceEventParser.Keywords.Stack)) // Chỉ thu thập sự kiện GC VÀ stack traces
};
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()}");
};
// Bắt đầu xử lý
await Task.Run(() => { source.Process(); }, cancellationToken);
}
public void StopCollecting()
{
_session?.Stop();
_session?.Dispose();
}
}
// Ví dụ sử dụng
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 sử dụng để thu thập event từ runtime, trong khi TraceLog cung cấp cơ sở hạ tầng để xử lý và phân tích các sự kiện này. TraceLog
(phải kèm theo rundown session nữa nha) là thiết yếu để có được stack trace có thể đọc được, vì nó cung cấp API dễ tiếp cận hơn để xử lý sự kiện.
Nguồn
All rights reserved