Levelwise
فارسی
Observability

متریک و هشدار (Metrics و Alert)

متریک یک عدد است که در طول زمان اندازه می‌گیریم، مثل تعداد سفارش در دقیقه یا زمان پاسخ. ابزار Prometheus این عددها را جمع می‌کند و Grafana آن‌ها را نمایش می‌دهد. هشدار خوب روی درد کاربر تنظیم می‌شود، کمی صبر می‌کند و فقط وقتی کسی را خبر می‌کند که واقعاً کاری باید انجام شود.

بازبینی نشدهبا کمک AI نوشته شدهزمان خواندن: ۱۵ دقیقهمثال پرداخت در فروشگاه اینترنتیکد C# و OpenTelemetry و Prometheus

نویسنده: bezzad

مشکل: اول مشتری خبردار می‌شود، بعد ما

جمعه شب است و فروشگاه شلوغ است. بانک کند شده و نیمی از پرداخت‌ها خطا می‌دهند. تیم ما یک ساعت بعد خبردار می‌شود، آن هم از پیام‌های عصبانی مشتری‌ها.

لاگ و Trace داشتیم. پس مشکل کجا بود؟

  1. لاگ و Trace برای جزئیات یک اتفاق هستند. برای جواب دادن به «الان حال کل سیستم چطور است؟» باید میلیون‌ها خط را بشماریم. این کار کند و گران است.
  2. هیچ کس به داشبورد نگاه نمی‌کرد. جمعه شب کسی پشت سیستم نیست.

پس دو چیز لازم داریم: عددهای خلاصه درباره حال سیستم، و هشداری که خودش ما را خبر کند.

متریک چیست؟

متریک یک عدد است که در طول زمان اندازه می‌گیریم. مثلاً تعداد سفارش در دقیقه، درصد خطای پرداخت یا زمان پاسخ API. هر متریک می‌تواند چند برچسب (Label) هم داشته باشد، مثل روش پرداخت یا کد وضعیت HTTP.

لاگ Trace متریک
به چه سؤالی جواب می‌دهد؟ دقیقاً چه اتفاقی افتاد؟ وقت کجا صرف شد؟ حال کل سیستم چطور است؟
واحد یک رویداد یک درخواست یک عدد خلاصه در هر بازه
هزینه با افزایش ترافیک زیاد می‌شود زیاد می‌شود، مگر با نمونه‌برداری تقریباً ثابت می‌ماند
مناسب برای هشدار کم کم خیلی خوب

هزینه متریک ثابت می‌ماند، چون برنامه هر درخواست را جدا ذخیره نمی‌کند. فقط یک شمارنده را یکی زیاد می‌کند. برای همین متریک بهترین پایه برای داشبورد و هشدار است.

سه نوع اصلی متریک

Counterشمارنده: فقط بالا می‌رودتعداد سفارش‌ها، تعداد خطاهاGaugeگیج: بالا و پایین می‌رودطول صف، حافظه مصرفیHistogramهیستوگرام: توزیع مقدارهازمان پاسخ، اندازه سبد خریدهر متریک یک عدد در طول زمان است، با چند برچسب
  1. شمارنده (Counter). فقط بالا می‌رود. مثل تعداد کل سفارش‌ها. خود عدد مهم نیست. سرعت تغییرش مهم است، مثلاً سفارش در ثانیه.
  2. گیج (Gauge). یک مقدار لحظه‌ای که بالا و پایین می‌رود. مثل تعداد پیام‌های داخل صف یا حافظه مصرفی.
  3. هیستوگرام (Histogram). مقدارها را در چند سطل (Bucket) می‌شمارد. مثلاً چند درخواست زیر ۱۰۰ میلی‌ثانیه، چند تا زیر ۵۰۰ میلی‌ثانیه و همین‌طور تا آخر. با هیستوگرام می‌شود صدک‌ها را حساب کرد.

میانگین دروغ می‌گوید

فرض کن از صد درخواست پرداخت، ۹۵ تا سریع هستند و ۵ تا چند ثانیه طول می‌کشند. میانگین زمان پاسخ خوب به نظر می‌رسد. ولی پنج مشتری از هر صد نفر، چند ثانیه منتظر مانده‌اند.

زمان پاسخ صد درخواست پرداختمیانگین: خوب به نظر می‌رسدصدک ۹۹: چند مشتری چند ثانیه منتظرندنود و پنج درخواست سریعپنج درخواست کندمیانگین، درد این پنج مشتری را پنهان می‌کند
برای زمان پاسخ، صدک‌ها را ببین، نه میانگین را.

