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

استراتيجيات أخذ العينات

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

استراتيجيات أخذ العينات

لا يستطيع نظام إنتاج يعالج 100,000 طلب في الثانية أن يسجل كل span لكل طلب. قد يولد trace واحد لاستدعاء نموذجي بين الخدمات المصغرة 30-50 span؛ وبدقة كاملة يعني ذلك 3-5 ملايين سجل span في الثانية، قبل أن تحتسب النطاق الترددي للاستيعاب وعبء المعالج لعمليات التسلسل والتصدير على كل pod تطبيق. أخذ العينات هو الطريقة التي تجعل التتبع الموزع مجدياً اقتصادياً على نطاق واسع دون فقدان الـ traces المهمة فعلاً.

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

أخذ العينات من الرأس (Head-Based Sampling)

أخذ العينات من الرأس يتخذ قرار الاحتفاظ أو الإسقاط في بداية الـ trace تماماً — عند أول span، قبل حدوث أي معالجة في المراحل اللاحقة. يُشفَّر القرار في حقل sampled ضمن ترويسة W3C Trace Context ويُنقل إلى كل خدمة في سلسلة الاستدعاء. كل خدمة تحترم هذا الحقل: إذا أُخذت عينة من الـ span الجذر، تُسجَّل كل فروعه؛ وإذا لم تُؤخذ، تتخطى كل خدمة الأداة لذلك الطلب تماماً.

الشكل الأكثر شيوعاً هو أخذ العينات الاحتمالي (المعدل): أخذ عينات من 1% أو 5% من جميع الطلبات الواردة عشوائياً. هذا رخيص بشكل تافه — مقارنة رقم عشوائي واحد لكل طلب — وينتج عرضاً إحصائياً تمثيلياً لحركة مرورك.

العيب المميت: سيُسقط محدد العينات بنسبة 1% نسبة 99% من أخطاء 500 ونسبة 99% من ارتفاعات زمن الاستجابة p99.9. عند 100k طلب في الثانية مع معدل خطأ 0.01% (100 خطأ/ثانية)، ستشهد إحصائياً trace خطأ واحداً فقط في الثانية.

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

أخذ العينات من الذيل (Tail-Based Sampling)

أخذ العينات من الذيل يخزن trace كاملاً في الذاكرة، ينتظر وصول جميع الـ spans، يقيّم الـ trace الكامل وفق سياسة، ثم يقرر الاحتفاظ به أو إسقاطه. لأن القرار يحدث بعد معرفة النتيجة، يمكنك تطبيق سياسات تلتقط فعلاً ما يهم:

  • أخذ عينات دائم للأخطاء — أي trace يحتوي على span بـ status.code = ERROR يُحتفظ به بنسبة 100%.
  • أخذ عينات بناءً على عتبة زمن الاستجابة — احتفظ بكل الـ traces التي يتجاوز إجمالي مدتها عتبة محددة.
  • تحديد المعدل لكل مسار — احتفظ بـ N trace كحد أقصى في الثانية لكل endpoint.
  • سياسات مركبة — ادمج قواعد متعددة: دائمة للأخطاء، مبنية على زمن الاستجابة للبطيئة، احتمالية لما تبقى.

التكلفة معمارية: أخذ العينات من الذيل يتطلب طبقة تجميع ذات حالة (stateful). يجب توجيه جميع الـ spans لـ traceId معين إلى نفس instance المجمع — لا يمكن توزيعها عبر مجمعات عديمة الحالة. يُطبَّق هذا عبر التوجيه بمعرف الـ trace: موازن تحميل أمام طبقة مجمعك يجزئ traceId إلى شظية collector محددة.

Head vs Tail Sampling Decision Points HEAD-BASED SAMPLING Client Request API Gateway DECISION HERE DROP (99%) blind — no outcome known KEEP (1%) Service A honors flag Service B honors flag TAIL-BASED SAMPLING Service A emits spans Service B emits spans Service C emits spans TraceID Router hash(traceId) → shard Collector Shard buffer 30-60s eval policy: error? slow? rare? DECISION HERE DROP (informed) Backend Jaeger / Tempo
أخذ العينات من الرأس يقرر عند نقطة الدخول (بشكل أعمى)؛ أخذ العينات من الذيل يخزن جميع الـ spans ويقرر بعد اكتمال الـ trace.

ضبط أخذ العينات من الذيل في OTel Collector

معالج tailsampling في OpenTelemetry Collector ينفذ هذا النمط. فيما يلي إعداد تمثيلي للإنتاج: الاحتفاظ دائماً بالأخطاء والـ traces الأبطأ من ثانية واحدة، وتحديد معدل الـ traces الصحية بـ 10 في الثانية، واستخدام نافذة تخزين مؤقت لمدة 30 ثانية للـ spans المتأخرة.

