التتبع الموزع وOpenTelemetry

لماذا التتبع الموزع؟

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

لماذا التتبع الموزع؟

لديك مقاييس Prometheus. لديك سجلات منظمة في Loki أو Elasticsearch. لوحات Grafana خضراء، وأهداف مستوى الخدمة ضمن الميزانية، ومع ذلك — يقضي مهندسوك الكبار ساعات في جلسات ما بعد الحوادث يتتبعون ارتفاعاً في زمن الاستجابة بمقدار 400ms أثر على 0.3% من الطلبات الثلاثاء الماضي بعد الظهر. المقاييس أظهرت اضطراباً طفيفاً. السجلات أظهرت بعض استعلامات قاعدة البيانات البطيئة. لكن لم يستطع أحد الإجابة على السؤال البسيط: أي خدمة تسببت فعلاً في التباطؤ، ولماذا أصابت تلك الطلبات تحديداً؟

هذه هي المشكلة التي بُني التتبع الموزع لحلها. إنه ليس بديلاً عن المقاييس أو السجلات — بل هو الركيزة الثالثة التي تتيح لك طرح فئة مختلفة جوهرياً من الأسئلة: ماذا حدث لهذا الطلب المحدد أثناء رحلته عبر نظامك؟

مشكلة إسناد زمن الاستجابة

في بنية الخدمات المصغرة، طلب واحد موجه للمستخدم يتشعب إلى عشرات الاستدعاءات للخدمات الخارجية. طلب الدفع قد يمر بـ API gateway، وخدمة مصادقة، وخدمة سلة التسوق، وخدمة التسعير، وخدمة الدفع، وخدمة كشف الاحتيال، وخدمة الطلبات — كل منها يستدعي قواعد بياناته الخاصة أو الكاش أو واجهات API خارجية. إجمالي زمن الاستجابة الذي يختبره المستخدم هو مجموع كل هذه الوقفات، بالإضافة إلى وقت الشبكة بينها.

المقاييس تعطيك إجماليات: "زمن p99 لخدمة الدفع هو 380ms." لكن أي 380ms؟ هل هي 200ms في خدمة الدفع، و100ms في فحص الاحتيال، و80ms في كل شيء آخر؟ أم 350ms في خدمة السلة عند إخفاق الكاش؟ هذان مشكلتان مختلفتان تماماً بحلول مختلفة تماماً، والمقياس الإجمالي لا يخبرك بأيهما تواجه.

السجلات تخبرك بما حدث داخل كل خدمة، لكن ربط قصة طلب واحد عبر عشر خدمات من عشرة تدفقات سجلات منفصلة — كل منها بانحراف طابع زمني خاص، وتنسيق سجل خاص، ومعدل أخذ عينات خاص — أمر مؤلم فعلاً على نطاق واسع. يتطلب من إنسان ربط الإدخالات يدوياً بمعرف الطلب، غالباً عبر شاشات واجهة مستخدم مختلفة أو خطوط أوامر grep.

الفكرة الأساسية: التتبع الموزع يرفق معرف تتبع واحد بكل طلب حين يدخل نظامك. كل خدمة تعالج ذلك الطلب تسجل عملها الخاص كـ امتداد (span) — وحدة عمل مُوقَّتة وموسومة بنفس معرف التتبع. يجمع الـ backend كل الامتدادات بنفس المعرف في مخطط شلال يُظهر الجدول الزمني والمدة الدقيقة لكل عملية، عبر كل خدمة، لذلك الطلب الواحد. تنتقل من "الدفع بطيء" إلى "استدعاء خدمة الاحتيال لـ API خارجية يضيف 340ms فقط للطلبات بقيمة سلة تتجاوز 500 دولار" في دقائق.

التتبعات مقابل المقاييس مقابل السجلات

معرفة متى تستخدم كل نوع من أنواع الإشارات هي مهارة أساسية لمهندس SRE. إنها ليست قابلة للتبادل — لكل منها نقطة قوة مميزة وملف تكلفة مميز.

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

