دليل تقني

توسيع أنظمة الباكند بدون إفراط في الهندسة: قائمة تحقق YAGNI للمعمارية

متى توسع ومتى لا. Monolith + Queue كأكثر معمارية مقللة من قيمتها، محفزات التوسع الحقيقية، اختناقات قاعدة البيانات، استراتيجية التخزين المؤقت، وقائمة تحقق YAGNI لقرارات المعمارية.

11 فبراير 202614 دقيقة للقراءةفريق هندسة أورنتس

وباء الإفراط في الهندسة

شفناها كثير: فريق من 3 مهندسين يبني معمارية مايكروسيرفسز لتطبيق عنده 200 مستخدم. Kubernetes cluster، Service Mesh، Event Bus، API Gateway، 5 قواعد بيانات منفصلة، وستاك مراقبة أعقد من التطبيق نفسه. بعد ستة أشهر، يدبغون مشاكل أنظمة موزعة بدل ما يبنون ميزات.

العكس كمان يصير: مونوليث يخدم 50,000 مستخدم متزامن، وكل طلب ياخذ 5 ثواني لأن قاعدة البيانات هي الاختناق وما فيه طبقة تخزين مؤقت. الفريق يضيف سيرفرات تطبيق أكثر، لكن قاعدة البيانات هي السقف.

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

قائمة تحقق YAGNI للمعمارية

قبل ما تضيف أي تعقيد معماري (خدمة جديدة، قاعدة بيانات جديدة، message broker جديد، طبقة cache جديدة)، اسأل نفسك:

السؤالإذا نعمإذا لا
عندنا مشكلة أداء مقاسة حالياً؟حل المشكلة المحددةلا تضيف تعقيد لمشكلة افتراضية
هل هذا يحل مشكلة واجهناها فعلاً؟فكر فيه، مع أدلةلا تحل مشاكل ما صادفتها بعد
نقدر نحلها بطريقة أبسط أولاً؟استخدم الطريقة الأبسطالحل المعقد ممكن يكون ضروري
هل 10x حركة مرور أكثر راح تكسر المعمارية الحالية؟خطط مسبقاً (لكن لا تبني بعد)المعمارية الحالية تمام
الفريق كبير كفاية ليصون هذا؟تابع إذا على الأقل شخصين يقدرون يملكونهلا تضيف أشياء ما حد يقدر يصونها
نقدر نرجع فيها إذا ما نفعت؟مخاطرة مقبولةخطر كبير

الجواب الافتراضي على "هل نضيف هذا؟" هو لا. أضف تعقيد لما عندك أدلة إنه ضروري، مش لما تتخيل إنه ممكن يكون ضروري.

Monolith + Queue: أكثر معمارية مقللة من قيمتها

تطبيق مونوليثي مع job queue للخلفية يتعامل مع 90% من أحمال عمل SaaS. كودبيس واحد، deployment واحد، قاعدة بيانات واحدة، وqueue للعمل الخلفي.

┌─────────────────────────────────────────┐
│            Monolith                      │
│                                          │
│  ┌──────────┐  ┌──────────┐            │
│  │  API      │  │  Admin   │            │
│  │  Routes   │  │  UI      │            │
│  └────┬─────┘  └────┬─────┘            │
│       │              │                   │
│  ┌────▼──────────────▼─────┐            │
│  │    Services              │            │
│  │    (منطق الأعمال)        │            │
│  └────┬────────────────────┘            │
│       │                                  │
│  ┌────▼──────────────────────┐          │
│  │  قاعدة البيانات (PostgreSQL)│         │
│  └───────────────────────────┘          │
│                                          │
│  ┌───────────────────────────┐          │
│  │  Job Queue (BullMQ/Redis) │          │
│  │  - إرسال الإيميلات        │          │
│  │  - فهرسة البحث           │          │
│  │  - توليد التقارير         │          │
│  │  - استيراد البيانات       │          │
│  └───────────────────────────┘          │
└─────────────────────────────────────────┘

