التتبع الشامل باستخدام OpenTelemetry

Nicolas Fränkel

Nicolas Fränkel

August 31, 2022

Ecosystem

سواء قمت بتنفيذ بنية الخدمات المصغرة (microservices) أم لا (وربما لا ينبغي عليك ذلك)، فمن المرجح أن نظامك يتكون من مكونات متعددة. أبسط نظام على الأرجح يتكون من وكيل عكسي (reverse proxy)، تطبيق، وقاعدة بيانات. في هذه الحالة، المراقبة ليست مجرد فكرة جيدة؛ بل هي متطلب. كلما زاد عدد المكونات التي قد يمر بها الطلب، زادت قوة هذا المتطلب.

ومع ذلك، فإن المراقبة هي مجرد بداية الرحلة. عندما تبدأ الطلبات في الفشل بشكل جماعي، تحتاج إلى عرض مجمع عبر جميع المكونات. يُطلق على هذا التتبع (tracing)، وهو أحد أركان الملاحظة (observability)؛ والركنان الآخران هما المقاييس (metrics) والسجلات (logs).

في هذه المقالة، سأركز فقط على التتبع وسأصف كيف يمكنك بدء رحلتك نحو الملاحظة.

مواصفات W3C Trace Context

يجب أن توفر حلول التتبع تنسيقًا قياسيًا للعمل عبر أكوام تقنية غير متجانسة. يحتاج هذا التنسيق إلى الالتزام بمواصفات، سواء كانت رسمية أو بحكم الواقع.

يجب أن نفهم أن المواصفات نادرًا ما تظهر من العدم. بشكل عام، يكون السوق لديه بالفعل عدد من التطبيقات المميزة. في معظم الأحيان، تؤدي المواصفات الجديدة إلى تطبيق إضافي، كما تصور الكاريكاتير الشهير XKCD:

الرسوم الكاريكاتورية الشهيرة XKCD تصف

ومع ذلك، في بعض الأحيان تحدث معجزة: يلتزم السوق بالمواصفات الجديدة. هنا، Trace Context هي مواصفات W3C، ويبدو أنها نجحت في ذلك:

تحدد هذه المواصفات رؤوس HTTP القياسية وتنسيق القيمة لنشر معلومات السياق التي تمكن من سيناريوهات التتبع الموزع. توحد المواصفات كيفية إرسال معلومات السياق وتعديلها بين الخدمات. تحدد معلومات السياق بشكل فريد الطلبات الفردية في نظام موزع وتحدد أيضًا وسيلة لإضافة ونشر معلومات السياق الخاصة بالمزود. -- https://www.w3.org/TR/trace-context/

يظهر من الوثيقة مفهومان رئيسيان:

  • التتبع (trace) يتبع مسار طلب يمتد عبر مكونات متعددة
  • الامتداد (span) مرتبط بمكون واحد ومرتبط بامتداد آخر بعلاقة أب-ابن

التتبع والامتدادات

في وقت كتابة هذا المقال، المواصفات هي توصية W3C، وهي المرحلة النهائية.

لدى Trace Context بالفعل العديد من التطبيقات. أحدها هو OpenTelemetry.

OpenTelemetry كمعيار ذهبي

كلما كنت أقرب إلى الجزء التشغيلي من تكنولوجيا المعلومات، زادت فرص سماعك عن OpenTelemetry:

OpenTelemetry هو مجموعة من الأدوات وواجهات برمجة التطبيقات (APIs) وSDKs. استخدمه لتجهيز وإنشاء وجمع وتصدير بيانات المراقبة (المقاييس، السجلات، والتتبع) لمساعدتك في تحليل أداء وسلوك برمجياتك. OpenTelemetry متاح بشكل عام عبر عدة لغات ومناسب للاستخدام. -- https://opentelemetry.io/

OpenTelemetry هو مشروع تديره CNCF. قبل OpenTelemetry، كان هناك مشروعان:

  • OpenTracing، الذي يركز على التتبع كما يوحي اسمه
  • OpenCensus، الذي كان هدفه إدارة المقاييس والتتبع

تم دمج المشروعين وإضافة السجلات على القمة. يقدم OpenTelemetry الآن مجموعة من "الطبقات" التي تركز على الملاحظة:

  • واجهات برمجة التطبيقات للتجهيز في مجموعة متنوعة من اللغات
  • تطبيقات قياسية، مرة أخرى في لغات مختلفة
  • مكونات البنية التحتية مثل المجمعات (collectors)
  • تنسيقات التشغيل البيني، مثل Trace Context من W3C