صدک ۹۹ (p99) یعنی ۹۹ درصد درخواست‌ها از این عدد سریع‌تر بودند. برای زمان پاسخ معمولاً صدک ۵۰، ۹۵ و ۹۹ را نگاه می‌کنیم. در سیستم پرترافیک، یک درصد یعنی هزاران مشتری در روز.

کد: متریک در .NET

خود ASP.NET Core و HttpClient از .NET 8 به بعد متریک‌های آماده دارند. مثلاً متریک http.server.request.duration یک هیستوگرام از زمان پاسخ همه درخواست‌ها است. فقط باید آن‌ها را جمع کنیم و بیرون بدهیم.

برای عددهای کسب‌وکار، متریک خودمان را با کلاس Meter می‌سازیم. رابط IMeterFactory آن را از DI می‌دهد:

public sealed class ShopMetrics
{
    public const string MeterName = "Shop.Orders";

    private readonly Counter<long> _ordersPlaced;
    private readonly Histogram<double> _paymentDuration;

    public ShopMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create(MeterName);
        _ordersPlaced = meter.CreateCounter<long>("shop.orders.placed");
        _paymentDuration = meter.CreateHistogram<double>("shop.payment.duration", unit: "s");
    }

    public void OrderPlaced(string paymentMethod) =>
        _ordersPlaced.Add(1, new KeyValuePair<string, object?>("payment.method", paymentMethod));

    public void PaymentFinished(TimeSpan elapsed) =>
        _paymentDuration.Record(elapsed.TotalSeconds);
}

حالا با OpenTelemetry همه متریک‌ها را در آدرس metrics برای Prometheus باز می‌کنیم:

builder.Services.AddSingleton<ShopMetrics>();

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("orders-api"))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddMeter(ShopMetrics.MeterName)
        .AddPrometheusExporter());

var app = builder.Build();
app.MapPrometheusScrapingEndpoint();   // exposes /metrics
نسخه پیش‌انتشار: بسته OpenTelemetry.Exporter.Prometheus.AspNetCore هنوز نسخه پایدار ندارد و به شکل beta منتشر می‌شود. راه دیگر این است که متریک‌ها را با OTLP به OpenTelemetry Collector بفرستی و Collector آن‌ها را به Prometheus بدهد.

جریان داده: Prometheus و Grafana

سرویس سفارش/metricsسه نسخهPrometheusذخیره عدد در طول زمانبررسی قانون‌های هشدارGrafanaداشبورد و نموداربرای دیدن روندAlertmanagerگروه‌بندی و ارسال هشدارآدم کشیکپیامک، تماس، چت تیمخواندنمثلاً هر ۱۵ ثانیهپرسیدنPromQL
برنامه فقط عددها را آماده نگه می‌دارد. Prometheus خودش سراغ آن‌ها می‌آید.
  1. برنامه عددهای فعلی را در آدرس metrics نشان می‌دهد.
  2. ابزار Prometheus در فاصله‌های ثابت این آدرس را از همه نسخه‌ها می‌خواند (Scrape) و عددها را با زمانشان ذخیره می‌کند.
  3. ابزار Grafana با زبان PromQL از Prometheus سؤال می‌پرسد و نمودار می‌کشد.
  4. ابزار Prometheus قانون‌های هشدار را مدام بررسی می‌کند. اگر قانونی برقرار شد، آن را به Alertmanager می‌دهد. Alertmanager هشدارهای مشابه را گروه می‌کند و به آدم درست می‌فرستد.

دو سؤال رایج با PromQL. اسم متریک‌ها در Prometheus کمی عوض می‌شود: نقطه به خط زیر تبدیل می‌شود و واحد به آخر اسم اضافه می‌شود.

# Requests per second that ended with a 5xx status, over the last 5 minutes
sum(rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m]))

# p99 response time of the checkout endpoint
histogram_quantile(0.99,
  sum by (le) (rate(http_server_request_duration_seconds_bucket{http_route="/checkout"}[5m])))

چه چیزی را اندازه بگیریم؟

برای هر سرویس، سه عدد اصلی را بگیر. به این روش RED می‌گویند:

  1. تعداد درخواست (Rate). چند درخواست در ثانیه می‌آید؟
  2. خطا (Errors). چند درصد آن‌ها خطا می‌دهند؟
  3. زمان (Duration). صدک‌های زمان پاسخ چقدر است؟

برای منابع، مثل CPU، حافظه، صف و Connection Pool، میزان پر بودن (Saturation) هم مهم است. مثلاً اگر Connection Pool دیتابیس همیشه پر است، به زودی درخواست‌ها منتظر می‌مانند.

کنار این‌ها، عددهای کسب‌وکار را فراموش نکن: تعداد سفارش در دقیقه و درصد پرداخت موفق. گاهی همه عددهای فنی سالم هستند، ولی سفارشی ثبت نمی‌شود.

