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

مشروع: تتبع طلب عبر خدمات مصغرة

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

مشروع: تتبع طلب عبر خدمات مصغرة

يجمع هذا الدرس الختامي كل ما في الدرس التعليمي في سير عمل متكامل يمثل البيئة الإنتاجية بدقة. ستُجهّز تدفق تجارة إلكترونية من ثلاث خدمات — بوابة API وخدمة طلبات وخدمة مخزون — بـ OpenTelemetry، وتُوجّه الامتدادات عبر OTel Collector، وتُخزّنها في Jaeger، وتُجري تحقيقاً واقعياً في تأخر باستخدام شلال التتبع. كل خطوة هنا تعكس ما تفعله فعلاً فرق هندسية في Stripe وDoorDash وShopify عند تتبع حركة مرور الإنتاج.

معمارية المشروع التجريبي

السيناريو: يدخل طلب POST /orders إلى api-gateway (Node.js)، الذي يستدعي order-service (Python/FastAPI)، الذي بدوره يستدعي inventory-service (Go) لحجز المخزون. خدمة المخزون تكتب أيضاً إلى PostgreSQL. تُصدر الخدمات الثلاث امتدادات OTLP إلى OTel Collector مشترك، يُصدّرها إلى Jaeger. هذا تصميم بسيط لكنه واقعي — أنماط التجهيز ذاتها تتسع لشبكات من 200 خدمة.

Project architecture: three services, OTel Collector, Jaeger Client HTTP POST api-gateway Node.js OTel SDK order-service Python OTel SDK inventory-svc Go OTel SDK + Postgres OTLP/gRPC OTLP/gRPC OTLP/gRPC OTel Collector receive → batch → export Jaeger Jaeger UI + Storage
معمارية المشروع: ثلاث خدمات مُجهَّزة تُصدر امتدادات OTLP إلى Collector مشترك يُصدّرها إلى Jaeger للعرض والتحليل.

الخطوة 1 — تشغيل البنية التحتية

استخدم Docker Compose لتشغيل Jaeger وOTel Collector جنباً إلى جنب. في الإنتاج ستنشرها كأحمال عمل Kubernetes (Deployment + Service)، لكن Compose مثالي للتكرار المحلي. احفظ هذا كـ docker-compose.yml في جذر مشروعك.

version: "3.9" services: jaeger: image: jaegertracing/all-in-one:1.55 ports: - "16686:16686" # واجهة Jaeger - "4317:4317" # OTLP gRPC (إن كشفتها مباشرة — تجنب في الإنتاج) environment: - COLLECTOR_OTLP_ENABLED=true otel-collector: image: otel/opentelemetry-collector-contrib:0.95.0 volumes: - ./otel-collector-config.yml:/etc/otelcol/config.yaml ports: - "4318:4318" # OTLP HTTP (الخدمات ترسل الامتدادات هنا) - "55679:55679" # واجهة zPages للصحة والتصحيح depends_on: - jaeger

أنشئ الآن otel-collector-config.yml. يعكس هذا إعداد Collector إنتاجياً أدنى — معالج batch لتقليل ضغط الكتابة على Jaeger، ومحدد الذاكرة لمنع OOM تحت ذروة حركة المرور.

receivers: otlp: protocols: grpc: endpoint: "0.0.0.0:4317" http: endpoint: "0.0.0.0:4318" processors: batch: timeout: 5s send_batch_size: 512 memory_limiter: check_interval: 1s limit_mib: 400 spike_limit_mib: 100 exporters: otlp/jaeger: endpoint: "jaeger:4317" tls: insecure: true logging: loglevel: warn # اضبطه على debug مؤقتاً عند استكشاف امتدادات مفقودة service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, batch] exporters: [otlp/jaeger, logging]
الفكرة الأساسية — ترتيب المعالجات مهم: ضع دائماً memory_limiter قبل batch في الأنبوب. إن ارتفع استخدام الذاكرة، يُسقط المحدد الامتدادات قبل تراكمها في مخزن الـ batcher. عكس الترتيب يعني أن الـ batcher يمتلئ تحت الضغط ثم يتخلص المحدد من مخزن كبير دفعة واحدة — محدثاً ارتفاعات في زمن الاستجابة في الـ Collector ذاته.