لاحظ أن OpenTelemetry هو تطبيق لـ Trace Context، ولكنه يفعل أكثر من ذلك. Trace Context يقتصر على HTTP، بينما يسمح OpenTelemetry للامتدادات بالعبور عبر مكونات غير ويب، مثل Kafka. هذا خارج نطاق هذه المقالة.

حالة الاستخدام

حالة الاستخدام المفضلة لدي هي متجر للتجارة الإلكترونية، لذا دعونا لا نغيرها. في هذه الحالة، تم تصميم المتجر حول خدمات مصغرة، كل منها يمكن الوصول إليه عبر واجهة برمجة تطبيقات REST ومحمي خلف بوابة API. لتبسيط البنية للمقالة، سأستخدم خدمتين مصغرتين فقط: catalog تدير المنتجات، وpricing تتعامل مع أسعار المنتجات.

عندما يصل المستخدم إلى التطبيق، تقوم الصفحة الرئيسية بجلب جميع المنتجات، والحصول على أسعارها المعنية، وعرضها.

مثال على تدفق الطلب عبر عدة مكونات

لجعل الأمور أكثر إثارة، catalog هو تطبيق Spring Boot مكتوب بلغة Kotlin، بينما pricing هو تطبيق Flask مكتوب بلغة Python.

يجب أن يسمح لنا التتبع باتباع مسار الطلب عبر البوابة، كلتا الخدمتين المصغرتين، وإذا أمكن، قواعد البيانات.

التتبع عند البوابة

نقطة الدخول هي الجزء الأكثر إثارة في التتبع، حيث يجب أن تولد معرف التتبع: في هذه الحالة، نقطة الدخول هي البوابة. سأستخدم Apache APISIX لتنفيذ العرض التوضيحي:

يوفر Apache APISIX ميزات غنية لإدارة حركة المرور مثل موازنة التحميل، المصدر الديناميكي، الإصدار التجريبي، كسر الدائرة، المصادقة، الملاحظة، إلخ. -- https://apisix.apache.org/

يعتمد Apache APISIX على بنية البرامج المساعدة ويوفر برنامجًا مساعدًا لـ OpenTelemetry:

يمكن استخدام البرنامج المساعد opentelemetry للإبلاغ عن بيانات التتبع وفقًا لمواصفات OpenTelemetry. يدعم البرنامج المساعد فقط الترميز الثنائي لـ OLTP عبر HTTP. -- https://apisix.apache.org/docs/apisix/plugins/opentelemetry/

لنقم بتكوين البرنامج المساعد opentelemetry:

apisix:
  enable_admin: false #1
  config_center: yaml #1
plugins:
  - opentelemetry #2
plugin_attr:
  opentelemetry:
    resource:
      service.name: APISIX #3
    collector:
      address: jaeger:4318 #4
  1. تشغيل Apache APISIX في الوضع المستقل لجعل العرض التوضيحي أسهل في المتابعة. إنها ممارسة جيدة في الإنتاج على أي حال
  2. تكوين opentelemetry كبرنامج مساعد عام
  3. تعيين اسم الخدمة. هذا هو الاسم الذي سيظهر في مكون عرض التتبع
  4. إرسال التتبعات إلى خدمة jaeger. سيتم وصفها في القسم التالي.

نريد تتبع كل مسار، لذا بدلاً من إضافة البرنامج المساعد إلى كل مسار، يجب علينا إعداد البرنامج المساعد كبرنامج مساعد عام:

global_rules:
  - id: 1
    plugins:
      opentelemetry:
        sampler:
          name: always_on #1
  1. للتتبع تأثير على الأداء. كلما زاد التتبع، زاد التأثير. ومن ثم، يجب علينا الموازنة بعناية بين تأثير الأداء وفوائد الملاحظة. ومع ذلك، بالنسبة للعرض التوضيحي، نريد تتبع كل طلب.

جمع، تخزين، وعرض التتبعات

بينما Trace Context هي مواصفات W3C وOpenTelemetry هو معيار بحكم الواقع، هناك العديد من الحلول في السوق لجمع وتخزين وعرض التتبعات. قد توفر كل حل جميع القدرات الثلاث أو جزء منها فقط. على سبيل المثال، تتعامل مجموعة Elastic مع التخزين والعرض، ولكن يجب الاعتماد على شيء آخر لجمع البيانات. من ناحية أخرى، توفر Jaeger وZipkin مجموعة كاملة لتلبية جميع القدرات الثلاث.

