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

ناقلات السجلات

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

ناقلات السجلات

ناقل السجلات هو العميل البرمجي الذي يجسر الفجوة بين مخرجات السجلات الخام — الملفات على القرص، وإخراج الحاويات عبر stdout، وسجلات systemd، ومقابس syslog — وخلفية التسجيل المركزية. إتقان هذه الطبقة أمر لا يقبل التفاوض: فناقل مُعد تهيئةً خاطئة يسقط الأحداث بصمت تحت الحمل، أو يُدخل ارتفاعات في الكمون بمقدار ثوانٍ متعددة، أو يستهلك من وحدة المعالجة المركزية ما يجعله المستهلك الرئيسي للموارد على عقدة الإنتاج. تقارن هذه الدرس الأدوات الثلاث الأكثر انتشاراً في الفرق الهندسية الحقيقية اليوم: Filebeat وFluent Bit وVector.

آلية متابعة الملفات (Tailing)

تعتمد هذه الأدوات الثلاث جميعها على نفس استدعاء النواة: inotify في Linux وkqueue في macOS/BSD. وتحتفظ كلٌّ منها بـسجل موضع (registry) — ملف حالة صغير يخزّن رقم inode والإزاحة البايتية لكل ملف تتابعه. عند إعادة التشغيل، ينتقل الناقل مباشرةً إلى آخر إزاحة مؤكدة كي لا تُعاد معالجة أي سطر ولا يُفقد أي منه. هذا السجل هو أول ما يجب عليك حمايته: فقدانه — كأن تمحو حاوية مؤقتة المسار /var/lib/filebeat — يضطر الناقل إلى البدء من طرف الملف الحالي، وتضيع الأحداث التي وصلت خلال الفجوة إلى الأبد.

إعادة تدوير Inode عند تدوير السجلات: حين يدوّر logrotate ملفاً دون استخدام copytruncate، يُلغى ربط inode القديم وينشأ inode جديد. إن كان ناقلك يراقب الملف بالاسم فقط، فإنه سيقرأ لحظياً من الملف الجديد الفارغ، مفوِّتاً آخر بايتات الملف القديم. احرص دائماً على ضبط الناقل كي يتابع inode القديم حتى نهايته قبل التحوّل — يسمى هذا الخيار close_inactive في Filebeat، وRotate_Wait في Fluent Bit.

Filebeat

Filebeat هو الناقل الأصيل في منظومة Elastic، مكتوب بلغة Go. تتمحور مهمته الرئيسية حول التسليم الموثوق إلى Elasticsearch أو Logstash. يتألق في البيئات التي تُشغّل ELK أصلاً، حيث تقلّص تلميحات الاكتشاف التلقائي (autodiscover) ومكتبة الوحدات الجاهزة (nginx وPostgreSQL وAWS وغيرها) والدعم المدمج لأنابيب Ingest Node من الكود المكرر. بصمته على وحدة المعالجة المركزية منخفضة، وعلى الذاكرة معتدلة لأنه يُخزّن طابور أحداث داخلياً قبل الإقرار بها.

# filebeat.yml — متابعة سجل JSON للتطبيق، الإثراء، والإرسال إلى Elasticsearch filebeat.inputs: - type: log enabled: true paths: - /var/log/myapp/*.log json.keys_under_root: true json.add_error_key: true fields: env: production service: payment-api fields_under_root: true close_inactive: 5m # تحرير مقبض الملف بعد 5 دقائق هدوء ignore_older: 24h # تخطي الملفات التي لم تُعدَّل منذ 24 ساعة processors: - drop_fields: fields: ["agent.ephemeral_id", "ecs.version"] - rename: fields: - from: "log.level" to: "level" output.elasticsearch: hosts: ["https://es-cluster:9200"] username: "${ES_USER}" password: "${ES_PASS}" index: "myapp-%{[fields.env]}-%{+yyyy.MM.dd}" bulk_max_size: 2048 worker: 4 queue.mem: events: 8192 # عمق الطابور في الذاكرة flush.min_events: 512 flush.timeout: 5s

Fluent Bit

Fluent Bit هو النسخة الخفيفة من Fluentd، مكتوبة بلغة C. بحجم ثنائي لا يتجاوز 650 كيلوبايت واستخدام RSS أقل من 10 ميغابايت في حالة الخمول، فإنه الناقل الافتراضي في DaemonSet لكل توزيعة Kubernetes مُدارة تقريباً (EKS وGKE وAKS جميعها تُشحن به). يعتمد نموذج الأنابيب الصريح والقابل للتكوين: Input → Parser → Filter → Buffer → Output. يمتلك مُدخِل tail محللاً متعدد الأسطر مخصصاً يعالج stack traces بشكل صحيح، وهو أمر بالغ الأهمية لخدمات Java وPython.