الخطوة 2 — تجهيز خدمة الطلبات (Python)

خدمة الطلبات تطبيق FastAPI. ثبّت حزم OTel SDK وهيّئ التجهيز التلقائي لـ HTTP وSQLAlchemy. استخدم التجهيز بلا كود لطبقات الإطار، والامتدادات اليدوية لمنطق أعمالك الخاص — هذا هو النمط الإنتاجي: دع التجهيز التلقائي يغطي الشيفرة المتكررة الممللة، واحتفظ بالامتدادات اليدوية للعمليات المهمة لنطاق خدمتك.

# إضافات requirements.txt opentelemetry-api==1.23.0 opentelemetry-sdk==1.23.0 opentelemetry-instrumentation-fastapi==0.44b0 opentelemetry-instrumentation-httpx==0.44b0 opentelemetry-instrumentation-sqlalchemy==0.44b0 opentelemetry-exporter-otlp-proto-grpc==1.23.0 # --- order_service/tracing.py --- from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource, SERVICE_NAME import os def configure_tracing(): resource = Resource.create({ SERVICE_NAME: "order-service", "service.version": os.getenv("APP_VERSION", "unknown"), "deployment.environment": os.getenv("ENV", "local"), }) provider = TracerProvider(resource=resource) exporter = OTLPSpanExporter( endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317"), insecure=True, ) provider.add_span_processor(BatchSpanProcessor( exporter, max_queue_size=2048, max_export_batch_size=512, schedule_delay_millis=5000, )) trace.set_tracer_provider(provider) # --- order_service/main.py --- @app.post("/orders") async def create_order(payload: dict): with tracer.start_as_current_span("validate-order") as span: span.set_attribute("order.item_count", len(payload.get("items", []))) span.set_attribute("order.customer_tier", payload.get("tier", "standard")) if not payload.get("items"): span.set_status(trace.StatusCode.ERROR, "empty order") span.record_exception(ValueError("Order has no items")) raise HTTPException(400, "Order must contain at least one item") # استدعاء httpx مُجهَّز تلقائياً — traceparent يُحقن تلقائياً async with httpx.AsyncClient() as client: resp = await client.post( "http://inventory-service:8001/reserve", json={"items": payload["items"]}, timeout=5.0, ) resp.raise_for_status() return {"order_id": "ord_" + str(uuid.uuid4())[:8], "status": "confirmed"}
ممارسة احترافية — سمات الـ Resource هي هويتك في Jaeger: اضبط دائماً service.name وservice.version وdeployment.environment في الـ Resource. يستخدم Jaeger service.name كمفتاح فهرس رئيسي لقائمة الخدمات. بدون service.version، لا تستطيع التمييز بين أي نشر أدخل انحدار زمن الاستجابة حين تعمل ثلاث بودات canary بإصدارات مختلفة في وقت واحد.

الخطوة 3 — تجهيز خدمة المخزون (Go)

تستخدم خدمة Go حزمة OTel Go SDK. غلّف استدعاءات قاعدة البيانات بامتدادات يدوية — مشغّل Go SQL ليس لديه تجهيز تلقائي مستقر في كل الإصدارات، لذا إنشاء الامتداد الصريح هو النهج الموثوق. لاحظ كيف تتبع سمات الامتداد الاتفاقيات الدلالية لـ OpenTelemetry لعمليات قاعدة البيانات: db.system وdb.name وdb.statement.

