السجلات على نطاق واسع: ELK وLoki

Grafana Loki

18 دقيقة الدرس 6 من 28

Grafana Loki

يقوم Elasticsearch بفهرسة كل رمز في كل سطر سجل. هذا النهج القوي لكنه مكلف: بيئة سجلات بحجم 1 تيرابايت يومياً تحتاج عادةً إلى 3-5 أضعاف ذلك في تخزين Elasticsearch (الفهارس والنسخ الاحتياطية وعمليات دمج الأجزاء) وكلستر يكلف آلاف الدولارات شهرياً. صُمم Grafana Loki من قِبل فريق Grafana Labs للإجابة على سؤال مختلف: ماذا لو خزّنّا السجلات بنفس الطريقة التي يخزّن بها Prometheus المقاييس؟ نفهرس التصنيفات فقط، نضغط النص الخام، ونستعلم بشكل كسول عند وقت القراءة. النتيجة تكلفة أقل بكثير مع مقايضة في سرعة الاستعلام الخام التي نادراً ما تكون عنق الزجاجة في التحقيق الفعلي من الحوادث.

يأخذك هذا الدرس في عمق معمارية Loki ولغة استعلامه LogQL والمواقف التشغيلية التي يتفوق فيها Loki، حتى تتمكن من اختيار الأداة المناسبة وتشغيلها بشكل صحيح في الإنتاج.

الفهرسة القائمة على التصنيفات مقابل الفهرسة النصية الكاملة

في Elasticsearch، يُحلَّل كل سطر سجل عند الاستيعاب: كل كلمة وكل رقم وكل حقل يصبح إدخالاً في فهرس مقلوب. هذا يجعل البحث النصي الكامل العشوائي فورياً، لكنه يعني أيضاً أن الفهرس يمكن أن يكون أكبر من البيانات الخام بسهولة. يحترق المعالج عند الاستيعاب لتحليل الرموز ودمج الأجزاء؛ وتحترق الذاكرة عند الاستعلام لتحميل قوائم النشر.

يتبع Loki النهج المعاكس. عند الاستيعاب، يستخرج فقط التصنيفات التي تعلنها — أزواج مفتاح-قيمة ثابتة يرفقها ناقل السجلات، مثل app="api-gateway" وenv="production" وnamespace="payments". هذه التصنيفات تعرّف تدفقاً. داخل كل تدفق، تُحزَم سطور السجلات في أجزاء مضغوطة (Snappy أو LZ4 افتراضياً) وتُكتب في التخزين الكائني (S3, GCS, Azure Blob). يعيش فهرس التصنيفات في مخزن صغير — كان Cassandra أو BoltDB في البداية، والآن فهرس TSDB المدمج الذي جاء في Loki 2.8. عند الاستعلام، يحدد Loki أولاً التدفقات المطابقة عبر التصنيفات، ثم يفك ضغط الأجزاء ذات الصلة فقط ويطبق أي تعبيرات تصفية على النص الخام في الذاكرة. فكر في الأمر على أنه grep على سجلات مضغوطة مع بوابة تصنيفية.

النموذج الذهني الأساسي: تصنيفات Loki مخصصة لاختيار أي التدفقات يجب قراءتها؛ تعبيرات التصفية في LogQL (|= و|~ وخطوط أنابيب المحلل) مخصصة لإيجاد ماذا داخل تلك التدفقات. إذا لامس استعلامك عدداً كبيراً جداً من التدفقات، فسيكون بطيئاً. وإذا كان لدى تدفق ما عدد كبير جداً من السطور في الثانية، تكبر الأجزاء وتصبح عملية فك الضغط هي عنق الزجاجة — كلاهما مشكلة في تصميم التصنيفات.

معمارية Loki

يعمل نشر Loki الإنتاجي في وضع الخدمات المصغرة مع مكونات قابلة للتوسع بشكل مستقل:

  • Distributor — يستقبل طلبات الدفع من Promtail/Alloy/Fluent Bit عبر نقطة نهاية HTTP /loki/api/v1/push. يتحقق من صحة التصنيفات ويفرض حدود المعدل ويوزع على Ingesters عبر حلقة التجزئة المتسقة.
  • Ingester — يحتفظ بسجل الكتابة المسبقة (WAL) في الذاكرة والأجزاء المفتوحة. يُفرغ الأجزاء المختومة إلى التخزين الكائني ويكتب الفهرس إلى مخزن TSDB. وسّعه أفقياً؛ استخدم WAL على SSD محلي للمتانة.
  • Querier — يتعامل مع طلبات /loki/api/v1/query_range. يجلب البيانات من Ingesters في الذاكرة (البيانات الحديثة) ومن التخزين الكائني (البيانات القديمة)، يدمج النتائج ويطبق خطوط أنابيب LogQL.
  • Query Frontend — يشظّي الاستعلامات ذات النطاق الزمني الطويل، يخزّن النتائج مؤقتاً، ويضع طلبات في قوائم الانتظار للعدالة. انشر هذا دائماً أمام Queriers على نطاق واسع.
  • Ruler — يقيّم قواعد LogQL من نوع المقياس (التنبيه والتسجيل) وفق جدول زمني، مطابق في المفهوم لقواعد Prometheus.
  • Compactor — يدمج بشكل دوري أجزاء فهرس TSDB الصغيرة ويفرض الاحتفاظ عبر علامات الحذف في التخزين الكائني.