# fluent-bit.conf — متابعة سجلات حاويات Kubernetes مع الإثراء [SERVICE] Flush 5 Daemon Off Log_Level warn storage.path /var/log/flb-storage/ storage.sync normal storage.checksum off storage.max_chunks_up 128 [INPUT] Name tail Path /var/log/containers/*.log multiline.parser cri, docker Tag kube.* Refresh_Interval 10 Rotate_Wait 30 Mem_Buf_Limit 64MB Skip_Long_Lines On DB /var/log/flb_kube.db # سجل الإزاحات [FILTER] Name kubernetes Match kube.* Kube_URL https://kubernetes.default.svc:443 Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token Merge_Log On Keep_Log Off K8S-Logging.Parser On K8S-Logging.Exclude On Labels On Annotations Off [FILTER] Name record_modifier Match kube.* Record cluster prod-us-east-1 [OUTPUT] Name loki Match kube.* Host loki.monitoring.svc.cluster.local Port 3100 Labels job=fluentbit,namespace=$kubernetes[\'namespace_name\'],pod=$kubernetes[\'pod_name\'] label_keys $level,$service Batch_Size 1048576 Batch_Wait 1 auto_kubernetes_labels On
Mem_Buf_Limit هو خط الدفاع الأول ضد الضغط العكسي في Fluent Bit. حين تبلغ الأجزاء المخزنة في الذاكرة هذا الحد، يتوقف مُدخِل tail عن القراءة. يحمي ذلك الناقل من الإيقاف بسبب نفاد الذاكرة (OOM) خلال انفجارات السجلات. اقرن هذا الإعداد بالتخزين على نظام الملفات (storage.path) حتى تتسرب الأجزاء التي تتجاوز حد الذاكرة إلى القرص بدلاً من إسقاطها.

Vector

Vector، من إنتاج Datadog (مفتوح المصدر)، هو الأحدث بين الثلاثة والأكثر طموحاً. يُنمذج كل شيء — العملاء والمجمّعات والمحوّلات — كثنائي موحد ذي رسم بياني طوبولوجي. لغة remap الخاصة به (VRL — Vector Remap Language) هي لغة تعبيرية مخصصة وآمنة لتحويل السجلات، أكثر قدرةً بكثير من معالجات Filebeat أو مرشّحات Lua في Fluent Bit. كما ينشر Vector مقاييس مراقبة ذاتية داخلية تتيح لك التنبيه على إنتاجية الناقل ومعدلات الأخطاء.

# vector.yaml — متابعة الملفات والتحليل والإثراء والتوجيه حسب مستوى السجل sources: app_logs: type: file include: - /var/log/myapp/*.log read_from: beginning # استخدم "end" في daemonsets الإنتاجية ignore_older_secs: 86400 fingerprint: strategy: checksum # بصمة inode + checksum لهوية مستقرة multiline: start_pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}' condition_pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}' mode: halt_before timeout_ms: 2000 transforms: parse_json: type: remap inputs: [app_logs] source: | . = parse_json!(string!(.message)) .env = get_env_var!("DEPLOY_ENV") .host = get_hostname!() if exists(.duration_ms) { .duration_ms = to_float!(.duration_ms) } route_by_level: type: route inputs: [parse_json] route: errors: '.level == "error" || .level == "fatal"' debug: '.level == "debug"' sinks: loki_errors: type: loki inputs: [route_by_level.errors] endpoint: http://loki:3100 encoding: codec: json labels: level: "{{ level }}" service: "{{ service }}" buffer: type: disk max_size: 268435456 # مخزن قرص 256 ميغابايت لسجلات الأخطاء loki_default: type: loki inputs: [parse_json, route_by_level.debug] endpoint: http://loki:3100 encoding: codec: json labels: level: "{{ level }}" buffer: type: memory max_events: 10000 when_full: drop_newest # تجاهل سجلات debug بدلاً من نفاد الذاكرة

التحليل والإثراء

سطور السجلات الخام عديمة الجدوى للاستعلام على النطاق الواسع — تحتاج إلى حقول منظمة. أبرز أنماط التحليل:

  • JSON مباشر: إن كانت خدمتك تُصدر JSON أصلاً (وينبغي أن تفعل — راجع الدرس 2)، فالناقل يحتاج فقط إلى تحليل السلسلة الخارجية. استخدم مفاتيح json.* في Filebeat، أو محلل json في Fluent Bit، أو parse_json!() في Vector.
  • Grok/Regex: للسجلات النصية القديمة كسجلات وصول nginx وApache. يدعم Filebeat وFluent Bit كلاهما Grok. Grok بطيء — إن كنت تملك الخدمة، انتقل إلى التسجيل المنظم بدلاً منه.
  • تجميع متعدد الأسطر: تمتد stack traces عبر أسطر عديدة. تدعم الأدوات الثلاث التجميع المعتمد على regex. اضبط المهلة الزمنية بحذر (2–5 ثوانٍ) — القصيرة جداً تقسّم التتبعات، والطويلة جداً تُضيف كموناً لكل حدث.

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

الضغط العكسي والتخزين المؤقت