// inventory-service/tracing.go func InitTracer(ctx context.Context) (*sdktrace.TracerProvider, error) { exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint("otel-collector:4317"), otlptracegrpc.WithInsecure(), ) res, _ := resource.New(ctx, resource.WithAttributes( semconv.ServiceName("inventory-service"), semconv.ServiceVersion("1.4.2"), attribute.String("deployment.environment", "local"), ), ) tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), // 10% head-based sampling في الإنتاج؛ 100% محلياً sdktrace.WithSampler(sdktrace.TraceIDRatioBased(1.0)), ) otel.SetTracerProvider(tp) return tp, nil } // inventory-service/handler.go func reserveStock(ctx context.Context, items []Item) error { tracer := otel.Tracer("inventory-service") ctx, span := tracer.Start(ctx, "db.reserve-stock") defer span.End() span.SetAttributes( attribute.String("db.system", "postgresql"), attribute.String("db.name", "inventory"), attribute.Int("db.items_count", len(items)), // ملاحظة: احذف db.statement في الإنتاج إن كان يحتوي على بيانات شخصية أو أسرار attribute.String("db.statement", "UPDATE stock SET reserved = reserved + $1 WHERE sku = $2"), ) // تنفيذ استعلامات قاعدة البيانات... span.SetStatus(codes.Ok, "") return nil }

الخطوة 4 — توليد الحمل والعثور على تتبع في Jaeger

شغّل الكومة، أرسل طلباً تجريبياً، وانتقل إلى واجهة Jaeger على http://localhost:16686.

# تشغيل Collector وJaeger docker compose up -d # إرسال طلب لإنشاء طلب شراء curl -X POST http://localhost:8000/orders \ -H "Content-Type: application/json" \ -d '{"items":[{"sku":"SKU-001","qty":2},{"sku":"SKU-007","qty":1}],"tier":"gold"}' # الاستجابة المتوقعة: # {"order_id":"ord_a3f9bc12","status":"confirmed"} # التحقق من أن الـ Collector يستقبل الامتدادات curl http://localhost:55679/debug/tracez | grep -A5 "order-service" # في واجهة Jaeger: # 1. اختر "order-service" من قائمة الخدمات # 2. انقر "Find Traces" # 3. انقر على التتبع الذي يُظهر POST /orders # 4. لاحظ الشلال: api-gateway جذر → validate-order → httpx POST → db.reserve-stock

الخطوة 5 — حقن خطأ تأخر وتصحيحه بالتتبعات

حاكِ الآن السيناريو الذي يواجهه كل مهندس مناوبة في نهاية المطاف: انحدار في زمن الاستجابة في خدمة خارجية غير مرئي من لوحة مقاييس المستوى الأعلى. احقن تأخيراً اصطناعياً في خدمة المخزون — هذا يمثل فحص SQL بطيئاً، أو كاش بارداً، أو API خارجية مثقلة.

// احقن هذا في reserveStock() لمحاكاة استعلام بطيء time.Sleep(350 * time.Millisecond) span.AddEvent("slow-index-scan-detected", trace.WithAttributes( attribute.String("db.index", "stock_sku_idx"), attribute.Int("db.rows_scanned", 95000), ))

بعد حقن هذا التأخير، أرسل طلباً آخر. افتح Jaeger، وابحث عن التتبعات من order-service بمدة أكبر من 300ms (استخدم مرشح "Min Duration"). انقر على التتبع البطيء. ستر بدقة ما يكشفه التجهيز:

  • الامتداد الجذر POST /checkout في api-gateway يُظهر الآن ~370ms إجمالاً.
  • امتداد validate-order يكتمل في ~2ms — ليس هو السبب.
  • امتداد httpx POST /reserve هو ~365ms — الاستدعاء إلى المخزون بطيء.
  • بداخله، db.reserve-stock هو ~355ms، وسجل أحداثه يُظهر slow-index-scan-detected مع db.rows_scanned: 95000.

السبب الجذري محدد في أقل من 60 ثانية. لا بحث بـ grep في السجلات. لا تنقل بين اللوحات. سمة الامتداد db.rows_scanned: 95000 تخبرك بالضبط ما تُصلحه: أضف فهرساً تغطوياً على stock(sku). هذه القيمة الكاملة للتتبع الموزع تتحقق عملياً.

