المستوى: محترف (Professional) — يفترض إن عندك Prometheus شغّال في الإنتاج وتعرف أساسيات الـ labels والـ scraping.
لو Prometheus بتاعك اتقتل فجأة بـ OOMKilled والرام قفزت من 4 جيجا لـ 40 جيجا من غير أي زيادة في الترافيك، المشكلة في 90% من الحالات مش السيرفر ولا الاستعلامات. المشكلة ليبل واحد غير محدود دخّلته على مقياس، وفجّر عدد السلاسل الزمنية. هنا هتعرف تكتشفه بأمر واحد وتقفله قبل ما يوقّع الـ node كله.
الكارديناليتي: العدّاد الخفي اللي بيحدد استهلاك رامك
المشكلة باختصار
Prometheus بيخزّن كل توليفة فريدة من (اسم المقياس + مجموعة الليبلات) كسلسلة زمنية منفصلة ومستقلة في الذاكرة. كل سلسلة نشطة (active series) بتاكل تقريبًا من 1 لـ 3 كيلوبايت في الـ head block. الرقم ده يبان صغير، لكنه بيتضرب في عدد السلاسل. ولو عدد السلاسل بقى ملايين، الحسبة بتتحول من ميجابايت لعشرات الجيجابايت في دقائق.
خليها بمثال قبل التعريف العلمي
تخيل مكتبة عندها فهرس. الفهرس ده كل صف فيه بيمثّل كتاب. لو صنّفت الكتب حسب "النوع" (روايات، علوم، تاريخ) هتطلع عندك 3 صفوف. لو صنّفتها حسب "النوع × دار النشر × سنة الطبع" هتطلع مئات الصفوف. لكن لو قررت تعمل صف في الفهرس لكل "رقم عضوية قارئ استعار الكتاب"، الفهرس هيكبر بعدد القرّاء، وممكن يوصل ملايين الصفوف. الفهرس نفسه بقى أكبر من المكتبة.
ده بالظبط اللي بيحصل في Prometheus. الليبل زي method أو status عنده عدد قيم محدود (bounded)، فالسلاسل قليلة. لكن الليبل زي user_id أو email أو request_id أو الـ URL الكامل بقيمه، عدد قيمه غير محدود (unbounded)، وده اللي بيفجّر العدّاد.
التعريف العلمي الدقيق
عدد السلاسل لمقياس = حاصل ضرب عدد القيم المميزة لكل ليبل فيه. الصيغة: series = ∏ (distinct values per label). الجدول ده بيوضّح الفرق بين مقياس آمن ومقياس قنبلة:
| المقياس | الليبلات وعدد قيمها | عدد السلاسل | الذاكرة التقريبية |
|---|---|---|---|
| آمن (bounded) | method=4 × status=3 × handler=12 | 144 | ≈ 0.5 ميجابايت |
| قنبلة (unbounded) | method=4 × status=3 × user_id=200,000 | 2,400,000 | ≈ 7 جيجابايت (بحساب 3KB/سلسلة) |
لاحظ إن نفس المقياس، بمجرد إضافة ليبل واحد غير محدود، قفز من 144 سلسلة لـ 2.4 مليون. ده اللي بنسمّيه "cardinality explosion" أو "cardinality bomb".
إزاي تكتشفها في دقيقة (مثال تنفيذي)
Prometheus بيوفّر endpoint جاهز بيرتّبلك أكبر مصادر السلاسل. افتحه على المتصفح أو بـ curl:
# أكبر المقاييس من حيث عدد السلاسل
curl -s 'http://localhost:9090/api/v1/status/tsdb' | jq '.data.seriesCountByMetricName[:10]'
# وأكبر الليبلات من حيث عدد القيم المميزة
curl -s 'http://localhost:9090/api/v1/status/tsdb' | jq '.data.labelValueCountByLabelName[:10]'
ولو عايز تقيس إجمالي السلاسل النشطة وتراقب اتجاهها، استخدم استعلامات PromQL دي مباشرة:
# إجمالي السلاسل النشطة في الـ head
prometheus_tsdb_head_series
# أعلى 5 مقاييس مساهمة في العدد
topk(5, count by (__name__)({__name__=~".+"}))
لو لقيت مقياس واحد بياخد مئات الآلاف من السلاسل، أنت لقيت القنبلة.
إزاي تصلّحها: الـ relabeling (الحل الأسرع)
أسرع علاج وأرخصه هو إنك تحذف الليبل غير المحدود قبل ما يتخزّن، عبر metric_relabel_configs. المثال ده بيمسح ليبل user_id ويشيل أي مقياس debug بالكامل:
scrape_configs:
- job_name: 'my-app'
static_configs:
- targets: ['app:8080']
metric_relabel_configs:
# امسح الليبل غير المحدود من كل السلاسل
- source_labels: [user_id]
regex: '.+'
action: labeldrop
# ملاحظة: labeldrop بياخد regex على أسماء الليبلات
- regex: 'user_id'
action: labeldrop
# اسقط مقياس مزعج بالكامل قبل التخزين
- source_labels: [__name__]
regex: 'app_debug_.*'
action: drop
وكـ حاجز أمان (guardrail) على مستوى الـ scrape، فعّل الحدود دي علشان أي target بيتعدّى السقف يتم رفضه بدل ما يفجّر الـ TSDB:
sample_limit: 20000 # أقصى عدد عيّنات لكل scrape
label_limit: 30 # أقصى عدد ليبلات لكل سلسلة (v2.27+)
label_value_length_limit: 200
سيناريو واقعي بأرقام
في حادثة موثّقة (أبريل 2025)، مقياس nginx_ingress_controller_request_duration_seconds_bucket وصل لـ 256,548 سلسلة واستهلك حوالي 770 ميجابايت رام لوحده (256,548 × 3000 بايت). وفي كلاستر Kubernetes بيراقب 1,200 ServiceMonitor بربط 1:1، اتولّدت 50 مليون سلسلة زمنية، اللي محتاجة حوالي 150 جيجابايت رام قبل ما تحسب أصلًا الاستعلامات أو الـ WAL. دي مش أرقام نظرية، دي أسباب مباشرة لـ OOMKilled في الإنتاج.
الـ trade-offs بصراحة
حذف الليبل بيوفّر رام حقيقي، بس بتخسر القدرة على التقطيع (slicing) بالبُعد ده. لو محتاج تحلّل السلوك per-user، الـ label مش المكان الصح للبيانات دي — استخدم logs أو نظام tracing زي حاجة مبنية على OpenTelemetry، أو حوّلها لـ exemplars. القاعدة: الـ metrics للاتجاهات المجمّعة، مش لتتبّع كيان فردي.
بديل تاني: بدل ما تحذف، ممكن تعمل aggregation عبر recording rules تجمّع السلاسل في مقياس أقل كارديناليتي. بتكسب استعلامات أسرع وذاكرة أقل، بتخسر التفاصيل الدقيقة للسلاسل الأصلية.
متى لا تشغّل بالك بالموضوع
لو إجمالي السلاسل النشطة عندك أقل من بضع مئات الآلاف والرام مستقرة، مفيش مشكلة أصلًا، ومتقعدش تعمل optimization مبكّر. الحدود اللي بتقلق منها بتبدأ من ملايين السلاسل على instance واحد. كمان لو الليبل عدد قيمه محدود ومعروف (زي كود دولة أو نوع جهاز)، سيبه زي ما هو — مش كل ليبل نصّي هو قنبلة.
الخطوة التالية
افتح دلوقتي http://localhost:9090/api/v1/status/tsdb على Prometheus بتاعك وبُص على seriesCountByMetricName. لو لقيت مقياس واحد بياخد أكتر من 100 ألف سلسلة، حدّد الليبل المسؤول من labelValueCountByLabelName، وضيف له labeldrop في metric_relabel_configs. أعد التشغيل وراقب prometheus_tsdb_head_series — المفروض ينزل فورًا.
المصادر
- Prometheus Docs — Metric and label naming (تحذير رسمي من الليبلات غير المحدودة)
- Prometheus Docs — TSDB Stats endpoint (/api/v1/status/tsdb)
- Prometheus Docs — relabel_config و metric_relabel_configs
- High Cardinality Labels Are Killing Your Prometheus — Kaidalov (2025) لأرقام الحوادث
- How Cloudflare runs Prometheus at scale — Cloudflare Blog