Grafana Loki architecture: write and read paths WRITE PATH READ PATH Promtail / Alloy / Fluent Bit Distributor (rate limits) Ingester (WAL + chunks) Object Storage (S3 / GCS) TSDB Index (labels only) Query Frontend (shard + cache) Querier (merge results) Grafana (LogQL panels) Ruler (alerting rules) recent chunks older chunks Compactor (retention / merge)
مسار الكتابة (يسار) ومسار القراءة (يمين) في Loki: تصنيفات الفهرس في TSDB، الأجزاء الخام في التخزين الكائني.

LogQL — لغة استعلام Loki

صُممت LogQL عن قصد على غرار PromQL. يبدأ كل استعلام بـمحدد تدفق السجل — مجموعة من مطابقات التصنيفات في أقواس متعرجة — يتبعها مراحل خط الأنابيب الاختيارية التي تصفي وتحوّل سطور السجل.

# --- محدد تدفق السجل + التصفية --- # عرض آخر 5 دقائق من الأخطاء من خدمة المدفوعات {namespace="payments", app="checkout"} |= "error" | logfmt | level="error" # --- تصفية بالتعبير النمطي --- {app="nginx"} |~ "5[0-9]{2}" | json | status >= 500 # --- خط أنابيب المحلل: تحليل JSON ثم التصفية على حقل --- {app="api-gateway", env="production"} | json | duration_ms > 1000 | line_format "{{.method}} {{.path}} {{.status}} {{.duration_ms}}ms" # --- استعلام مقياس: معدل الطلبات حسب رمز الحالة --- sum by (status) ( rate({app="api-gateway", env="production"} | json | unwrap duration_ms [5m]) ) # --- عد الأخطاء في الدقيقة لقاعدة تنبيه --- sum(rate({namespace="payments"} |= "Exception" [1m])) > 10

المراحل الرئيسية في خط الأنابيب هي:

  • | json / | logfmt / | pattern / | regexp — تحليل السطر الخام إلى حقول
  • |= "string" / != "string" / |~ "regex" / !~ "regex" — مرشحات السطر (سريعة؛ تُطبق قبل فك الضغط في إصدارات Loki الأحدث)
  • | field_name = "value" — مرشحات التصنيف على الحقول المستخرجة
  • | line_format "template" — إعادة تشكيل سطر الإخراج باستخدام صياغة قالب Go
  • | unwrap field_name — استخراج حقل رقمي لتجميع المقاييس (rate, avg_over_time, quantile_over_time)
ضع دائماً التصنيف الأكثر انتقائية أولاً. يقيّم Loki محددات التدفق قبل التحليل، لذا فإن {app="checkout", env="production"} يمسح أجزاء أقل بكثير مقارنة بـ{env="production"} وحده. الاستعلام الذي يلمس أكثر من ~200 تدفق سيكون بطيئاً بغض النظر عن كفاءة خط أنابيب التصفية.

تصميم التصنيفات: القرار التشغيلي الأهم

تتصرف التصنيفات في Loki بشكل مطابق للتصنيفات في Prometheus: كل مجموعة فريدة من قيم التصنيفات تنشئ تدفقاً جديداً. الكثير من التدفقات — الأساسية العالية — يدمر ميزة التكلفة في Loki لأن فهرس TSDB ينفجر وتصبح ملفات الأجزاء صغيرة جداً (نسبة ضغط منخفضة). الأنماط المضادة الكلاسيكية هي:

  • استخدام user_id أو request_id أو trace_id كتصنيف. هذه تنتمي داخل سطر السجل، وتُستخرج عند وقت الاستعلام بخط أنابيب المحلل.
  • استخدام اسم الـ pod أو تجزئة النشر كتصنيف في Kubernetes — استخدم pod باعتدال وفضّل app / component.
  • ترميز مستوى الخطورة كتصنيف (مثل level="error") عندما يكون الحقل موجوداً بالفعل داخل حمولة JSON — حلّله باستخدام | json | level="error" عند وقت الاستعلام بدلاً من ذلك.

مجموعة آمنة للبداية في بيئة Kubernetes هي: cluster وnamespace وapp وenv. كل شيء آخر — اسم المضيف واسم الـ pod ومستوى السجل ومعرف التتبع — يعيش داخل سطر السجل ويُستخرج عبر خطوط أنابيب المحلل.

نشر Loki مع Promtail على Kubernetes