الضغط العكسي هو ما يمنع ناقلك من أن يصبح تسرباً للذاكرة غير محدود حين تكون الخلفية (Elasticsearch أو Loki أو Kafka) بطيئة أو غير متاحة. تتعامل كل أداة مع ذلك بطريقة مختلفة:

  • Filebeat: يستخدم طابور في الذاكرة (حجم قابل للضبط) مع خيار تخزين على القرص. حين يمتلئ الطابور، يتوقف الحاصد (قارئ الملفات) عن القراءة. مع الطابور الافتراضي في الذاكرة، يضيع تعطّل Filebeat ما فيه من أحداث غير مُقرَّة. للتوظيفات عالية الموثوقية، انتقل إلى طابور القرص (queue.disk في إصدارات Filebeat الحديثة).
  • Fluent Bit: تنجو الأجزاء المدعومة بنظام الملفات (storage.type filesystem) من عمليات إعادة التشغيل. يُطلق حد Mem_Buf_Limit على كل مُدخِل سلوك التوقف عند الامتلاء. تتحكم storage.max_chunks_up في عدد الأجزاء التي يمكن وجودها في الذاكرة في وقت واحد. بدون تخزين على نظام الملفات، يضيع كل ما هو مُخزَّن عند إعادة تشغيل العقدة.
  • Vector: مخازن مؤقتة صريحة لكل حوض — إما memory (سريع، يضيع بالتعطل) أو disk (متين، بتكلفة I/O). تتيح سياسة when_full الاختيار بين block (تطبيق الضغط العكسي مع خطر التباطؤ) وdrop_newest (قبول الخسارة لحماية الإنتاجية). اختر block لسجلات الأخطاء، وdrop_newest للتدفقات debug عالية الحجم.
قاعدة تحجيم الإنتاج: خصص مخزن قرص يعادل تقريباً 30 دقيقة من حجم الاستيعاب عند الذروة لكل عقدة ناقل. إن كانت خدمتك تولّد 50 ميغابايت/دقيقة من السجلات عند الذروة، وفّر ~1.5 غيغابايت من مخزن القرص لكل عقدة. هذا يمنحك 30 دقيقة من العطل في الخلفية قبل أن تبدأ بفقدان البيانات — وقت كافٍ لإخطار مهندس ومعالجة معظم الحوادث.

دليل اختيار الأداة

Log Shipper Selection Guide Tool Memory footprint Transform power Best fit Filebeat ~60–120 MB Moderate (processors) ELK-native; rich modules + disk queue option Ingest pipelines offload work Fluent Bit <10 MB at rest Basic–moderate (Lua/WASM) Kubernetes DaemonSets Smallest binary (~650 KB) Great k8s metadata filter Vector ~30–80 MB High (VRL language) Complex routing / multi-sink Also acts as aggregator Built-in internal metrics Typical flow: Log file → Shipper (tail + parse + enrich) → Buffer → Backend Log File Tail + Parse Enrich Buffer Backend إشارة الضغط العكسي
أنبوب ناقل السجلات: تتدفق الأحداث من اليسار إلى اليمين؛ يتدفق الضغط العكسي من اليمين إلى اليسار حين تكون الخلفية بطيئة.

أكثر أوضاع الإخفاق شيوعاً في الإنتاج

  • تلف سجل الموضع (Registry): إن حُذف ملف السجل أو اقتُطع — كما يحدث مع وحدة تخزين EmptyDir في Kubernetes — يُعيد الناقل الإرسال من بداية الملف. فيضان الأحداث المكررة يجتاح Elasticsearch ويُطلق عواصف من التنبيهات. الحل: استخدم وحدة تخزين hostPath للسجل، لا وحدة تخزين مؤقتة للحاوية.
  • تسرب الحاصدات (Harvester leak): يفتح Filebeat مقبض ملف لكل ملف يحصده. في البيئات ذات آلاف الملفات المُدارة دورياً، يستنزف ذلك حد الملفات المفتوحة للعملية (ulimit -n). اضبط close_inactive بحزم وارفع حد نظام التشغيل في وحدة systemd أو في securityContext الـ DaemonSet.
  • تتالي مهل الإخراج: حين تكون الخلفية بطيئة، يُحجب الإخراج فيمتلئ الطابور الداخلي فيتوقف الحاصد. وخلال ذلك يستمر الملف في الكتابة. إن دارت دورة السجل خلال فترة التوقف، تضيع البيانات غير المقروءة بعد الدورة. استخدم مخزناً مؤقتاً دائماً على القرص كي يتمكن الناقل من التفريغ بشكل غير متزامن.
  • انحراف الساعة والأحداث غير المرتبة: تنحرف ساعات الحاويات وساعات المضيف. أثرِ دائماً بطابع زمني من جانب الخادم في الناقل، واحتفظ بطابع التطبيق كحقل منفصل (app_timestamp). لا تستخدم وقت الاستيعاب طابعاً زمنياً وحيداً أبداً.
أفضل الممارسات التشغيلية: اكشف مقاييس كل ناقل الداخلية (Filebeat: نقطة مراقبة HTTP على المنفذ 5066؛ Fluent Bit: /api/v1/metrics على المنفذ 2020؛ Vector: نقطة Prometheus على المنفذ 9598) وأنشئ تنبيهاً على output_events_dropped_total > 0. الإسقاط الصامت للأحداث هو أصعب إخفاقات التسجيل في الكشف عنه بعد وقوعه.