Trace waterfall showing the injected latency regression 0ms 100ms 200ms 300ms 370ms api-gateway POST /orders 370ms (root) validate-order 2ms ✓ order→inventory httpx POST /reserve 365ms db.reserve-stock db.reserve-stock 355ms ← SLOW (rows_scanned: 95000) slow-index-scan root span fast span slow (cross-service) السبب الجذري الإصلاح: CREATE INDEX CONCURRENTLY ON stock(sku); rows_scanned تنخفض من 95,000 إلى 1 · زمن الاستجابة: من 355ms إلى 3ms
شلال التتبع بعد حقن خطأ التأخر: حدث slow-index-scan على امتداد db.reserve-stock يحدد السبب الجذري والإصلاح فوراً.

الخطوة 6 — ربط التتبعات بالمقاييس والسجلات (Exemplars)

في الإنتاج، تكتمل حلقة قابلية الملاحظة الكاملة حين تستطيع القفز من تنبيه Prometheus إلى تتبع محدد. تدعم OpenTelemetry exemplars — مراجع trace ID مضمّنة داخل نقاط بيانات المقاييس. حين يجمع Prometheus بياناتك، يمكن لحاويات المدرّج التكراري ذات الزمن العالي أن تحمل trace_id يرتبط مباشرة بالتتبع الذي أنتج تلك النقطة.

في Python مع مُصدّر OTel Prometheus، تُصدَر الـ exemplars تلقائياً حين يكون الامتداد الحالي مأخوذاً كعينة. في Grafana، فعّل الـ exemplars على لوحة المدرّج التكراري لترى نقاطاً برتقالية على الحاويات البطيئة — انقر أي نقطة للقفز مباشرة إلى تتبع Jaeger. هذا يلغي الخطوة اليدوية لنسخ trace ID من سطر سجل إلى مربع بحث Jaeger. إنه سير عمل "مقاييس-إلى-تتبعات" القياسي المستخدم في Grafana Cloud وDatadog وHoneycomb.

يجب أيضاً حقن trace_id في كل سطر سجل منظم يُصدَر خلال طلب. في Python مع structlog أو جسر تسجيل OTel، يحدث هذا تلقائياً حين تُهيّئ حزمة SDK تسجيل OTel. النتيجة: من استعلام Loki يمكنك استخراج حقل trace_id ومتابعته مباشرة إلى واجهة Jaeger — مغلقاً مسار الترابط "سجلات-إلى-تتبعات".

ممارسة احترافية — الترابط الثلاثي: في Uber وNetflix، المعيار الذهبي هو أن أي إشارة — تنبيه أو سطر سجل أو حاوية مدرّج تكراري ذات زمن عالٍ — تحمل trace_id قابلاً للنقر. إعداد هذا يتطلب: (1) نشر سياق تتبع OTel في السجلات عبر جسر التسجيل؛ (2) تفعيل exemplars على المدرّجات التكرارية؛ (3) تهيئة Grafana بمصدر بيانات Jaeger مرتبط بمصدر بيانات Prometheus. بعد التوصيل، يمكن لمهندس المناوبة الانتقال من تنبيه PagerDuty ← لوحة Grafana ← exemplar ذو زمن عالٍ ← تتبع Jaeger ← امتداد السبب الجذري في أقل من 3 دقائق. هذا هو هدف MTTD لمنظمة SRE ناضجة.

الخطوة 7 — أخذ العينات بالذيل لحجم الإنتاج

إعدادك المحلي يأخذ عينات من 100% من التتبعات. في الإنتاج عند إنتاجية ذات قيمة، لا تستطيع تخزين كل امتداد. الاستراتيجية الصحيحة — مغطاة بعمق في الدرس 7 — هي أخذ العينات بالذيل في الـ Collector: اجمع كل الامتدادات، اتخذ قرار أخذ العينة بعد اكتمال التتبع، واحتفظ بـ 100% من تتبعات الأخطاء والتتبعات البطيئة مع أخذ عينات من التتبعات الصحية السريعة بنسبة 1-10%.

أضف هذا إلى إعداد Collector لتفعيل أخذ العينات بالذيل في الإنتاج:

