الخدمات المصغّرة

:الفئات

أعادت أمازون تنظيم هندستها التقنية حول قاعدة واحدة: كل فريق يتواصل عبر واجهات برمجة التطبيقات (APIs) فقط. لا قواعد بيانات مشتركة، ولا استدعاءات مباشرة بين قواعد الأكواد، ولا تنسيق إصدارات بين الفرق. إذا بنيت خدمة، فأنت من يملكها. وإذا تعطلت في الساعة الثالثة فجرًا، فأنت من يتلقى التنبيه. النتيجة مئات الفرق التي تُطلق إصداراتها بشكل مستقل، وشركة قادرة على النشر آلاف المرات يوميًا. هذا ما تتيحه بنية الخدمات المصغّرة: تطبيق مُقسّم إلى خدمات صغيرة قابلة للنشر بشكل مستقل، كل واحدة مسؤولة عن قدرة عملية واحدة ومتصلة بواجهات برمجة تطبيقات محددة بوضوح. الحصول على البنية الصحيحة هو ما يفرّق بين فرق الهندسة التي تتحرك بسرعة وتلك التي تتباطأ مع نمو حجم العمل.

ما هي بنية الخدمات المصغّرة؟
لماذا تُعدّ الخدمات المصغّرة مهمة عند التوسّع؟
ما هي أفضل ممارسات الخدمات المصغّرة؟
ما هي التحديات الشائعة للخدمات المصغّرة؟
كيف تبني Digital Bunch بالخدمات المصغّرة؟

ما هي بنية الخدمات المصغّرة؟

البديل عن الخدمات المصغّرة هو البنية الأحادية (Monolith): قاعدة كود واحدة، ونشر واحد، وقاعدة بيانات واحدة. في المراحل الأولى من المشروع، تكون هذه البساطة مفيدة بالفعل. يمكن لفريق صغير أن يستوعب النظام بالكامل في ذهنه. النشر هو أمر واحد فقط. وتعمل أنظمة ناجحة كثيرة بهذه الطريقة ولا تحتاج إلى أي شيء آخر أبدًا.

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

ما يجعل هذا النمط ذا قيمة هو إمكانية النشر المستقل. يقوم فريق الفوترة بالنشر عندما يكون كوده جاهزًا. ويفعل فريق البحث الشيء نفسه. لا حاجة لمواءمة نوافذ الإصدار، ولا انتظار لانتهاء فرق أخرى. كما تملك كل خدمة بياناتها الخاصة، لذا لا يمكن لتغيير في مخطط قاعدة بيانات إحدى الخدمات أن يكسر خدمة أخرى. التداعيات العملية لكيفية تواصل الخدمات عبر واجهات برمجة التطبيقات مشروحة في تكامل واجهة برمجة التطبيقات (API)، والتفكير متعدد الطبقات الذي يتطلبه ذلك يرتبط بكيفية تنظيم فرق التطوير الشامل (Full-Stack) لعملها.

لماذا تُعدّ الخدمات المصغّرة مهمة عند التوسّع؟

تخلق قواعد الأكواد المشتركة احتكاكًا متزايدًا مع نمو الفرق. تتضاعف تعارضات الدمج (merge conflicts)، وتُسبب التغييرات في منطقة ما أعطالًا في مناطق غير متعلقة بها، وتتطلب الإصدارات تزامنًا بين الفرق. يتباطأ التطوير مع زيادة عدد الموظفين، وهو عكس ما تتوقعه المؤسسات.

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

يعمل التوسّع (scaling) بشكل مختلف أيضًا مع الخدمات المصغّرة. تتوسّع البنية الأحادية كوحدة واحدة: فإذا ارتفع الطلب على البحث خلال عرض ترويجي، عليك تزويد كل شيء بموارد إضافية، بما في ذلك عملية الدفع التي تبقى خاملة. أما مع الخدمات المنفصلة، فأنت توسّع فقط ما يتعرض لضغط حقيقي. تُشغّل Lyft أكثر من 100 خدمة مصغّرة على AWS لهذا السبب بالذات، حيث تتوسّع كل خدمة استجابةً للطلب الفعلي بدلًا من حمل عبء الخدمات المجاورة.

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

ما هي أفضل ممارسات الخدمات المصغّرة؟