السجلات هي سجلات أحداث غير قابلة للتغيير تُصدر في نقاط اعتباطية في الكود. تحمل سياقاً غير محدود — أي زوج مفتاح/قيمة تريد إضافته. السجلات المنظمة الحديثة (سطور JSON، logfmt) جعلت السجلات قابلة للاستعلام، وأدوات مثل Loki أو Datadog Logs تتيح الفلترة والتجميع عبر حقول ذات قيم كاردينالية عالية. ضعفها هو التكلفة والترابط: عند معدلات طلبات عالية، تخزين وفهرسة كل سطر سجل مكلف، وربط السجلات من خدمات متعددة لطلب واحد يتطلب معرفاً مشتركاً صريحاً.

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

Traces vs Metrics vs Logs — signal types compared METRICS http_requests_total{status="200"} checkout_latency_p99 = 380ms error_rate = 0.04% نقاط القوة رخيصة، سريعة، ممتازة للتنبيه على الأحوال المعروفة الضعف كاردينالية منخفضة — لا تجيب على "أي مستخدم؟" تجيب: هل ثمة خطأ؟ وما مدى انتشاره؟ LOGS {"level":"error","svc":"cart" "trace_id":"abc123" "msg":"db timeout 320ms"} نقاط القوة سياق غير محدود، قابلة للاستعلام تفاصيل غنية لكل حدث الضعف مكلفة على نطاق واسع؛ الترابط بين الخدمات يدوي تجيب: ماذا حدث داخل خدمة واحدة؟ TRACES api-gateway 0–12ms cart-service 12–48ms db-query 22–46ms fraud-check 48–388ms ! order-svc 388–400ms نقاط القوة خريطة زمن الاستجابة الكاملة، رسم بياني سببي للطلب الضعف تكلفة التخزين على نطاق واسع؛ تتطلب استراتيجية أخذ عينات تجيب: من أين جاء زمن الاستجابة لهذا الطلب؟
ركائز قابلية الملاحظة الثلاث: المقاييس تكشف الاتساع، السجلات تشرح العمق، التتبعات تكشف زمن الاستجابة السببي عبر الخدمات.

تشريح التتبع: مخطط الشلال

يُصوَّر التتبع كـ مخطط شلال (يُسمى أيضاً مخطط اللهب أو مخطط غانت). المحور الأفقي هو الزمن. كل صف هو امتداد واحد — وحدة عمل في خدمة واحدة. الامتدادات متداخلة لإظهار السببية: إن أطلق الامتداد أ الامتداد ب، يكون ب منتذراً تحت أ، ونطاق زمنه يقع ضمن نطاق أ.

كل امتداد يحمل مجموعة قياسية من الحقول:

  • trace_id — المعرف الـ 128 بت المشترك بين كل امتداد في نفس التتبع.
  • span_id — معرف الـ 64 بت الفريد لهذا الامتداد.
  • parent_span_id — span_id الامتداد الذي أطلق هذا (فارغ للامتداد الجذر).
  • name — اسم عملية مقروء للإنسان، مثل cart.GetItems أو db.query.
  • start_time وend_time — طوابع زمنية بدقة النانوثانية.
  • status — OK أو ERROR أو UNSET.
  • attributes — أزواج مفتاح/قيمة بسياق اعتباطي: http.method، db.statement، user.id، cart.item_count.
  • events — تعليقات توضيحية مُوقَّتة ضمن عمر الامتداد (مثل "إخفاق الكاش"، "محاولة إعادة رقم 2").
Trace waterfall — checkout request across five services 0ms 100ms 200ms 300ms 400ms api-gateway POST /checkout 400ms (root) auth-service 27ms cart-service 60ms redis 12 fraud-check third-party API call 275ms ← bottleneck order-service 30ms root span normal span slow / error span
شلال التتبع لطلب دفع: كل صف هو خدمة، العرض يُظهر المدة، التداخل يُظهر السببية. امتداد fraud-check مرئي فوراً كعنق الزجاجة.

لماذا لا تستطيع المقاييس والسجلات وحدها فعل ذلك

