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

المقاطع والتتبعات ونشر السياق

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

المقاطع والتتبعات ونشر السياق

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

تشريح المقطع (Span)

المقطع (Span) هو الوحدة الذرية في التتبع الموزع. يمثّل وحدة عمل واحدة: معالج HTTP وارد، استدعاء gRPC صادر، استعلام قاعدة بيانات، بحث في ذاكرة التخزين المؤقت، خطوة في مهمة خلفية. كل مقطع يحمل مجموعة ثابتة من الحقول يحددها مواصفات OpenTelemetry:

  • Trace ID — معرّف فريد عالميًا بحجم 128 بت (16 بايت) لرحلة الطلب بأكملها. تشترك جميع المقاطع التابعة للطلب نفسه في هذا المعرّف. يُرمَّز عادةً كسلسلة سداسية عشرية من 32 حرفًا: 4bf92f3577b34da6a3ce929d0e0e4736.
  • Span ID — معرّف فريد بحجم 64 بت (8 بايت) لهذا المقطع تحديدًا ضمن تتبّعه. يُرمَّز كـ 16 حرفًا سداسيًا: 00f067aa0ba902b7.
  • Parent Span ID — معرّف المقطع الأصلي المباشر. المقطع الجذري (نقطة الدخول) ليس له أصل (أو معرّف أصل يساوي صفرًا). هذا الحقل هو ما ينشئ شجرة الأصل-الفرع.
  • اسم العملية — اسم قابل للقراءة يصف العمل: HTTP GET /api/orders، db.query SELECT orders، redis.get order:8821.
  • وقت البداية — طابع زمني عالي الدقة (نانوثانية منذ بداية Unix).
  • المدة — الوقت المنقضي من البداية حتى النهاية بالنانوثانية.
  • الحالة — إحدى ثلاث قيم: UNSET أو OK أو ERROR. تعيين ERROR على مقطع هو ما يجعله قابلاً للاكتشاف في واجهات الخلفية وسياسات أخذ العينات الذيلية.
  • السمات (Attributes) — أزواج مفتاح-قيمة من البيانات الوصفية المنظمة. يُعرّف OTel اتفاقيات دلالية للسمات الشائعة: http.method، http.status_code، db.system، db.statement. أضف سماتك الخاصة: order.id، user.tier.
  • الأحداث (Events) — تعليقات ذات طوابع زمنية داخل مدة المقطع: تتبع مكدس الاستثناءات، إخفاقات ذاكرة التخزين المؤقت، محاولات إعادة المحاولة. ليست سجلات منفصلة — تعيش داخل المقطع.
  • الروابط (Links) — مراجع إلى مقاطع في تتبعات أخرى، تُستخدم لقوائم انتظار الرسائل وسير العمل غير المتزامن.
  • النوع (Kind) — تصنيف الدور: SERVER، CLIENT، PRODUCER/CONSUMER، INTERNAL.
السمات هي رافعتك الأساسية في تصحيح الأخطاء. التتبع يخبرك أين أُنفق الوقت. السمات تخبرك لماذا. في Google وUber، تتضمن سمات المقاطع السياق التجاري (مستوى المستخدم، مجموعة التجارب، حجم العربة) حتى يتمكن المهندسون من ربط ارتفاعات الكمون بشرائح مرور محددة فورًا — دون الحاجة إلى الانضمام عبر السجلات. حدد اتفاقياتك الدلالية مبكرًا وطبّقها في مكتبة قياس مشتركة.

العلاقات الأصل-الفرع وشجرة التتبع

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

تخيّل طلب دفع يتدفق عبر أربع خدمات. يُنشئ بوابة API المقطع الجذري. تستدعي خدمتين تاليتين بشكل متزامن — order-service وinventory-service — كل منها ينشئ مقطعًا فرعيًا. تستدعي order-service بعد ذلك قاعدة بيانات المدفوعات، مُنشئةً مقطعًا حفيدًا. الشجرة الناتجة لها أربعة عقد، ومدة الطلب الكلية يحددها المسار الحرج: أطول سلسلة متتابعة من المقاطع من الجذر إلى الورقة.

Trace tree and waterfall — spans, parent-child, and critical path Span Tree API Gateway root span trace_id=4bf92f… order-service span_id=00f067… inventory-service span_id=89ad12… postgres span_id=c3d4e5… المسار الحرج (أطول سلسلة متتابعة) Waterfall View (الخط الزمني) 0 ms 200 400 600 800 ms API Gateway 800 ms (root) order-service 690 ms inventory-svc 200 ms postgres 595 ms ← عنق الزجاجة
يسار: شجرة المقاطع مع علاقات الأصل-الفرع. يمين: عرض الشلال الزمني — المقاطع المتوازية تتداخل؛ المسار الحرج (استعلام postgres) يحدد الكمون الكلي.