ليش يشتغل

  • كودبيس واحد: الـ Refactoring آمن. البحث والاستبدال يشتغل. الأنواع تتدفق عبر التطبيق كله.
  • Deployment واحد: انشر مرة. ما فيه تنسيق خدمات. ما فيه rollbacks موزعة.
  • قاعدة بيانات مشتركة: الـ Joins تشتغل. المعاملات ACID. ما فيه صداع Eventual Consistency.
  • Job Queue: الشغل طويل المدة (إيميلات، استيراد، فهرسة) ما يحجز معالجة الطلبات.
  • تصحيح أخطاء بسيط: Stack traces كاملة. اللوقات في مكان واحد. ما تحتاج Distributed Tracing.

متى Monolith + Queue يوصل حدوده

العرضالسببالحل
وقت رد API > 2 ثانيةاستعلامات قاعدة البيانات بطيئة جداًإضافة فهارس، تحسين الاستعلامات، Read Replicas
CPU قاعدة البيانات > 80% مستمرحجم أو تعقيد الاستعلامات عالي جداًRead Replicas، طبقة تخزين مؤقت، تحسين الاستعلامات
الـ Deploys تخوفالكودبيس كبيرة جداً، التيستات بطيئة جداًتيستات أفضل، Feature Flags، Canary Deploys (مش مايكروسيرفسز)
ميزة واحدة تكسر كل شيما فيه حدود وحداتتنظيم كود أفضل (مش مايكروسيرفسز)
الفريق ما يقدر يشتغل بالتوازيتعارضات كود، جحيم الـ Mergeملكية الوحدات، Trunk-Based Development

لاحظ: ولا واحد من هالأعراض ينحل بإضافة مايكروسيرفسز. ينحلون بهندسة أفضل داخل المونوليث. المايكروسيرفسز تحل مشكلة مختلفة.

متى تفصل: المحفزات الحقيقية

افصل لخدمات منفصلة بس لما:

1. حجم الفريق يفرض ذلك. لما عندك 20+ مهندس وحدود الفريق ما تتوافق مع الكودبيس. فريق A يملك ميزة X وفريق B يملك ميزة Y، ويحتاجون ينشرون بشكل مستقل. هذا السبب الرئيسي للمايكروسيرفسز.

2. متطلبات توسع مختلفة. خدمة البحث تحتاج 10x حوسبة أكثر من خدمة كتالوج المنتجات. تشغيلهم في نفس العملية يهدر موارد. خدمات منفصلة تقدر توسع بشكل مستقل.

3. متطلبات تقنية مختلفة. خدمة ML Inference تحتاج Python و GPUs. خدمة API تحتاج Node.js. تشغيل كلاهما في عملية واحدة غير عملي.

4. عزل الأعطال. كراش في خدمة الدفع ما لازم يوقع خدمة كتالوج المنتجات. لما عزل الأعطال حرج للأعمال، العمليات المنفصلة تساعد.

5. حدود الامتثال. معالجة الدفع المتوافقة مع PCI لازم تنفصل عن باقي التطبيق لأغراض التدقيق. خدمة منفصلة بحدود أمنية خاصة تبسط الامتثال.

شو مش سبب للفصل

  • "المايكروسيرفسز أفضل ممارسة" (هي مقايضة، مش أفضل ممارسة)
  • "ممكن نحتاج نوسع" (وسع لما تحتاج، مش قبل)
  • "Clean Architecture" (حدود الوحدات داخل المونوليث أنظف من حدود الخدمات)
  • "تطوير مدفوع بالسيرة الذاتية" (Kubernetes على سيرتك الذاتية ما يساعد مستخدميك)

قاعدة البيانات عادةً هي الاختناق

في 80% من مشاكل الأداء، قاعدة البيانات هي الاختناق. مش كود التطبيق. مش الشبكة. قاعدة البيانات.

التشخيص

-- إيجاد الاستعلامات البطيئة (PostgreSQL)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- إيجاد الفهارس الناقصة
SELECT schemaname, tablename, seq_scan, idx_scan,
       CASE WHEN seq_scan > 0 THEN round(100.0 * idx_scan / (seq_scan + idx_scan), 1) ELSE 100 END AS idx_pct
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_scan DESC
LIMIT 20;