# أضف إلى قسم processors في otel-collector-config.yml: processors: tail_sampling: decision_wait: 30s # انتظر حتى 30 ثانية لوصول كل الامتدادات num_traces: 100000 # احتفظ بحتى 100k تتبع جارٍ في الذاكرة expected_new_traces_per_sec: 1000 policies: - name: keep-errors type: status_code status_code: {status_codes: [ERROR]} - name: keep-slow type: latency latency: {threshold_ms: 500} - name: probabilistic-baseline type: probabilistic probabilistic: {sampling_percentage: 5} # احتفظ بـ 5% من التتبعات السريعة الصحية service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, tail_sampling, batch] exporters: [otlp/jaeger, logging]
مصيدة إنتاجية — ذاكرة أخذ العينات بالذيل: يحدد إعداد num_traces عدد التتبعات المفتوحة التي يحتفظ بها الـ Collector في الذاكرة أثناء انتظار وصول كل الامتدادات. عند 1,000 طلب في الثانية مع نافذة 30 ثانية، تحتاج سعة لـ 30,000 تتبع جارٍ. كل تتبع يحتفظ بامتداداته في الذاكرة — عند 10 امتدادات لكل تتبع بمتوسط 1KB لكل منها، ذلك 300MB من ذاكرة العمل فقط لمخزن أخذ العينات. حجّم بود الـ Collector وفقاً لذلك، واضبط limit_mib على memory_limiter باعتدال. سيُسقط الـ Collector الامتدادات بأناقة قبل الـ OOM، لكن نافذة قرار أخذ العينة ستتقلص تحت الضغط — راقب otelcol_processor_tail_sampling_sampling_decision_timer_latency في Prometheus.

اكتمال المشروع: ما بنيته

لديك الآن نظام تتبع موزع يعمل بشكل كامل. ثلاث خدمات تُصدر امتدادات بصيغة OTLP، يستقبلها الـ Collector ويجمعها بكفاءة ويطبق أخذ العينات بالذيل ويُصدّرها إلى Jaeger. أظهر شلال التتبع اختناق قاعدة بيانات بمقدار 355ms في أقل من 60 ثانية — وهو ما كان سيستغرق 30-45 دقيقة بربط السجلات. الـ Exemplars تغلق الحلقة من المقاييس إلى التتبعات، وجسر التسجيل يغلقها من السجلات إلى التتبعات. هذه هي قابلية الملاحظة الموحدة: نفس الحدث مرئي من ثلاثة زوايا، كل منها قابل للتنقل إلى الآخرين بنقرة واحدة.

الأنماط هنا — OTLP عبر gRPC، وBatchSpanProcessor، والاتفاقيات الدلالية لسمات db.* وhttp.*، وأخذ العينات بالذيل في الـ Collector، والـ exemplars في Prometheus — هي بالضبط ما ستوصّله لدى أي صاحب عمل في بيئة التقنية الكبرى. OpenTelemetry الآن الخيار الافتراضي في Google وAWS وMicrosoft وكل منظمة cloud-native رئيسية. حياد SDK من البائع يعني أنك تستطيع مبادلة Jaeger بـ Tempo، أو Tempo بـ Honeycomb، بتغيير سطرين في إعداد الـ Collector — شيفرة تطبيقك غير متأثرة. تلك الإمكانية على النقل هي القيمة الدائمة لبناء منظومة OTel القياسية.

اكتمل الدرس التعليمي: غطّيت قوس قابلية الملاحظة الكامل — من السبب في التتبع (الدرس 1) عبر الامتدادات ونشر السياق (الدرس 2)، ومعيار OpenTelemetry (الدرس 3)، وتجهيز حزمة SDK (الدرس 4)، والـ Collector (الدرس 5)، وخلفيات Jaeger وTempo (الدرس 6)، واستراتيجيات أخذ العينات (الدرس 7)، وسير عمل تصحيح الإنتاج (الدرس 8)، وقابلية الملاحظة الموحدة بالركائز الثلاث (الدرس 9)، وهذا المشروع الشامل (الدرس 10). أنت مجهّز لتصميم وتنفيذ وتشغيل تتبع موزع إنتاجي على نطاق واسع.