الخطأ الأكثر شيوعًا في تصميم الخدمات هو تنظيمها حول الوظيفة التقنية بدلًا من مجال العمل. فخدمة قاعدة بيانات أو طبقة تسجيل لا تفصل المسؤوليات فعليًا. مفهوم "السياقات المحدودة" (bounded contexts) في التصميم المُوجَّه بالمجال (Domain-Driven Design) أكثر فائدة: حدّد أين ينقسم عملك بشكل طبيعي، مثل الطلبات، والمخزون، وحسابات العملاء، وارسم حدود الخدمات هناك. وهذه الحدود تتوافق عادةً مع الطريقة التي تفكر بها الفرق بالملكية أصلًا.

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

الفشل مُدخل تصميمي، لا فكرة لاحقة. استدعاءات الشبكة بين الخدمات تفشل. الخدمات البطيئة في الأسفل تُبقي الخيوط (threads) مشغولة، وتبعية واحدة مشبعة يمكنها إيقاف طوابير الطلبات في كل خدمة تستدعيها. تساعد قواطع الدارة (Circuit breakers) من خلال إيقاف الاستدعاءات لخدمة متعطلة بدلًا من ترك الطلبات تنتظر في طابور حتى انتهاء المهلة. ممارسات DevOps المتعلقة بالنشر الآلي والتراجع عن الإصدارات تجعل النظام بأكمله أكثر مرونة عند تعطل الخدمات.

تحتاج كل خدمة إلى خط أنابيب نشر (deployment pipeline) خاص بها. خطوط الأنابيب المشتركة التي تجمع عدة خدمات معًا تُعيد إنتاج مشكلة التنسيق التي كان من المفترض أن تحلها الخدمات المصغّرة. يحتاج التوثيق (authentication) بين الخدمات أيضًا إلى آلية عديمة الحالة (stateless). JWT (رمز JSON المُوقّع) هو الأسلوب المعياري، لأنه يتيح للخدمات التحقق من الهوية دون تشارك حالة الجلسة.

قابلية الرصد (Observability) ليست اختيارية. التتبّع الموزّع (distributed tracing)، والتسجيل المركزي، ونقاط فحص الصحة (health endpoints) يجب أن تكون جزءًا من أول خدمة تبنيها، لا أن تُضاف بعد أول حادثة في بيئة الإنتاج. فعندما يفشل طلب بعد مروره بعشرات الخدمات، تحتاج إلى معرفة أين حدث الخلل دون فحص سجلات كل خدمة على حدة.

ما هي التحديات الشائعة للخدمات المصغّرة؟

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

أنماط الفشل مختلفة أيضًا. في البنية الأحادية، يُفشل الخطأ دالّة واحدة فقط. في نظام موزّع، تُبقي خدمة بطيئة الخيوط مشغولة في كل خدمة فوقها. وإذا تراكم ذلك بما يكفي، يمكن أن تنتشر الأعطال المتسلسلة عبر خدمات لا يوجد خلل في كودها الخاص. كما يصبح اتساق البيانات أكثر صعوبة: لا توجد معاملة موزّعة (distributed transaction) تتراجع عبر حدود الخدمات إذا فشل شيء في منتصف عملية ما. حلول مثل معاملات الساغا (saga) والطوابير المُوجَّهة بالأحداث تعالج هذا الأمر، لكنها تضيف سطح تنفيذ إضافيًا.

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

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

كيف تبني Digital Bunch بالخدمات المصغّرة؟

في Digital Bunch، صممنا بنيات خدمات مصغّرة لعملاء في قطاعات التكنولوجيا المالية، وتطوير تطبيقات الهاتف، والبرمجيات المؤسسية. هذه بيئات يكون فيها لاستقلالية النشر والتوسّع المستهدف تأثير مباشر على الأعمال، وحيث يخلق الخطأ في رسم الحدود مشكلات تستغرق أشهرًا لإصلاحها.

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

تُدمج قابلية الرصد مع أول خدمة: التتبّع الموزّع، والتسجيل المركزي مع معرّفات الربط (correlation IDs)، وخطوط أنابيب نشر خاصة بكل خدمة، ونقاط فحص صحة متصلة بالتعافي الآلي. وللفرق التي ترث بنى أحادية قائمة، نحدد الأجزاء التي تُولّد عبء تنسيق أو توسّع حقيقيًا، ثم نستخرج تلك الخدمات أولًا. عمليات الهجرة الشاملة التي تحاول تفكيك كل شيء في الوقت نفسه نادرًا ما تكتمل.

تراودك أسئلة؟

تدور على حلول واضحة؟ خلينا نتكلم عن كيف خبرتنا تقدر تفيدك.