إليك السيناريو الذي أقنع كل شركة تقنية كبرى بالاستثمار الجاد في التتبع الموزع. لديك انحدار في زمن p99 — الدفع انتقل من 120ms إلى 400ms بين عشية وضحاها. المقاييس تُظهر الانحدار بوضوح. تعرف أنه حقيقي. ماذا الآن؟

مع المقاييس وحدها: تتحقق من لوحات الخدمات الخارجية واحدة تلو الأخرى. Auth تبدو بخير. Cart تبدو بخير. Fraud تبدو بخير — p99 فيها 280ms، ارتفع من 95ms، لكنك تلاحظ هذا فقط بعد 25 دقيقة من البحث في اللوحات، ولا تزال لا تعرف إن كانت fraud هي المُسبِّب أم ضحية لتباطؤ قاعدة البيانات.

مع السجلات وحدها: تبحث grep عن الطلبات ذات زمن الاستجابة العالي، تجد معرف تتبع في السجلات، ثم تفتح أربع واجهات بحث سجلات مختلفة لإيجاد كل سطور السجل بذلك المعرف، وتحسب الفجوات الزمنية بينها يدوياً. ممكن، لكن يستغرق 45 دقيقة ويتطلب أن كل خدمة سجلت بالفعل معرف التتبع (وهو ما لا يحدث في أغلب الأحيان).

مع التتبعات: تفتح واجهة التتبع، تُفلتر بـ service=checkout AND duration>200ms، تنقر على تتبع واحد، وترى الشلال فوراً. امتداد fraud-check أحمر وعرضه 275ms. تنقر عليه، تقرأ سماته: fraud.provider=acme-fraud-api، http.url=https://api.acmefraud.com/v2/check. تتحقق من صفحة حالة مزود الاحتيال — كان لديهم تدهور بدأ الساعة 23:47 الليلة الماضية. إجمالي وقت التحقيق: 4 دقائق.

ممارسة احترافية على النطاق الكبير: في Google (مع Dapper)، وUber (مع Jaeger)، وTwitter (مع Zipkin)، سير العمل القائم على التتبع أولاً هو المعيار للتحقيق في زمن الاستجابة. المبدأ: لا تُصحح زمن الاستجابة الموزع بالنظر إلى مقاييس كل خدمة بمعزل. ابدأ دائماً بتتبع يمثل الطلب المتدهور، ثم استخدم سمات الامتداد للتعمق في الخدمة والعملية المحددة التي تسبب المشكلة. هذا يقلل متوسط وقت التشخيص (MTTD) من ساعات إلى دقائق لانحدارات زمن الاستجابة.

نشر السياق: الغراء الذي يربط كل شيء

لكي يعمل التتبع عبر حدود الخدمات، يجب أن كل خدمة تمرر سياق التتبع للخدمة التالية. حين تستدعي الخدمة أ الخدمة ب عبر HTTP، تحقن معرف التتبع ومعرف الامتداد في ترويسات HTTP. حين تستدعيها عبر gRPC، تحقنهما في بيانات gRPC الوصفية. حين تنشر رسالة في Kafka، تحقنهما في ترويسات الرسالة. الخدمة ب تستخرج السياق عند الاستلام، تنشئ امتداداً جديداً ابناً، وتكمل التتبع.

تنسيق الترويسة القياسي (المحدد بمواصفة W3C Trace Context، المعتمد من OpenTelemetry) هو:

# ترويسة W3C traceparent — المعيار لنشر السياق # الصيغة: traceparent: {version}-{trace-id}-{parent-span-id}-{flags} traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 # ^^ version (دائماً 00) # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ trace ID بـ 128 بت (hex) # ^^^^^^^^^^^^^^^^ parent span ID بـ 64 بت (hex) # ^^ flags (01 = تم أخذ العينة) # حين تستدعي الخدمة أ (api-gateway) الخدمة ب (cart-service): # api-gateway تنشئ الامتداد الجذر: # trace_id = 4bf92f3577b34da6a3ce929d0e0e4736 # span_id = 00f067aa0ba902b7 # api-gateway تحقن في طلب HTTP الصادر: # GET /cart/items HTTP/1.1 # Host: cart-service.internal # traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 # tracestate: vendor-specific-data (اختياري) # cart-service تستخرج الترويسة، تنشئ امتداداً ابناً: # trace_id = 4bf92f3577b34da6a3ce929d0e0e4736 (نفسه!) # parent_id = 00f067aa0ba902b7 (امتداد api-gateway) # span_id = a3ce929d0e0e4736 (جديد، فريد لـ cart) # الـ backend يجمع كل الامتدادات بنفس trace_id في تتبع واحد.