W3C Trace Context: رأسية traceparent

لكي تعمل التتبعات عبر حدود الخدمات، يجب أن ينتقل سياق التتبع مع الطلب. إذا أنشأت الخدمة A مقطعًا جذريًا وأجرت استدعاءً HTTP للخدمة B، يجب أن تستقبل الخدمة B معرّف التتبع ومعرّف المقطع الأصلي حتى يُربط المقطع الذي تنشئه بمقطع الخدمة A في التتبع نفسه. بدون هذا النشر، تحصل على جزر منفصلة من المقاطع — عديمة الفائدة لتحليل السبب الجذري.

يحدد معيار W3C Trace Context (RFC نُشر عام 2021، مدعوم الآن بشكل شامل من OTel وJaeger وZipkin وDatadog وجميع موردي APM الرئيسيين) رأستَين HTTP لهذا الغرض:

  • traceparent — تحمل السياق الأساسي: الإصدار ومعرّف التتبع ومعرّف المقطع الأصلي وأعلام التتبع.
  • tracestate — أزواج مفتاح-قيمة اختيارية خاصة بالمورد (أولوية أخذ عينات Datadog، أعلام B3، إلخ) تنتقل جنبًا إلى جنب دون تعارض مع المعيار.

رأسية traceparent لها صيغة محددة بدقة: version-traceId-parentSpanId-flags. عمليًا تبدو هكذا:

# صيغة رأسية W3C traceparent: # <version>-<trace-id>-<parent-span-id>-<trace-flags> # # version = "00" (إصدار مواصفة W3C الحالي) # trace-id = 32 حرفًا سداسيًا (128-بت) — مشترك بين جميع المقاطع في هذا التتبع # parent-span-id= 16 حرفًا سداسيًا (64-بت) — معرّف المقطع المُستدعي (يصبح أصل المقطع الجديد) # trace-flags = 2 حرفًا سداسيًا — البت 0: علم أخذ العينات (01 = مأخوذ، 00 = غير مأخوذ) traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^ version trace-id parent-span-id flags # tracestate — امتدادات المورد بجانب الرأسية المعيارية tracestate: vendor1=abc123,dd=s:1;t.dm:-0 # مثال: بوابة API (مقطع جذري) تستدعي order-service عبر HTTP. # تضع بوابة API هذه الرأسيات على الطلب الصادر: GET /internal/orders HTTP/1.1 Host: order-service.svc.cluster.local traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 tracestate: rojo=00f067aa0ba902b7 # تستقبل order-service الطلب وتستخرج الرأسية: # - trace_id = 4bf92f3577b34da6a3ce929d0e0e4736 (إعادة استخدام — نفس التتبع) # - parent_span_id = 00f067aa0ba902b7 (مقطع بوابة API هو الأصل) # تُنشئ مقطع SERVER جديدًا بـ span_id جديد (مثلاً 89ad12c3b45e6f70) # ثم تضع traceparent على كل استدعاء صادر تُجريه، مستخدمةً span_id الجديد كأصل.
علم أخذ العينات استشاري وليس إجباريًا. علم 01 في traceparent يُشير إلى "أخذت عينة من هذا التتبع — الخدمات التالية، يرجى أيضًا أخذ العينات والإبلاغ عن المقاطع." لكن للخدمة التالية حرية تجاهله. في الممارسة الإنتاجية، تحترم الأنظمة العلم لضمان جمع جميع مقاطع التتبع المأخوذ منه عينة. عندما يكون العلم 00، لا تُبلّغ الخدمات التالية عادةً عن مقاطع، مما يُبقي الحمل العام قريبًا من الصفر للحركة غير المأخوذ منها عينات. يتعامل OTel SDK مع كل هذا تلقائيًا عند استخدام W3CPropagator.

نشر السياق في الممارسة

نشر السياق هو الآلية التي يُحقن بها سياق التتبع في الطلبات الصادرة ويُستخرج من الواردة. يوفر OTel SDK واجهة propagator تتعامل مع الحقن والاستخراج لتنسيقات نقل مختلفة. مُنشر W3C Trace Context هو الافتراضي لـ HTTP. لقوائم انتظار الرسائل (Kafka، RabbitMQ، SQS)، تُوضع المعرّفات نفسها في رأسيات الرسالة أو سماتها.