-- إيجاد تضخم الجداول
SELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total_size,
       pg_size_pretty(pg_relation_size(tablename::regclass)) AS data_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC
LIMIT 20;

ترتيب الإصلاح

  1. إضافة الفهارس الناقصة (دقائق للتطبيق، تأثير ضخم)
  2. تحسين الاستعلامات البطيئة (إعادة كتابة استعلامات N+1، إضافة WHERE، استخدام EXPLAIN ANALYZE)
  3. إضافة تخزين مؤقت (Redis للبيانات كثيرة القراءة قليلة التغيير)
  4. Read Replicas (توجيه استعلامات القراءة للنسخ، الكتابة للأساسي)
  5. Connection Pooling (PgBouncer بين التطبيق وقاعدة البيانات)
  6. التوسع العمودي (مثيل أكبر، ذاكرة أكثر، تخزين أسرع)
  7. التوسع الأفقي (Sharding، بس لما كل شي ثاني ينتهي)

أغلب الفرق تقفز للخطوة 7 قبل ما تجرب الخطوات 1-3. الخطوات 1-3 مجانية أو رخيصة وغالباً تحل المشكلة بالكامل.

استراتيجية التخزين المؤقت

شو تخزن مؤقتاً

البياناتتخزين مؤقت؟TTLالإبطال
جلسات المستخدمنعممدة الجلسةعند تسجيل الخروج
كتالوج المنتجاتنعم5-15 دقيقةعند حدث تحديث المنتج
نتائج البحثممكن1-5 دقائقعند تحديث الفهرس
بيانات خاصة بالمستخدمبحذرقصير (1-5 دقائق)عند إجراء المستخدم
إعدادات / تكويننعمطويل (ساعة واحدة)عند تغيير المسؤول
تجميعات محسوبةنعم15-60 دقيقةعند تغيير البيانات الأساسية
بيانات فورية (مخزون)لاN/Aدائماً اجلب مباشر

شو ما تخزن مؤقتاً

  • بيانات خاصة بالمستخدم مع آثار خصوصية: محتويات سلة مشتريات مخزنة مؤقتاً تُعرض للمستخدم الغلط هي تسريب بيانات.
  • بيانات سريعة التغير: مستويات المخزون، أسعار مباشرة، عروض المزادات. TTL التخزين المؤقت ما يقدر يواكب.
  • عمليات كثيفة الكتابة: إذا البيانات تتغير أكثر مما تُقرأ، التخزين المؤقت يضيف تعقيد بدون فائدة.

نمط إبطال التخزين المؤقت

// إبطال مبني على الأحداث (موصى به)
eventBus.on('product.updated', async (product) => {
    await cache.delete(`product:${product.id}`);
    await cache.delete(`product-list:${product.categoryId}`);
    // لا تحاول تحدث الكاش هنا. خل القراءة الجاية تملأه.
});

// Read-through مع stale-while-revalidate
async function getProduct(id: string): Promise<Product> {
    const cached = await cache.get(`product:${id}`);
    if (cached && !isExpired(cached)) {
        return cached.data;
    }

    // قديم لكن قدمه وأنت تحدث في الخلفية
    if (cached && isExpired(cached)) {
        refreshInBackground(id); // async، لا تنتظره
        return cached.data;      // قدم القديم
    }

    // Cache miss
    const product = await db.products.findById(id);
    await cache.set(`product:${id}`, product, { ttl: 300 });
    return product;
}

محادثة التوسع مع أصحاب المصلحة

المهندسون يبون يوسعون لأسباب تقنية. أصحاب المصلحة يبون يوسعون لأسباب تجارية. المحادثة تحتاج لغة مشتركة.

أصحاب المصلحة يقولونشو يقصدونرد الهندسة
"نحتاج نتعامل مع 10x حركة مرور"حملة تسويقية جاية، أو تمنيات"شو الجدول الزمني المتوقع؟ خلنا نعمل load test للسعة الحالية أولاً."
"الموقع بطيء"صفحة واحدة بطيئة، مش الموقع كله"أي صفحة؟ خلني أعمل لها profile."
"نحتاج مايكروسيرفسز"قرأوا مقال أو سمعوا عنها في مؤتمر"أي مشكلة نحلها؟ خلني أوريك المعمارية الحالية."
"نقدر نتحمل Black Friday؟"قلق حقيقي من حركة المرور القصوى"خلنا نعمل load test بـ 3x الذروة الحالية ونشوف وين ينكسر."