نشر السياق هو ما يحوّل مجموعة امتدادات منفصلة لكل خدمة إلى تتبع موزع متماسك. بدونه لديك قياسات محلية منفصلة. بوجوده لديك رسم بياني سببي كامل لرحلة الطلب عبر نظامك بأكمله.

مصيدة إنتاجية — فقدان السياق الصامت: يتقطع نشر السياق بصمت في أنماط شائعة عدة: (1) أي خدمة تنشئ goroutine/thread خلفية جديدة دون نشر السياق تفقد التتبع من تلك النقطة فصاعداً؛ (2) مستهلكو قوائم الرسائل الذين لا يستخرجون ترويسات التتبع من بيانات الرسائل الوصفية يبدؤون تتبعاً جديداً منفصلاً؛ (3) أي خدمة تستدعي طرفاً ثالثاً ولا تحقن الترويسات تنشئ فجوة في التتبع. النتيجة هي تتبع مُجتزأ يجعل خدمتك تبدو كعنق الزجاجة بينما المشكلة في الواقع في استدعاء خارجي. أدِّر مسارات نشرك واختبرها صراحةً — النشر المكسور أحد أهم أسباب التتبعات المضللة في الإنتاج.

الحالة الإنتاجية: حين يُسدد التتبع تكلفته

للتتبع الموزع تكاليف حقيقية: عمل التجهيز، بنية تحتية للجامع، تخزين للامتدادات. عند إنتاجية عالية (أكثر من 10,000 طلب في الثانية)، تخزين كل امتداد بدون تخطيط مُكلف. لكن حساب العائد على الاستثمار واضح لأي منظمة تُشغّل خدمات مصغرة على نطاق واسع.

تأمل: انحدار في زمن p99 يُدهور معدل إتمام الدفع بنسبة 2% لشركة تعالج 10 ملايين دولار يومياً. كل ساعة يستمر فيها الانحدار تُكلف نحو 83 ألف دولار في إيرادات ضائعة. إن قلّصت التتبعات وقت التشخيص من 3 ساعات (ربط السجلات يدوياً) إلى 10 دقائق (شلال التتبع)، ذلك يوفر ساعتين و50 دقيقة لكل حادث — نحو 233 ألف دولار لكل حادث. تكلفة تشغيل مجموعة Jaeger أو Tempo مع أخذ عينات رأسية بنسبة 10% بضعة آلاف دولار شهرياً. الحساب ليس متقارباً.

هذا هو السبب في انتقال التتبع الموزع من نموذج أولي للبحث في Google (Dapper، 2010) إلى متطلب أساسي لأي شركة تُشغّل خدمات مصغرة على نطاق واسع. وهو أيضاً سبب إنشاء OpenTelemetry — المعيار المحايد للبائعين لإصدار التتبعات والمقاييس والسجلات — لمنع الاحتكار البائعي لحزم SDK التتبع الاحتكارية. الدروس الأخرى في هذا الدرس التعليمي تبني منظومة OpenTelemetry الكاملة، من التجهيز إلى الـ backends إلى أخذ العينات إلى سير عمل التصحيح في الإنتاج.

إلى أين نتجه: هذا الدرس وضّح السبب. الدرس 2 يُعرّف الامتدادات والتتبعات ونشر السياق بمصطلحات تقنية دقيقة. الدرس 3 يُقدّم OpenTelemetry كحزمة SDK معيارية وصيغة سلكية. الدروس 4-9 تغطي التجهيز وجامع OTel وخلفيات Jaeger/Tempo واستراتيجيات أخذ العينات والتصحيح في الإنتاج. الدرس 10 يجمع كل شيء في مشروع عملي يتتبع طلباً حقيقياً لخدمات مصغرة من البداية إلى النهاية.