W3C traceparent propagation across three services API Gateway creates root span trace_id=4bf92f… span_id=00f067… parent=none (root) traceparent: 00-4bf92f…-00f067…-01 order-service extracts header → new span trace_id=4bf92f… (SAME) span_id=89ad12… (NEW) parent=00f067… (A) traceparent: 00-4bf92f…-89ad12…-01 payment-svc extracts header → new span trace_id=4bf92f… (SAME) span_id=c3d4e5… (NEW) parent=89ad12… (B) trace_id واحد يسافر عبر كل خدمة — دون تغيير. كل قفزة: استخراج traceparent الوارد → إنشاء مقطع جديد (span_id جديد) → حقن traceparent المُحدَّث على الاستدعاءات الصادرة. تجمع الخلفية جميع المقاطع في شجرة تتبع واحدة باستخدام مراجع trace_id + parent_span_id.
نشر W3C traceparent: نفس trace_id يتدفق دون تغيير عبر كل قفزة؛ كل خدمة تُنشئ span_id جديدًا وتُعيّن مُستدعيها كأصل.
# Python (FastAPI) — نشر السياق الصريح مع OTel SDK # pip install opentelemetry-sdk opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-httpx from opentelemetry import trace from opentelemetry.propagate import inject, extract from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator import httpx tracer = trace.get_tracer("order-service", "1.0.0") # الوارد: قياس FastAPI في OTel يستخرج traceparent من رأسيات الطلب تلقائيًا # ويُنشئ مقطع SERVER كأصل لجميع الأعمال في هذا الطلب. # الصادر: حقن سياق التتبع في استدعاءات HTTP للتدفق التالي async def call_inventory_service(order_id: str, parent_ctx): with tracer.start_as_current_span( "inventory.check_stock", kind=trace.SpanKind.CLIENT, attributes={ "http.method": "GET", "http.url": f"http://inventory-svc/stock/{order_id}", "order.id": order_id, }, ) as span: headers = {} inject(headers) # OTel يكتب traceparent + tracestate في هذا القاموس # headers الآن: {"traceparent": "00-4bf92f...-89ad12...-01"} async with httpx.AsyncClient() as client: response = await client.get( f"http://inventory-svc/stock/{order_id}", headers=headers, # السياق منقول إلى inventory-service ) if response.status_code != 200: span.set_status(trace.StatusCode.ERROR, "inventory check failed") span.set_attribute("http.status_code", response.status_code) return response.json() # غير متزامن/قائمة انتظار رسائل: نشر في رأسيات رسائل Kafka def publish_order_event(producer, topic: str, payload: dict): with tracer.start_as_current_span("kafka.produce", kind=trace.SpanKind.PRODUCER) as span: headers = {} inject(headers) # نفس استدعاء inject() — يعمل لأي قاموس حامل producer.produce( topic, value=json.dumps(payload).encode(), headers=list(headers.items()), # traceparent ينتقل كرأسية رسالة Kafka ) span.set_attribute("messaging.system", "kafka") span.set_attribute("messaging.destination", topic)

أحداث المقاطع وسماتها: أنماط الإنتاج

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

حدث المقطع هو تعليق ذو طابع زمني مرفق بمقطع. بدلاً من إصدار سطر سجل منفصل لـ "إخفاق ذاكرة التخزين المؤقت، العودة إلى قاعدة البيانات"، سجّله كحدث على المقطع النشط. يُبقي ذلك البيانات في مكان واحد — عندما تنظر إلى مقطع بطيء في واجهة التتبع، ترى بالضبط ما حدث ومتى، دون الحاجة إلى التحوّل إلى مخزن السجلات والربط بالطابع الزمني.

يجب أن تلتقط السمات السياق التجاري الذي يحوّل "استعلام قاعدة البيانات هذا كان بطيئًا" إلى "استعلام قاعدة البيانات هذا كان بطيئًا لمستخدمي Premium في ألمانيا الذين يطلبون أكثر من 50 عنصرًا." أضف بحد أقصى 20-30 سمة لكل مقطع — كل سمة مُفهرَسة في الخلفية ولها تكلفة تخزين. تجنّب القيم عالية القدرة الاستيعابية (أجسام استعلامات SQL الخام، أجسام استجابة HTTP الكاملة) كسمات مقطع؛ قُصّها أو احذفها عند الحاجة.