دائماً ابدأ بالقياس. "الموقع بطيء" مش متطلب توسع. "صفحة قائمة المنتجات تاخذ 4 ثواني مع 500 مستخدم متزامن، الهدف 500 مللي ثانية" هو متطلب توسع بهدف قابل للقياس.

أخطاء شائعة

  1. مايكروسيرفسز مع 100 مستخدم. إذا المونوليث يتحمل الحمل وفريقك أقل من 10 أشخاص، المايكروسيرفسز تضيف تعقيد تشغيلي بدون فائدة.

  2. تخزين مؤقت بدون استراتيجية إبطال. إضافة cache سهلة. الحفاظ على تناسقه مع قاعدة البيانات صعب. خطط للإبطال قبل ما تضيف الـ cache.

  3. توسيع التطبيق لما قاعدة البيانات هي الاختناق. إضافة 5 سيرفرات تطبيق ما يساعد إذا كلهم ينتظرون نفس استعلام قاعدة البيانات البطيء.

  4. ما فيه load testing. "نظن إننا نحتاج نوسع" مش دليل. اعمل load test بـ 3-5x حركة المرور القصوى الحالية. شوف وين فعلاً ينكسر.

  5. التوسع العمودي كتابو. مثيل قاعدة بيانات أكبر يكلف $200/شهر أكثر وياخذ 10 دقائق للتجهيز. هذا تقريباً دائماً أرخص من أسبوع عمل معماري.

  6. Sharding قبل ما تجرب كل شي ثاني. Sharding قاعدة البيانات أعقد حل توسع. جرب الفهارس، تحسين الاستعلامات، التخزين المؤقت، Read Replicas، والتوسع العمودي أولاً.

  7. البناء لتوسع ما راح توصله. إذا تطبيق SaaS عندك 500 مستخدم وينمو 10% شهرياً، ما تحتاج تتعامل مع مليون مستخدم. تحتاج تتعامل مع 5,000 مستخدم السنة الجاية.

النقاط الرئيسية

  • Monolith + Queue يتعامل مع 90% من أحمال عمل SaaS. كودبيس واحد، قاعدة بيانات واحدة، deployment واحد، و job queue للعمل الخلفي. لا تفصل حتى يكون عندك أدلة إنك تحتاج.

  • قاعدة البيانات عادةً هي الاختناق. أضف فهارس، حسّن الاستعلامات، أضف تخزين مؤقت، ثم فكر في Read Replicas. أغلب مشاكل الأداء تنحل بالخطوات 1-3.

  • افصل الخدمات بسبب حجم الفريق، مش التوسع التقني. المايكروسيرفسز تحل مشكلة "20 مهندس ما يقدرون يشتغلون في كودبيس واحد"، مش مشكلة "نحتاج أداء أكثر".

  • قِس قبل ما توسع. اعمل load test بـ 3-5x الذروة الحالية. اعمل profile للصفحات البطيئة. لاقِ الاختناق الفعلي. ثم حل ذاك بالتحديد.

  • خزّن مؤقتاً القراءات، مش الكتابات. خزّن مؤقتاً البيانات اللي تُقرأ كثير وتتغير نادراً. أبطل عند أحداث تغيير البيانات. قدم بيانات قديمة وأنت تحدث في الخلفية.

  • وسّع بأرخص طريقة أولاً. مثيل قاعدة بيانات أكبر (دقائق، $200/شهر) يتغلب على شهر عمل معماري. التوسع العمودي مش فشل.

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

المواضيع المغطاة

معمارية التوسعتوسيع الباكندمتى التوسعالتحسين المبكرمعمارية YAGNIمونوليث ضد مايكروسيرفسزاستراتيجية التخزين المؤقتاختناق قاعدة البيانات

جاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟

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

ابدأ محادثة