مستوى المقال: محترف. موجّه لمهندسي الـ backend والـ SRE اللي بيشغّلوا خدمات مصغّرة في الإنتاج ومحتاجين يلاقوا سبب البطء بسرعة.
لو طلب POST /checkout بياخد 742 مللي ثانية وانت بتقلّب في لوجات 7 خدمات عشان تعرف مين المتسبّب، انت بتضيّع وقتك. الـ Distributed Tracing بيوريك الخدمة المسؤولة بالظبط في أقل من دقيقة، من غير ما تخمّن.
التتبّع الموزّع بـ OpenTelemetry: حدّد الخدمة اللي بتبطّئ نظامك
المشكلة باختصار
في المعمارية المتجانسة (monolith)، البطء بيتقاس بـ profiler واحد. في الخدمات المصغّرة، الطلب الواحد بيعدي على 5 لـ 15 خدمة عبر الشبكة. اللوج بيقولك إن كل خدمة "اشتغلت"، لكن مفيش حاجة بتربط سطور اللوج المتفرّقة في رحلة طلب واحدة. النتيجة: P95 latency بيرتفع، ومحدش عارف الخدمة المتسبّبة.
الـ tracing بيحل ده بإنه بيدّي كل طلب معرّف واحد بيتنقل مع الطلب من خدمة لخدمة. كل خدمة بتسجّل الجزء بتاعها تحت نفس المعرّف، فتقدر تجمّعهم وتشوف الرحلة كاملة.
المفهوم الأساسي: trace و span وانتشار السياق
قبل التعريف العلمي، خد المثال ده. تخيّل شحنة بتتبعت من مخزن لباب البيت. الشركة بتديها رقم تتبّع واحد. كل محطة (الفرز، الشحن الدولي، مكتب التوزيع، المندوب) بتسجّل دخول وخروج الشحنة تحت نفس الرقم. في الآخر بتفتح صفحة التتبّع وتشوف كل محطة أخدت قد إيه، وتعرف فين الشحنة اتأخرت.
دلوقتي التعريف الدقيق. الـ trace هو رحلة الطلب الكاملة عبر النظام، وله معرّف trace_id فريد. كل عملية داخل الرحلة (نداء HTTP، استعلام DB، نداء خارجي) بتتسجّل كـ span له span_id، ووقت بداية ونهاية، وعلاقة أب-ابن مع الـ span اللي ناداه. لما تجمّع كل الـ spans بنفس الـ trace_id بترتّبهم زمنيًا، بتطلع لوحة شلالية (waterfall) بتوريك أنهي span أخد أطول وقت.
القطعة اللي بتخلّي ده يشتغل عبر الشبكة اسمها context propagation: الخدمة بتبعت الـ trace_id و span_id الحالي في هيدر HTTP اسمه traceparent حسب مواصفة W3C Trace Context. شكله كده:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01الخدمة اللي بتستقبل الهيدر ده بتكمّل نفس الـ trace بدل ما تبدأ واحد جديد. ده اللي بيربط الخدمات مع بعض في رحلة واحدة.
التطبيق العملي: instrumentation بـ OpenTelemetry
OpenTelemetry (اختصارًا OTel) هو المعيار الموحّد تحت مظلة CNCF لتوليد الـ traces والـ metrics والـ logs. ميزته إنه vendor-neutral: بتجمّع البيانات مرة واحدة وتبعتها لأي backend (Jaeger، Tempo، Datadog) من غير ما تغيّر كود التطبيق.
أسرع طريقة في بايثون هي الـ auto-instrumentation. بيلفّ مكتبات شائعة (Flask، requests، psycopg) تلقائيًا من غير تعديل كود:
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=order-service \
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 \
opentelemetry-instrument python app.pyللأجزاء الحرجة زوّد span يدوي بسمات (attributes) بتفرّق وقت التشخيص. ركز على إنك تسجّل القيم اللي هتدوّر بيها بعدين:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("payment.charge") as span:
span.set_attribute("payment.provider", "bank-api")
span.set_attribute("order.amount_egp", order.total)
resp = call_bank_api(order)
span.set_attribute("http.status_code", resp.status_code)في السيناريو اللي فوق، لما تفتح الـ waterfall هتلاقي span اسمه payment.charge أخد 528 مللي من أصل 742، يعني حوالي 74% من زمن الطلب الكلي، وكله مستنّي رد الـ bank-api الخارجي. هنا القرار واضح: المشكلة مش في كودك، هي في الاعتماد الخارجي، والحل timeout أقصر مع retry محسوب أو نداء غير متزامن.
المعاينة (sampling): الـ trade-off الأهم في الإنتاج
تسجيل 100% من الـ traces في خدمة بتستقبل عشرات آلاف الطلبات في الثانية بيغرق الشبكة والتخزين. الحل هو المعاينة. وهنا الفرق الجوهري:
- Head sampling: بتقرّر تسجّل الطلب من عند أول خدمة (مثلًا 10% عشوائي). رخيص، لكن ممكن يرمي الطلب اللي فيه الخطأ.
- Tail sampling: بتستنى الـ trace يكتمل، وبعدين تقرّر تحتفظ بيه لو فيه خطأ أو بطء. بيضمن إنك تشوف كل المشاكل، مقابل إنه محتاج ذاكرة في الـ Collector عشان يجمّع الـ spans.
إعداد tail sampling في الـ OpenTelemetry Collector بيحتفظ بكل الأخطاء وكل طلب أبطأ من 500 مللي، ويعاين 10% من الباقي:
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: keep-errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: keep-slow
type: latency
latency: { threshold_ms: 500 }
- name: sample-the-rest
type: probabilistic
probabilistic: { sampling_percentage: 10 }الافتراض هنا إن عندك Collector مستقل (مش agent جوّه كل pod) عشان الـ tail sampling محتاج يشوف كل spans الـ trace في مكان واحد. لو الـ Collector متعدّد النسخ من غير load balancing على أساس الـ trace_id، هتفقد أجزاء من الـ traces.
التكلفة الحقيقية وما يجب الانتباه له
الـ overhead بتاع الـ auto-instrumentation في معظم الخدمات بيكون أقل من 5% على الـ CPU (رقم تقديري بيختلف حسب عدد الـ spans وحجم السمات). لكن في فخّين بيوجعوا:
- Cardinality: لو حطّيت قيمة عالية التغيّر كسمة (زي
user_idأوorder_id) على metric، هتفجّر تكلفة التخزين. خلّي القيم دي على الـ spans مش على الـ metrics. - حجم الـ attributes: تسجيل body كامل لكل request بيكبّر كل span ويرفع تكلفة الشبكة. سجّل المفيد بس.
- الـ context propagation الناقص: لو خدمة واحدة في السلسلة مش بتمرّر هيدر
traceparent، الـ trace هيتقطع عندها وهتشوف رحلتين منفصلتين بدل واحدة.
متى لا تستخدم هذه الطريقة
لو نظامك monolith واحد من غير نداءات شبكة بين مكوّنات، الـ distributed tracing بيبقى overhead بلا فايدة، والـ profiler العادي أنسب وأرخص. وكمان لو عندك خدمتين بس بتتكلموا مرة في الطلب، الـ structured logging بـ correlation id ممكن يكفّي من غير ما تركّب stack كامل. الـ tracing بيستحق التعب لما يبقى عندك 4 خدمات فأكثر في مسار الطلب الواحد.
الخطوة التالية
اختار أبطأ endpoint عندك دلوقتي، شغّل عليه opentelemetry-instrument وابعت الـ traces لـ Jaeger محلي عبر Docker. افتح أول waterfall وشوف أنهي span واخد أطول وقت. الأغلب إنك هتلاقي السبب في نداء خارجي أو استعلام DB مش متوقّع، مش في الكود اللي بتشكّ فيه.
المصادر
- OpenTelemetry — التوثيق الرسمي ومفاهيم الـ traces والـ instrumentation: opentelemetry.io/docs
- W3C Trace Context — مواصفة هيدر
traceparent(W3C Recommendation): w3.org/TR/trace-context - OpenTelemetry Collector — موثّق الـ tailsamplingprocessor على GitHub: github.com/open-telemetry/opentelemetry-collector-contrib
- Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Sigelman et al., Google, 2010 (الورقة الأصل لمفهوم الـ span وانتشار السياق)
- CNCF — OpenTelemetry كمشروع graduated: cncf.io