MAISON CODE .
/ Tech · Architecture · Microservices · Backend · Distributed Systems

الخدمات المصغرة: فخ متراصة الموزعة

إن تقسيم تطبيقك إلى 10 خدمات لا يجعله أسرع. يجعل التصحيح أكثر صعوبة. دليل فني للأحجار المتراصة المعيارية، والتصميم المعتمد على المجال (DDD)، ونمط الملحمة.

AB
Alex B.
الخدمات المصغرة: فخ متراصة الموزعة

في عام 2015، كان شعار الصناعة مطلقًا: “الوحدات المتراصة هي ديناصورات. والخدمات الصغيرة هي المستقبل”. بحلول عام 2025، بدأت الآثار المترتبة على ذلك. فالشركات التي قامت بتقسيم تطبيقاتها بشكل أعمى تغرق الآن فيما نسميه المونوليث الموزع: نظام يتسم بكل تعقيدات الحوسبة الموزعة ولا يتمتع بأي من فوائد الفصل.

في Maison Code Paris، نعمل بمثابة الضمير المعماري لعملائنا. غالبًا ما نرث مشاريع “الخدمات الصغيرة” حيث يلامس تسجيل دخول المستخدم البسيط ست خدمات مختلفة، ويصل زمن الاستجابة إلى ثانيتين، ويكلف 5000 يورو شهريًا كرسوم دخول إلى السحابة.

هذه المقالة عبارة عن دليل فني رصين حول متى يتم اعتماد الخدمات الصغيرة، والأهم من ذلك، متى يجب تشغيل الصراخ منها.

لماذا تتحدث Maison Code عن هذا

في Maison Code Paris، نعمل كضمير معمari لعملائنا. غالبًا ما نرث حزمًا “حديثة” تم بناؤها دون فهم أساسي للحجم.

نناقش هذا الموضوع لأنه يمثل نقطة تحول حاسمة في النضج الهندسي. التنفيذ الصحيح يميز MVP الهش عن منصة مؤسسية مرنة يمكنها التعامل مع حركة مرور الجمعة السوداء.

قضية المونوليث المعياري

قبل أن تكتب mkdir Service-user، فكر في Modular Monolith. هذه وحدة واحدة قابلة للنشر (ثنائية/حاوية واحدة) حيث يتم فصل التعليمات البرمجية بشكل صارم إلى وحدات (حزم) تعكس مجالات الأعمال.

مسائل الهيكل

يحتوي “Spaghetti Monolith” على ملفات تستورد بعضها البعض بشكل عشوائي. يحتوي Monolith المعياري على حدود صارمة يتم فرضها من خلال قواعد الفحص أو أهداف التجميع.

سرك/
  الوحدات/
    auth/ # السياق المحدود: المصادقة
      API/ # الواجهة العامة
      داخلي/ # التنفيذ الخاص (قاعدة البيانات، المنطق)
    المخزون/ # السياق المحدود: المخزون
      واجهة برمجة التطبيقات/
      داخلي/
    الفواتير/ # السياق المحدود: الفواتير
      واجهة برمجة التطبيقات/
      داخلي/

قانون ديميتر: الكود الموجود في الفوترة لا يمكن استيراد المخزون/الداخلي. يمكنه فقط استدعاء “inventory/api”. إذا اتبعت عملية التحقق هذه، فستحصل على 90% من فوائد الخدمات الصغيرة (فصل الفريق، التعليمات البرمجية المنظفة) مع 0% من العيوب (زمن وصول الشبكة، والاتساق النهائي، وتعقيد النشر).

متى يتم التقسيم (شروط الحدود)

ننصح فقط بتقسيم الوحدة إلى خدمة صغيرة منفصلة إذا كانت تلبي أحد هذه المعايير الصارمة:

  1. متطلبات الأجهزة غير المتجانسة:

    • واجهة برمجة التطبيقات الأساسية مرتبطة بالإدخال/الإخراج. يحتاج إلى 1 جيجابايت من ذاكرة الوصول العشوائي و 0.5 وحدة المعالجة المركزية.
    • معالج الصور مرتبط بوحدة المعالجة المركزية (CPU). يحتاج إلى 16 جيجابايت من ذاكرة الوصول العشوائي ووحدة معالجة الرسومات.
    • القرار: استخراج معالج الصور. يعد نشر عقد GPU لواجهة برمجة التطبيقات (API) مضيعة للمال.
  2. سرعة الإطلاق المستقلة:

    • يقوم فريق الدفع بإرسال الرمز كل ساعة.
    • يقوم فريق دفتر الأستاذ (المحاسبة) بالدفع مرة واحدة شهريًا بعد إجراء تدقيق صارم.
    • القرار: قم باستخراج Checkout لتجنب عرقلة فريق السرعة العالية.
  3. العزلة القاتلة (“نصف قطر الانفجار”):

    • إذا كان مولد PDF يعاني من تسرب للذاكرة وتعطل، فلا ينبغي له إزالة بوابة الدفع.
    • القرار: عزل المكونات غير المستقرة أو المحفوفة بالمخاطر.