processors: tail_sampling: decision_wait: 30s # نافذة التخزين المؤقت — انتظر هذه المدة لجميع الـ spans num_traces: 200000 # حجم مخزن الـ trace في الذاكرة expected_new_traces_per_sec: 5000 policies: - name: always-sample-errors type: status_code status_code: { status_codes: [ERROR] } - name: slow-traces type: latency latency: { threshold_ms: 1000 } - name: low-volume-endpoints type: string_attribute string_attribute: key: http.route values: ["/admin/*", "/internal/*"] enabled_regex_matching: true - name: probabilistic-baseline type: probabilistic probabilistic: { sampling_percentage: 2 } - name: composite-policy type: and and: and_sub_policy: - name: not-health-check type: string_attribute string_attribute: key: http.route values: ["/healthz", "/readyz"] invert_match: true - name: service-rate-limit type: rate_limiting rate_limiting: { spans_per_second: 200 } service: pipelines: traces: receivers: [otlp] processors: [tail_sampling, batch] exporters: [jaeger]
نصيحة متقدمة: اضبط decision_wait على ضعف p99 لأطول استدعاء بين خدماتك على الأقل. إذا كان استعلام قاعدة البيانات يستغرق أحياناً 20 ثانية، تضمن نافذة 30 ثانية وصول الـ span قبل اتخاذ القرار. راقب مقياس otelcol_processor_tail_sampling_late_span_goes_to_new_decision بانتظام.

Load-Balancing Exporter: التوجيه بمعرف الـ Trace

لأن أخذ العينات من الذيل يتطلب وجود جميع الـ spans لـ trace ما على شظية واحدة، تحتاج إلى طبقة موازنة تحميل تُوجِّه بـ traceId لا بالاتصال أو Round-Robin. loadbalancingexporter في OTel Collector يفعل هذا بالضبط — يجلس في طبقة "router" تستقبل الـ spans من جميع الخدمات وتُعيدها إلى مجموعة ثابتة من instances collector عبر التجزئة المتسقة على معرف الـ trace.

# collector-router.yaml (الطبقة الأمامية — عديمة الحالة، واحدة لكل AZ) exporters: loadbalancing: routing_key: traceID # تجزئة متسقة على traceId protocol: otlp: timeout: 1s tls: { insecure: false } resolver: dns: hostname: otelcol-sampler.observability.svc.cluster.local port: 4317 interval: 5s # إعادة استيفاء عند تحجيم الـ pods service: pipelines: traces: receivers: [otlp] exporters: [loadbalancing]

أخذ العينات على نطاق واسع: أنماط فشل الإنتاج

عدة أنماط فشل تؤلم الفرق التي اختبرت أخذ العينات من الذيل على أحمال صغيرة فقط:

  • فيضان المخزن المؤقت: إذا كان num_traces صغيراً جداً وتوافدت ذروة حركة مرور، يُخلي المجمع الـ traces الأقدم قبل اتخاذ القرار — يسقط بصمت الـ traces التي قد تكشف السبب الجذري. راقب otelcol_processor_tail_sampling_sampling_decision_timer_fired وdropped_spans.
  • النقاط الساخنة في الشظايا: التجزئة المتسقة توزع معرفات الـ trace بالتساوي من الناحية النظرية، لكن خدمة تولد عدداً عالياً جداً من الـ spans لكل trace يمكن أن تُثقل شظية واحدة.
  • ضغط الذاكرة: كل span مخزن يشغل heap على المجمع. احرص على تحجيم pods المجمع بشكل مناسب.
  • إعادة تشغيل المجمع تُفقد القرارات الجارية: إعادة التشغيل المتدحرجة لـ pods الشظية في منتصف نافذة القرار تُنتج "دماغاً منقسماً" — بعض الـ spans للـ trace ذاته يذهب إلى الـ pod القديم وبعضها إلى الجديد.
مخاطرة إنتاجية: لا تُشغّل أبداً أخذ العينات من الذيل على نفس instance المجمع الذي يتعامل أيضاً مع المقاييس والسجلات. المخزن المؤقت للذاكرة لقرارات الـ trace يتنافس مع تجميع المقاييس ودفع السجلات. في الإنتاج، افصل fleet المجمعات إلى طبقات متخصصة: طبقة router (عديمة الحالة)، طبقة sampler (ذات حالة)، وطبقة تصدير (دفعية إلى الـ backends).

الاستراتيجية الهجينة: الرأس والذيل معاً في الممارسة

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

  1. أخذ عينات من الرأس على جانب العميل في SDK: إسقاط endpoints فحص الصحة (/healthz، /readyz) كلياً بنسبة 100% — هذه تولد حجماً هائلاً من الـ spans دون أي قيمة تشخيصية. استخدم ParentBased sampler حتى تحترم الخدمات اللاحقة قرار الخدمة الأعلى.
  2. أخذ عينات احتمالي من الرأس في مجمع طبقة Router بنسبة 20-50% — هذا يقلل البيانات التي يجب على طبقة الـ sampler تخزينها مؤقتاً.
  3. تقييم سياسة أخذ العينات من الذيل في طبقة الـ sampler: احتفظ بـ 100% من الأخطاء والـ traces البطيئة من الـ 20-50% المصفاة مسبقاً، وطبّق تحديد المعدل على الباقي.

النتيجة: حجم الـ trace الإجمالي المرسل إلى Backend قد يكون 0.5-2% من حجم الطلبات الخام، لكن ضمن هذه النسبة الصغيرة لديك تغطية شبه 100% لجميع الأخطاء وحالات الشذوذ في زمن الاستجابة.

فكرة أساسية: هدف أخذ العينات ليس توفير المال — بل الاحتفاظ بالـ traces الصحيحة. استراتيجية أخذ العينات الجيدة هي التي كل trace أسقطته كنت لن تحتاجه، وكل trace احتجته احتفظت به. صمّم سياساتك حول هذا التعريف، وقس نجاحها بمعدل التقاط الـ traces الخاطئة كمؤشر KPI.