لا تضع أبدًا بيانات شخصية أو أسرارًا في سمات المقاطع أو أحداثها. تُرسَل التتبعات إلى خلفية (Jaeger، Tempo، Datadog، Honeycomb) وتُخزَّن لأيام أو أسابيع، غالبًا بوصول داخلي واسع. سمات المقاطع تُصدَّر كثيرًا إلى خلفيات SaaS طرف ثالث. نظّف عناوين البريد الإلكتروني لأرقام الهواتف وأرقام بطاقات الدفع وكلمات المرور والرموز المميزة للمصادقة قبل أن تظهر في أي مقطع. استخدم طبقة تعقيم بيانات في مكتبة القياس المشتركة، مُطبَّقة على مستوى SDK، وليست متروكة للمطورين الأفراد.
# إضافة أحداث وسمات مقطع — Go (OpenTelemetry SDK) # go get go.opentelemetry.io/otel import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" ) tracer := otel.Tracer("checkout-service") func processOrder(ctx context.Context, orderID string, userTier string) error { ctx, span := tracer.Start(ctx, "checkout.process_order", trace.WithSpanKind(trace.SpanKindServer), trace.WithAttributes( attribute.String("order.id", orderID), attribute.String("user.tier", userTier), // سياق تجاري semconv.ServiceNameKey.String("checkout"), ), ) defer span.End() // فحص ذاكرة التخزين المؤقت item, found := cache.Get(orderID) if !found { // حدث المقطع: تعليق ذو طابع زمني داخل هذا المقطع span.AddEvent("cache.miss", trace.WithAttributes( attribute.String("cache.key", "order:"+orderID), attribute.String("cache.store", "redis"), )) item = db.FetchOrder(ctx, orderID) // مقطع فرعي يُنشأ تلقائيًا بقياس DB } if err := payments.Charge(ctx, item); err != nil { // تعيين المقطع كخطأ — يظهر في فلاتر الخطأ بـ Jaeger/Tempo span.RecordError(err) span.SetStatus(codes.Error, "payment charge failed") span.SetAttribute(attribute.String("error.type", "payment_declined")) return err } span.SetAttributes( attribute.Int64("order.item_count", int64(len(item.SKUs))), attribute.Float64("order.amount_usd", item.TotalUSD), ) return nil }
انشر السياق عبر الحدود غير المتزامنة بشكل صريح. Goroutines وpools الخيوط والمهام غير المتزامنة تقطع سلسلة السياق الضمنية. مرّر دائمًا context.Context (Go) أو Context (Java) أو contextvars.Context (Python) بشكل صريح عبر الحدود غير المتزامنة. إذا بدأت goroutine أو مهمة pool خيوط، التقط سياق المقطع الحالي قبل الحد غير المتزامن واستعده بالداخل. لا يستطيع OTel SDK فعل ذلك تلقائيًا — وهو أحد أكثر الأسباب شيوعًا للتتبعات المعطوبة في الإنتاج حيث تكون روابط الأصل-الفرع مفقودة.

ما يُعطّل التتبعات في الإنتاج

فهم أنماط الفشل بنفس أهمية فهم المسار السعيد. الأسباب الشائعة للتتبعات المعطوبة أو غير المكتملة:

  • نشر مفقود عند قفزة واحدة: خدمة واحدة — غالبًا نظام قديم أو موزّع حمل أو بوابة API — تحذف traceparent أو تتجاهلها. جميع المقاطع التالية لا تزال تحمل معرّف التتبع لكن رابط أصلها يشير إلى مقطع لم تستقبله الخلفية أبدًا، مُنشئةً شجرة فرعية منفصلة. الإصلاح: تدقيق كل حد خدمة وكل تكوين HTTP proxy.
  • فقدان سياق غير متزامن: يبدأ مقطع في خيط A، يُقابَل العمل في خيط B، ويُنشئ خيط B مقاطع فرعية — لكن بدون نقل السياق عبر الحد غير المتزامن، يُنشئ خيط B مقطعًا جذريًا جديدًا بدلاً من فرعي. ينقسم التتبع إلى شجرتين غير مترابطتين.
  • انحراف الساعة: تأتي الطوابع الزمنية للمقطع من المضيف الذي يعمل عليه SDK. إذا كان للمضيفين انجراف ساعة (NTP غير مكوّن)، تبدو المقاطع وكأنها تبدأ قبل انتهاء أصلها — حالة مستحيلة فيزيائيًا. إصلاح الإنتاج: تشغيل chrony أو ntpd على جميع العقد.
  • عدم تطابق أخذ العينات: أخذ عينات رأسي بمعدلات مختلفة لكل خدمة يعني أن الخدمة A تأخذ 10% والخدمة B تأخذ 5%. قد لا يأخذ تتبع مأخوذ عند A عينات عند B، مُنشئًا تتبعًا غير مكتمل. استخدم أخذ عينات ذيلي في طبقة Collector لاتخاذ قرار الاحتفاظ/الحذف مرة واحدة، مركزيًا، لكامل التتبع.
  • إسقاط دفعات المقاطع تحت الحمل: يُجمّع OTel SDK المقاطع في الذاكرة قبل التصدير. تحت ذروة الحركة، إذا امتلأت قائمة انتظار الدفعات أسرع مما يمكن للمُصدِّر تفريغها، تُسقَط المقاطع. راقب otelcol_exporter_send_failed_spans_total في Collector وحجّم ذاكرة المعالج الدفعي (queue_size) لذروة حمولتك.

في الدرس التالي ننتقل إلى معيار OpenTelemetry نفسه — نموذج مكوناته (SDK، API، Collector، الاتفاقيات الدلالية)، وكيف حقق الحياد من البائع، وكيفية تقييمه مقابل الوكلاء الخاصة مثل Datadog tracer أو Dynatrace OneAgent لخدمة جديدة أو ترحيل.