أنماط الاتصال: REST مقابل gRPC مقابل الأحداث

بمجرد أن يكون لديك خدمات متعددة، فإن المشكلة الأصعب هي التواصل. “استدعاءات الوظائف” تصبح “حزم الشبكة”.

متزامن: فخ HTTP

الخدمة “أ” تستدعي الخدمة “ب” عبر HTTP/REST. الحصول على /المستخدمين/123.

يؤدي هذا إلى ازدواج توفر A إلى B. إذا كان B معطلاً، فإن A يفشل. إذا كان B بطيئًا، فسيتوقف A. إذا اتصل A بـ B، والذي يتصل بـ C، والذي يتصل بـ D، فلديك سلسلة اتصال. إذا كانت كل خدمة تتمتع بنسبة 99.9% من التوفر، فإن سلسلة من 4 خدمات لديها 0.999^4 \approx 99.6%$ من التوفر. أنت هندسة الفشل الصحيح.

gRPC: البديل عالي الأداء

بالنسبة للاتصالات الداخلية، نفضل gRPC (Protobufs) على REST/JSON.

  • حزمة ثنائية: حمولات أصغر بمقدار 10 مرات.
  • مكتوب بقوة: يضمن إنشاء التعليمات البرمجية أن الخدمة “أ” تعرف بالضبط ما تتوقعه الخدمة “ب”.
  • تعدد الإرسال: دعم HTTP/2 خارج الصندوق.

غير متزامن: البنية المبنية على الأحداث

هذه هي الكأس المقدسة للفصل. الخدمة أ لا تستدعي الخدمة ب. الخدمة أ تبث حدثًا.

  1. سجلات المستخدم (الخدمة أ).
  2. الخدمة “أ” تنشر الحدث “USER_REGISTERED” إلى Kafka/RabbitMQ.
  3. الخدمة ب (البريد الإلكتروني) تستهلك الحدث -> إرسال بريد إلكتروني ترحيبي.
  4. تستهلك الخدمة C (التحليلات) الحدث -> لوحة معلومات التحديثات.

إذا كانت الخدمة “ب” غير متصلة بالإنترنت (الصيانة، الأعطال)، فستستمر الخدمة “أ” في العمل. الرسالة موجودة في قائمة الانتظار. عندما تعود الخدمة “ب” إلى الإنترنت مرة أخرى، فإنها تقوم بمعالجة الأعمال المتراكمة. هذه هي ** المرونة **.

تناسق البيانات: الجزء الأصعب

في Monolith، لديك معاملات ACID. بدء المعاملة؛ إدراج المستخدم؛ أدخل الحساب؛ الالتزام؛ إما أن يحدث كلاهما، أو لا يحدث أي منهما.

في الخدمات الصغيرة، لديك قاعدة بيانات لكل خدمة. تحتوي الخدمة “أ” على “users_db”. تحتوي الخدمة “ب” على “accounts_db”. لا يمكنك تشغيل معاملة عبر خادمين مختلفين لقاعدة البيانات.

نمط الملحمة

كيف يمكنك التعامل مع المعاملات الموزعة؟ السيناريو: يقدم المستخدم طلبًا.

  1. خدمة المخزون: المخزون الاحتياطي.
  2. خدمة الدفع: بطاقة الشحن.
  3. ** خدمة الشحن **: طباعة الملصق.

ماذا لو فشلت “بطاقة الشحن”؟ لقد قمت بحجز المخزون بالفعل. يجب عليك “التراجع” عن الخطوة 1. هذه معاملة تعويضية.

مخطط تسلسل
    المشارك O كمنسق الطلب
    المشارك الأول كمخزون
    المشارك P كدفعة
    
    O->>I: المخزون الاحتياطي (البند X)
    أنا-->>س: النجاح
    O->>P: اشحن 100 دولار
    P-->>O: الفشل (nsf)
    O->>I: تحرير المخزون (التعويض)
    أنا-->>س: النجاح
    O-->>المستخدم: فشل الطلب

يعد تنفيذ Sagas بشكل صحيح أمرًا معقدًا بشكل لا يصدق. أنت بحاجة إلى مُنسق آلة الحالة (مثل AWS Step Functions القياسي أو Temporal.io). إذا لم تكن بحاجة إلى هذا التعقيد، فلا تقم بإنشاء خدمات صغيرة.

قابلية الملاحظة: الرؤية في الظلام

في الوحدة المتراصة، يكون تصحيح الأخطاء هو tail -f /var/log/app.log. في الخدمات الصغيرة، قد يصل طلب مستخدم واحد إلى 10 حاويات مختلفة في 10 عقد مختلفة.

يجب عليك تنفيذ التتبع الموزع (OpenTelemetry). يحصل كل طلب على “TraceID” في Ingress Load Balancer. يتم نشر هذا المعرف في رؤوس HTTP (X-B3-TraceId) إلى كل خدمة نقل البيانات.

