يا جماعة الخير، السلام عليكم ورحمة الله.
اسمحوا لي اليوم أحكي لكم قصة صارت معي قبل كم سنة، قصة علّمتني درس ما بنساه. كنا في الشركة شغالين على إطلاق منصة تجارة إلكترونية جديدة، وكلها مبنية على معمارية الخدمات المصغرة (Microservices). كان عنا خدمة للمنتجات، وخدمة للسلة، وخدمة للطلبات، وخدمة للدفع، وغيرهم كثار… الوضع كان تمام والكل مبسوط بالإنجاز.
إجا اليوم الموعود، يوم “الجمعة البيضاء”، والضغط على الموقع كان هائل. فجأة، بدأت توصلنا شكاوى من المستخدمين: “دفعت الفلوس وانسحبت من حسابي، بس الطلب مش موجود في حسابي!”. ندخل على نظام إدارة الطلبات، وفعلاً، الطلب مش موجود. نروح على سجلات خدمة الدفع، نلاقي عملية الدفع ناجحة. نروح على سجلات خدمة الطلبات، ما في أثر للطلب هاد بالذات. ضاعت الطاسة، زي ما بنحكي.
قعدنا أيام وليالي نحاول نربط السجلات (logs) بين الخدمات المختلفة. كل خدمة بتسجل بمعزل عن الثانية، وكمية السجلات كانت مرعبة. كنا زي اللي بدور على إبرة في كومة قش، بل في عدة أكوام قش! كل فريق يتهم الفريق الثاني، والضغط من الإدارة كان يزيد. “وين راحت الطلبات يا أبو عمر؟ شو القصة؟”. شعرت وقتها بعجز حقيقي، وبدأ الشك يتسلل لنفسي: هل كانت فكرة الخدمات المصغرة قراراً صائباً أصلاً؟
ومن رحم هذه المعاناة، وُلد الحل الذي أنقذنا، والذي أصبح اليوم جزءاً لا يتجزأ من أي نظام أقوم ببنائه: التتبع الموزع (Distributed Tracing).
لماذا تفشل الطرق التقليدية في عالم الخدمات المصغرة؟
في الأنظمة القديمة المتجانسة (Monolithic)، كانت الحياة أبسط. كل الكود موجود في مكان واحد. لما يصير خطأ، بتفتح ملف السجلات (log file)، وبتقرأ التسلسل الزمني للأحداث من فوق لتحت، وبتعرف وين المشكلة. بسيط ومباشر.
لكن مع الخدمات المصغرة، اختلف الوضع تماماً. الطلب الواحد من المستخدم (مثلاً، إتمام عملية شراء) ممكن يمر على 5 أو 10 أو حتى 20 خدمة مختلفة قبل ما يكتمل. كل خدمة لها سجلاتها الخاصة، وسيرفراتها الخاصة، وقواعد بياناتها الخاصة. لما تحدث مشكلة، السؤال بصير:
- في أي خدمة حدث الخطأ؟
- ما هو المسار الكامل الذي سلكه هذا الطلب تحديداً عبر الخدمات؟
- أي خدمة هي التي تسببت في بطء الاستجابة؟ هل هي خدمة (أ) أم خدمة (ب) التي نادتها؟
محاولة الإجابة على هذه الأسئلة باستخدام السجلات التقليدية وحدها هو جحيم حقيقي. وهنا يأتي دور بطل قصتنا.
ما هو التتبع الموزع (Distributed Tracing)؟
بكل بساطة، تخيل أن كل طلب يدخل نظامك هو طرد بريدي. التتبع الموزع هو نظام التتبع الذي يتيح لك رؤية رحلة هذا الطرد من لحظة إرساله، مروراً بكل محطات الفرز والتوزيع، حتى وصوله إلى وجهته النهائية. لو ضاع الطرد أو تأخر، يمكنك بسهولة رؤية آخر محطة كان فيها وتحديد مكان المشكلة.
تقنياً، يقوم التتبع الموزع على ربط كل العمليات التي تشكل طلباً واحداً عبر الخدمات المختلفة باستخدام مُعرّفات فريدة. دعونا نفهم المكونات الأساسية:
- Trace: يمثل الرحلة الكاملة للطلب عبر جميع الخدمات. له مُعرّف فريد يسمى
Trace ID. - Span: يمثل عملية واحدة محددة داخل هذه الرحلة (مثلاً، استدعاء HTTP لخدمة الدفع، أو استعلام لقاعدة البيانات). كل Span له مُعرّف فريد خاص به (
Span ID) ويحتوي أيضاً علىTrace IDلربطه بالرحلة الكاملة. - Parent-Child Relationship: كل Span (باستثناء الأول) يكون له أب (Parent Span). هذا يسمح لنا ببناء شجرة هرمية تظهر كيف استدعت العمليات بعضها البعض.
- Context Propagation: هذه هي الآلية السحرية. عندما تقوم خدمة (أ) باستدعاء خدمة (ب)، فإنها تقوم بتمرير “السياق” (Context) الذي يحتوي على
Trace IDوSpan IDالخاص بالأب، غالباً عبر هيدرز الـ HTTP (مثلtraceparent). خدمة (ب) تقرأ هذا السياق وتعرف أنها جزء من رحلة أكبر، فتقوم بإنشاء Span جديد خاص بها وتربطه بالأب.
مثال لتوضيح الفكرة
- مستخدم يضغط “إتمام الشراء”.
- خدمة الواجهة الأمامية (API Gateway) تستقبل الطلب. هنا تبدأ الرحلة:
- يتم إنشاء
Trace IDفريد (مثلاً:abc-123). - يتم إنشاء أول Span (الأب) لهذه العملية (مثلاً:
span-A).
- يتم إنشاء
- الـ API Gateway يستدعي خدمة الطلبات (Order Service) لإنشاء طلب جديد.
- يتم تمرير
Trace ID: abc-123وParent ID: span-Aفي هيدر الطلب.
- يتم تمرير
- خدمة الطلبات تستقبل الطلب، وتقرأ السياق:
- تنشئ Span جديداً خاصاً بها (
span-B) مع نفس الـTrace IDوتجعلspan-Aأباه. - تقوم بحفظ الطلب في قاعدة البيانات (وهذه يمكن أن تكون Span فرعية أخرى!).
- تنشئ Span جديداً خاصاً بها (
- خدمة الطلبات تستدعي خدمة الدفع (Payment Service):
- تمرر
Trace ID: abc-123وParent ID: span-Bفي الهيدر.
- تمرر
- خدمة الدفع تقوم بمعالجة الدفع وتُرجع النتيجة.
كل هذه الـ Spans (A, B, C…) تُرسَل إلى نظام مركزي يقوم بتجميعها وعرضها لك على شكل مخطط زمني (Gantt chart)، يوضح لك مسار الطلب بدقة، وكم من الوقت استغرقت كل خطوة.
نصيحة من أبو عمر: لا تفكر في التتبع الموزع كأداة لتحليل الأخطاء فقط، بل هو أداة لفهم أداء النظام. من خلاله، ستكتشف عنق الزجاجة (bottlenecks) والمشاكل الكامنة قبل أن تتحول إلى كوارث.
أدوات الشغل: كيف نبدأ؟
في الماضي، كان لكل شركة (مثل Google, Uber) نظامها الخاص. لكن اليوم، المجتمع التقني اتفق على معيار موحد ومفتوح المصدر هو OpenTelemetry (OTel). هذا هو الخيار الذي أنصح به بشدة اليوم.
OpenTelemetry يوفر لك مجموعة من المكتبات (SDKs) لكل لغات البرمجة تقريباً. مهمتها هي:
- Instrumentation: “تعديل” مكتباتك الشائعة (مثل Express, Flask, HttpClient, JDBC) بشكل تلقائي لإنشاء الـ Spans وتمرير السياق دون تدخل كبير منك.
- Data Exporting: إرسال بيانات التتبع (Traces)، والمقاييس (Metrics)، والسجلات (Logs) إلى أي نظام خلفي (Backend) تريده.
أما بالنسبة للأنظمة الخلفية التي تستقبل وتخزن وتعرض هذه البيانات، فأشهر الخيارات مفتوحة المصدر هي:
- Jaeger: بدأ في شركة Uber، وهو قوي جداً ومشهور.
- Zipkin: بدأ في تويتر، وهو أيضاً خيار ممتاز وبسيط.
وهناك أيضاً حلول سحابية مدفوعة مثل Datadog, New Relic, Honeycomb وغيرها.
مثال عملي بسيط باستخدام Node.js و OpenTelemetry
لنفترض أن لدينا خدمة بسيطة مكتوبة بـ Node.js و Express. انظر كم هو سهل إضافة التتبع الموزع الأساسي.
أولاً، نقوم بإنشاء ملف إعداد لـ OpenTelemetry (مثلاً tracing.js):
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
// إعداد الـ Exporter الذي سيرسل البيانات إلى Jaeger
// يمكن استبداله بـ ConsoleSpanExporter للطباعة في الكونسول فقط
const exporter = new JaegerExporter({
// يمكن تكوين endpoint هنا
// endpoint: 'http://localhost:14268/api/traces',
});
const sdk = new NodeSDK({
// بدلاً من ConsoleSpanExporter، نستخدم JaegerExporter
traceExporter: exporter,
// هذه هي الخطوة السحرية: تقوم بتفعيل التتبع التلقائي لمكتبات Node.js الشائعة
instrumentations: [getNodeAutoInstrumentations()],
});
// بدء تشغيل الـ SDK
sdk.start();
console.log('Tracing initialized');
// للتعامل مع إغلاق العملية بأمان
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
الآن، كل ما عليك فعله هو تشغيل هذا الملف قبل تشغيل تطبيقك الرئيسي:
node -r ./tracing.js your-main-app.js
هذا كل شيء! بمجرد القيام بذلك، ستقوم مكتبة OpenTelemetry تلقائياً بالآتي:
- لكل طلب HTTP وارد إلى سيرفر Express، ستقوم بإنشاء Span جديد.
- إذا كان الطلب يحتوي على هيدر تتبع (قادم من خدمة أخرى)، فستقوم بربط الـ Span الجديد به.
- عندما يقوم كودك بإجراء طلب HTTP خارجي (باستخدام
httpأوaxiosمثلاً)، ستقوم تلقائياً بحقن هيدرز التتبع في الطلب الصادر.
الآن، عندما تذهب إلى واجهة Jaeger، سترى رسماً بيانياً جميلاً يوضح لك رحلة كل طلب عبر خدماتك المختلفة، مع أزمنة دقيقة لكل خطوة.
الخلاصة: من الظلام إلى النور
التتبع الموزع ليس مجرد أداة تقنية فاخرة، بل هو تغيير جذري في طريقة تفكيرنا وتصحيحنا للأخطاء في الأنظمة الحديثة. إنه ينقلك من التخمين والبحث اليائس في أكوام السجلات، إلى رؤية واضحة ومبنية على البيانات لمسار كل طلب في نظامك.
بالنسبة لقصتي، بعد أن طبقنا التتبع الموزع (استخدمنا Jaeger وقتها)، استغرقنا أقل من 10 دقائق لتحديد المشكلة. اكتشفنا أن خدمة الدفع كانت أحياناً تتأخر في الرد لأكثر من 30 ثانية، وكانت خدمة الطلبات لديها مهلة زمنية (timeout) أقصر، فتفشل وتلغي العملية قبل أن يصلها الرد الإيجابي من خدمة الدفع. لولا التتبع الموزع، لربما بقينا نبحث عن السبب لأسابيع.
نصيحتي الأخيرة لك: لا تنتظر حتى تقع الكارثة. إذا كنت تبني أو تدير نظاماً يعتمد على الخدمات المصغرة، فاجعل “قابلية المراقبة” (Observability) – والتي يعتبر التتبع الموزع أحد أركانها الثلاثة مع السجلات والمقاييس – أولوية قصوى من اليوم الأول. ستشكر نفسك لاحقاً على هذا القرار. 🚀
أتمنى لكم كل التوفيق في مشاريعكم، وإذا عندكم أي سؤال، أنا جاهز.