# تثبيت حزمة Loki عبر Helm (وضع ثنائي واحد للتطوير؛ استخدم الخدمات المصغرة للإنتاج) helm repo add grafana https://grafana.github.io/helm-charts helm repo update helm upgrade --install loki grafana/loki \ --namespace monitoring --create-namespace \ --set loki.commonConfig.replication_factor=1 \ --set loki.storage.type=s3 \ --set loki.storage.s3.bucketnames=my-loki-chunks \ --set loki.storage.s3.region=us-east-1 \ --set singleBinary.replicas=1 # تثبيت Promtail كـ DaemonSet — يرسل سجلات العقد والـ pods إلى Loki helm upgrade --install promtail grafana/promtail \ --namespace monitoring \ --set config.lokiAddress=http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push
# مقتطف loki-values.yaml للإنتاج (وضع الخدمات المصغرة) loki: auth_enabled: true # تعدد المستأجرين عبر رأس X-Scope-OrgID limits_config: ingestion_rate_mb: 32 # حد الكتابة MB/s لكل مستأجر ingestion_burst_size_mb: 64 max_streams_per_user: 10000 # حارس الأساسية العالية max_query_parallelism: 32 retention_period: 30d # يفرضه Compactor storage_config: tsdb_shipper: active_index_directory: /loki/tsdb-index cache_location: /loki/tsdb-cache aws: s3: s3://us-east-1/my-loki-chunks s3forcepathstyle: false compactor: working_directory: /loki/compactor retention_enabled: true delete_request_store: s3

متى يفوز Loki (ومتى لا يفوز)

يفوز Loki عندما:

  • تشغّل بالفعل Grafana وPrometheus وتريد حزمة مراقبة موحدة دون فريق Elasticsearch منفصل.
  • حجم السجلات مرتفع (مئات الجيجابايت يومياً) والتكلفة قيد — تخزين S3 أرخص بـ10-30 مرة من أقراص Elasticsearch EBS لكل جيجابايت.
  • تتبع الاستعلامات نمطاً معروفاً: "أرني سجلات من هذه الخدمة في هذا النطاق الزمني خلال نافذة الحادثة." الاسترجاع القائم على التصنيفات سريع لهذا العبء.
  • تحتاج إلى ربط السجلات بالمقاييس والتتبعات في لوحة Grafana واحدة — تربط طريقة عرض Loki's Explore مباشرة بتتبعات Tempo عبر حقول traceID.

يواجه Loki صعوبات عندما:

  • تحتاج إلى بحث نصي كامل عشوائي شبه فوري عبر جميع السجلات في آن واحد (أدوات الامتثال، التحقيق الجنائي في الأنماط غير المعروفة). Elasticsearch أسرع هنا.
  • سير عمل فريقك مبني حول واجهة مستخدم Kibana — LogQL لها منحنى تعلم وـ Grafana Explore ليست بديلاً مباشراً.
  • لديك سجلات غير منظمة وبتنسيقات غير متسقة من أنظمة قديمة حيث التحليل غير موثوق — الفهرس النصي الكامل لـ Elasticsearch أكثر تسامحاً.
مخاطرة إنتاجية — عدم تطبيق الاحتفاظ: لا يحذف Loki الأجزاء القديمة تلقائياً إلا إذا تم تعيين retention_enabled: true في إعدادات Compactor وكان مكون Compactor يعمل فعلاً. يكتشف كثير من الفرق بعد 90 يوماً أن حاوية S3 الخاصة بهم قد نمت دون حدود. تحقق دائماً باستخدام aws s3 ls s3://my-loki-chunks --recursive --summarize | tail -2 بعد أسبوع من التشغيل.

التنبيه مع Loki Ruler

يقيّم Loki Ruler تعبيرات المقاييس في LogQL وفق جدول زمني ويطلق تنبيهات متوافقة مع Prometheus. عرّف القواعد بنفس تنسيق YAML كـ Prometheus:

# loki-rules.yaml — تنبيه عندما يتجاوز معدل الأخطاء الحد groups: - name: application_errors rules: - alert: HighErrorRate expr: | sum by (namespace, app) ( rate({env="production"} | json | level="error" [5m]) ) > 1 for: 2m labels: severity: warning team: platform annotations: summary: "High error rate in {{ $labels.namespace }}/{{ $labels.app }}" description: "Error rate is {{ $value | humanize }} errors/s over the last 5 minutes." - alert: PaymentServiceDown expr: | absent(rate({app="checkout", env="production"}[1m])) > 0 for: 1m labels: severity: critical annotations: summary: "No logs received from checkout service"

تُحمَّل القواعد عبر Ruler API أو تُخزَّن في التخزين الكائني (بادئة S3). تمر التنبيهات عبر Alertmanager بشكل مطابق لـ Prometheus. يتيح لك هذا الحفاظ على شجرة توجيه تنبيهات واحدة لكل من التنبيهات القائمة على المقاييس والسجلات.