تقوم أدوات مثل Jaeger أو Datadog أو Honeycomb بتصور “الشلال” للطلب.

  • “لماذا كانت هذه الـ 500 مللي ثانية بطيئة؟”
  • “أوه، استغرقت الخدمة D 450 مللي ثانية في استعلام قاعدة البيانات.”

بدون تتبع، أنت تطير أعمى.

البنية التحتية: مثال للتعليمات البرمجية

نحن نستخدم Terraform لتوفير البنية التحتية للخدمات المستقلة.

# خدمة الدفع.tf
المورد "kubernetes_deployment" "الدفع" {
  البيانات الوصفية {
    الاسم = "خدمة الدفع"
  }
  المواصفات {
    النسخ المتماثلة = 3
    محدد {
      match_labels = {
        التطبيق = "الدفع"
      }
    }
    القالب {
      البيانات الوصفية {
        التسميات = {
          التطبيق = "الدفع"
        }
      }
      المواصفات {
        حاوية {
          الصورة = "رمز الدار/الدفع: الإصدار 1.2"
          الاسم = "الدفع"
          
          # القاعدة الذهبية: الحد من الموارد
          الموارد {
            الحدود = {
              وحدة المعالجة المركزية = "500 م"
              الذاكرة = "512مي"
            }
            الطلبات = {
              وحدة المعالجة المركزية = "250 م"
              الذاكرة = "256مي"
            }
          }
          
          البيئة {
             الاسم = "DB_HOST"
             القيمة_من {
               Secret_key_ref {
                 الاسم = "الدفع ديسيبل الاعتمادات"
                 المفتاح = "المضيف"
               }
             }
          }
        }
      }
    }
  }
}

استراتيجية التحلل: الشكل الخانق

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

  1. ضع وكيلًا (بوابة API) أمام Monolith.
  2. قم بتوجيه كل حركة المرور إلى Monolith.
  3. حدد سياقًا محددًا واحدًا (على سبيل المثال، “المراجعات”).
  4. بناء “خدمة المراجعات”.
  5. تغيير الوكيل: إذا كان المسار هو /api/reviews، فقم بالتوجيه إلى خدمة جديدة. وإلا، الطريق إلى مونوليث.
  6. كرر ذلك حتى يختفي المونوليث.

11. دور بوابة API

لا تريد أن تتعامل 10 خدمات مع المصادقة بشكل مستقل. يمكنك وضع بوابة (Kong أو Tyk أو AWS API Gateway) على الحافة. يعالج:

  • المصادقة: التحقق من صحة JWT.
  • تحديد السعر: 100 طلب/دقيقة لكل IP.
  • التخزين المؤقت: تخزين طلبات GET العامة مؤقتًا.
  • التوجيه: /api/v1/users -> خدمة المستخدم. وهذا يبقي خدماتك الداخلية “غبية” وتركز على منطق العمل.

12. شبكة الخدمة (Istio / Linkerd)

عندما يكون لديك 50 خدمة، تحتاج “الخدمة أ” التي تتحدث إلى “الخدمة ب” إلى التشفير (mTLS). إدارة الشهادات يدويًا هي الجحيم. تقوم شبكة الخدمة بإدخال “Sidecar Proxy” (Envoy) بجوار كل حاوية. يعالج الوكيل:

  • mTLS: التشفير التلقائي.
  • إعادة المحاولة: “إذا فشلت، أعد المحاولة 3 مرات مع التراجع الأسي”.
  • قطع الدائرة: “إذا كانت الخدمة B عبارة عن أخطاء بنسبة 50%، فتوقف عن الاتصال بها لمدة دقيقة واحدة”. فهو يفصل منطق الشبكة عن رمز التطبيق.

13. اختبار العقد (الميثاق)

كيف يمكنك اختبار الخدمة “أ” دون تشغيل الخدمة “ب”؟ اختبار العقد. الخدمة أ (المستهلك) تكتب “ملف الاتفاقية”: “أتوقع أن يُرجع GET /user { id: string }”. تجري الخدمة ب (المزود) اختبارًا مقابل ملف Pact هذا في مسار CI الخاص به. إذا قامت الخدمة “ب” بتغيير “المعرف” إلى “معرف المستخدم”، فستفشل عملية الإنشاء. يؤدي هذا إلى اكتشاف التغييرات العاجلة قبل النشر.

14. الاستنتاج

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

إذا كنت فريقًا مكونًا من 5 مطورين، فأنت تقوم ببناء Monolith. يمكنك أن تسميها خدمات صغيرة، ولكنك تقوم فقط ببناء آلة توزيع الألم.

قم بتوسيع بنية التعليمات البرمجية الخاصة بك قبل توسيع البنية الأساسية الخاصة بك.


هل تحتاج إلى نصيحة معمارية؟

نحن ننقذ الشركات الناشئة من الجحيم الموزع.

قم بتعيين مهندسي الأنظمة لدينا لإجراء تدقيق للبنية التحتية. قم بتوظيف مهندسينا.