مراقب برچسب‌ها باش. هر ترکیب جدید از مقدار برچسب‌ها یک سری زمانی جدید در Prometheus می‌سازد. اگر شناسه کاربر یا شماره سفارش را برچسب کنی، میلیون‌ها سری ساخته می‌شود و Prometheus کند می‌شود یا از کار می‌افتد. برچسب باید چند مقدار محدود داشته باشد، مثل روش پرداخت. شناسه‌ها جایشان در لاگ و Trace است.

هشدار خوب

هشدار یعنی بیدار کردن یک آدم. پس هر هشدار باید ارزش آن را داشته باشد:

  1. روی درد کاربر، نه روی علت. «درصد خطای پرداخت بالا است» درد کاربر است. «CPU هشتاد درصد است» شاید هیچ دردی نداشته باشد.
  2. کمی صبر کن. یک جهش چند ثانیه‌ای ارزش بیدار کردن کسی را ندارد. در Prometheus با بخش for می‌گوییم شرط باید مدتی پشت سر هم برقرار بماند.
  3. قابل اقدام باشد. کسی که هشدار را می‌گیرد، باید بداند چه کند. یک لینک به راهنمای رفع مشکل (Runbook) کنار هشدار بگذار.
  4. سطح داشته باشد. فقط مشکل فوری باید آدم را بیدار کند. بقیه می‌توانند یک تیکت یا پیام در چت تیم باشند.

ببین بخش for در عمل چه می‌کند:

مثال زنده: هشدار درصد خطای پرداخت

قانون: اگر درصد خطا پنج دقیقه پشت سر هم بیشتر از ۵٪ بود، به آدم کشیک خبر بده. هر قدم نمودار یک دقیقه است.

5%20%0%

    همان قانون در Prometheus این شکل را دارد:

    groups:
      - name: payments
        rules:
          - alert: PaymentErrorRateHigh
            expr: |
              sum(rate(http_server_request_duration_seconds_count{http_route="/pay", http_response_status_code=~"5.."}[5m]))
                /
              sum(rate(http_server_request_duration_seconds_count{http_route="/pay"}[5m]))
                > 0.05
            for: 5m
            labels:
              severity: page
            annotations:
              summary: "More than 5% of payments fail"
              runbook_url: "https://wiki.example.com/runbooks/payment-errors"
    هدف سطح سرویس (SLO): تیم‌های بالغ‌تر اول یک هدف می‌نویسند. مثلاً «۹۹٫۵ درصد پرداخت‌ها در یک ماه موفق باشند.» بعد هشدار را روی این تنظیم می‌کنند که آیا با این سرعت خطا، هدف ماه از دست می‌رود یا نه. این روش هشدارهای بی‌دلیل را خیلی کم می‌کند.

    اشتباه‌های رایج

    اشتباه نتیجه راه درست
    نگاه کردن به میانگین زمان پاسخ کندی بخشی از مشتری‌ها پنهان می‌ماند. صدک ۹۵ و ۹۹ با هیستوگرام.
    شناسه کاربر یا سفارش به عنوان برچسب تعداد سری‌ها منفجر می‌شود و Prometheus از کار می‌افتد. برچسب با مقدارهای محدود.
    هشدار روی CPU و حافظه هشدارهای زیاد بدون درد واقعی. هشدار روی خطا و کندی که کاربر حس می‌کند.
    هشدار بدون مدت انتظار هر جهش کوچک یک نفر را بیدار می‌کند. بخش for با چند دقیقه.
    هشدارهای زیاد که کسی کاری با آن‌ها نمی‌کند تیم به هشدارها عادت می‌کند و هشدار واقعی را هم نادیده می‌گیرد. هر هشدار بی‌اقدام را حذف یا اصلاح کن.
    فقط متریک فنی سیستم سالم به نظر می‌رسد، ولی سفارشی ثبت نمی‌شود. متریک کسب‌وکار هم داشته باش.

    خلاصه در شش خط

    1. متریک یک عدد در طول زمان است و هزینه‌اش با ترافیک زیاد نمی‌شود.
    2. سه نوع اصلی داریم: شمارنده، گیج و هیستوگرام.
    3. برای زمان پاسخ صدک‌ها را ببین، نه میانگین را.
    4. برای هر سرویس تعداد درخواست، درصد خطا و زمان پاسخ را بگیر، کنار عددهای کسب‌وکار.
    5. برچسب‌ها مقدارهای محدود داشته باشند. شناسه‌ها جایشان در لاگ و Trace است.
    6. هشدار روی درد کاربر باشد، کمی صبر کند و همیشه یک اقدام روشن داشته باشد.