Jaeger وZipkin سبقا OpenTelemetry، لذا لكل منهما تنسيق نقل تتبع خاص به. ومع ذلك، يوفر كل منهما تكاملًا مع تنسيق OpenTelemetry.

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

المنافذ ذات الصلة للصورة هي التالية:

المنفذالبروتوكولالمكونالوظيفة
16686HTTPqueryخدمة الواجهة الأمامية
4317HTTPcollectorقبول بروتوكول OpenTelemetry (OTLP) عبر gRPC، إذا تم تمكينه
4318HTTPcollectorقبول بروتوكول OpenTelemetry (OTLP) عبر HTTP، إذا تم تمكينه

يبدو جزء Docker Compose كما يلي:

services:
  jaeger:
    image: jaegertracing/all-in-one:1.37 #1
    environment:
      - COLLECTOR_OTLP_ENABLED=true #2
    ports:
      - "16686:16686" #3
  1. استخدام صورة all-in-one
  2. مهم جدًا: تمكين المجمع بتنسيق OpenTelemetry
  3. تعريض منفذ الواجهة الأمامية

الآن بعد أن قمنا بإعداد البنية التحتية، يمكننا التركيز على تمكين التتبع في تطبيقاتنا.

التتبع في تطبيقات Flask

خدمة pricing هي تطبيق Flask بسيط. توفر نقطة نهاية واحدة لجلب سعر منتج واحد من قاعدة البيانات.

@app.route('/price/<product_str>')                           #1-2
def price(product_str: str) -> Dict[str, object]:
    product_id = int(product_str)
    price: Price = Price.query.get(product_id)               #3
    if price is None:
        return jsonify({'error': 'Product not found'}), 404
    else:
        low: float = price.value - price.jitter              #4
        high: float = price.value + price.jitter             #4
        return {
            'product_id': product_id,
            'price': round(uniform(low, high), 2)            #4
        }
  1. نقطة النهاية
  2. يتطلب المسار معرف المنتج
  3. جلب البيانات من قاعدة البيانات باستخدام SQLAlchemy
  4. محركات التسعير الحقيقية لا تعيد نفس السعر مع مرور الوقت؛ دعنا نعشو السعر قليلاً للمتعة

تحذير: جلب سعر واحد لكل استدعاء غير فعال للغاية. يتطلب ذلك العديد من الاستدعاءات مثل المنتجات، ولكنه يجعل التتبع أكثر إثارة. في الحياة الواقعية، يجب أن تكون نقطة النهاية قادرة على قبول معرفات منتجات متعددة وجلب جميع الأسعار المرتبطة في طلب-استجابة واحد.

حان الوقت الآن لتجهيز التطبيق. هناك خياران متاحان: التجهيز التلقائي والتجهيز اليدوي. التجهيز التلقائي هو جهد منخفض وفوز سريع؛ التجهيز اليدوي يتطلب وقت تطوير مركز. أنصح بالبدء بالتجهيز التلقائي وإضافة التجهيز اليدوي فقط إذا لزم الأمر.

نحتاج إلى إضافة بضعة حزم Python:

opentelemetry-distro[otlp]==0.33b0
opentelemetry-instrumentation
opentelemetry-instrumentation-flask

نحتاج إلى تكوين بضعة معلمات:

pricing:
  build: ./pricing
  environment:
    OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1
    OTEL_RESOURCE_ATTRIBUTES: service.name=pricing #2
    OTEL_METRICS_EXPORTER: none #3
    OTEL_LOGS_EXPORTER: none #3
  1. إرسال التتبعات إلى Jaeger
  2. تعيين اسم الخدمة. هذا هو الاسم الذي سيظهر في مكون عرض التتبع
  3. نحن غير مهتمين بالسجلات أو المقاييس

الآن، بدلاً من استخدام الأمر القياسي flask run، نقوم بلفه:

opentelemetry-instrument flask run

فقط مع هذا، نجمع بالفعل الامتدادات من استدعاءات الطرق ونقاط نهاية Flask.

يمكننا يدويًا إضافة امتدادات إضافية إذا لزم الأمر، على سبيل المثال:

from opentelemetry import trace

@app.route('/price/<product_str>')
def price(product_str: str) -> Dict[str, object]:
    product_id = int(product_str)
    with tracer.start_as_current_span("SELECT * FROM PRICE WHERE ID=:id", attributes={":id": product_id}) as span: #1
        price: Price = Price.query.get(product_id)
    # ...
  1. إضافة امتداد إضافي مع التسمية والسمة المكونة

التتبع في تطبيقات Spring Boot

خدمة catalog هي تطبيق Spring Boot تفاعلي تم تطويره بلغة Kotlin. توفر نقطتي نهاية:

  • واحدة لجلب منتج واحد
  • الأخرى لجلب جميع المنتجات

كلاهما يبحث أولاً في قاعدة بيانات المنتجات، ثم يستعلم عن خدمة pricing أعلاه للحصول على السعر.

كما هو الحال في Python، يمكننا الاستفادة من التجهيز التلقائي واليدوي. لنبدأ بالفاكهة السهلة، التجهيز التلقائي. على JVM، نحقق ذلك من خلال وكيل:

java -javaagent:opentelemetry-javaagent.jar -jar catalog.jar

كما في Python، فإنه ينشئ امتدادات لكل استدعاء طريقة ونقطة دخول HTTP. كما أنه يجهز استدعاءات JDBC، ولكن لدينا كومة تفاعلية وبالتالي نستخدم R2DBC. للإشارة، هناك قضية GitHub مفتوحة لإضافة الدعم.

نحتاج إلى تكوين السلوك الافتراضي:

catalog:
  build: ./catalog
  environment:
    APP_PRICING_ENDPOINT: http://pricing:5000/price
    OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1
    OTEL_RESOURCE_ATTRIBUTES: service.name=orders #2
    OTEL_METRICS_EXPORTER: none #3
    OTEL_LOGS_EXPORTER: none #3
  1. إرسال التتبعات إلى Jaeger
  2. تعيين اسم الخدمة. هذا هو الاسم الذي سيظهر في مكون عرض التتبع
  3. نحن غير مهتمين بالسجلات أو المقاييس

كما هو الحال في Python، يمكننا رفع مستوى اللعبة بإضافة التجهيز اليدوي. هناك خياران متاحان، البرمجي والقائم على التعليقات التوضيحية. الأول معقد قليلاً ما لم نقدم Spring Cloud Sleuth. دعنا نضيف التعليقات التوضيحية.

نحتاج إلى تبعية إضافية:

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-instrumentation-annotations</artifactId>
    <version>1.17.0-alpha</version>
</dependency>

كن حذرًا، تم نقل الأداة مؤخرًا من io.opentelemetry:opentelemetry-extension-annotations.

يمكننا الآن تعليق الكود الخاص بنا:

@WithSpan("ProductHandler.fetch")                                               //1
suspend fun fetch(@SpanAttribute("id") id: Long): Result<Product> {             //2
    val product = repository.findById(id)
    return if (product == null) Result.failure(IllegalArgumentException("Product $id not found"))
    else Result.success(product)
}
  1. إضافة امتداد إضافي مع التسمية المكونة
  2. استخدام المعلمة كسمة، مع تعيين المفتاح إلى id والقيمة هي قيمة المعلمة في وقت التشغيل

النتيجة

يمكننا الآن اللعب مع العرض التوضيحي البسيط لرؤية النتيجة:

curl localhost:9080/products
curl localhost:9080/products/1

الاستجابات ليست مثيرة للاهتمام، ولكن دعونا ننظر إلى واجهة Jaeger. نجد كلا التتبعات، واحد لكل استدعاء:

قائمة التتبعات في واجهة Jaeger

يمكننا الغوص في الامتدادات لتتبع واحد:

جميع الامتدادات التي تشكل تتبعًا واحدًا

لاحظ أنه يمكننا استنتاج تدفق التسلسل دون مخطط UML أعلاه. الأفضل من ذلك، يعرض التسلسل الاستدعاءات الداخلية للمكون.

كل امتداد يحتوي على السمات التي أضافها التجهيز التلقائي وتلك التي أضفناها يدويًا:

سمات الامتداد

الخلاصة

في هذه المقالة، عرضت التتبع من خلال متابعة طلب عبر بوابة API، تطبيقين يعتمدان على أكوام تقنية مختلفة، وقواعد البيانات الخاصة بهما. لقد لمست فقط سطح التتبع: في العالم الحقيقي، قد يتضمن التتبع مكونات غير مرتبطة بـ HTTP، مثل Kafka وقوائم الانتظار.

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

يمكن العثور على الكود المصدري الكامل لهذه المقالة على GitHub.

للمضي قدمًا:

Tags: