# 25 — بنية السيرفرات والحاويات (التنظيم الكامل) > إجابات أسئلة المالك 2026-07-18: كم حاوية؟ هل الدفع منفصل؟ أين السوبر-أدمن؟ ما حجم السيرفر؟ كيف ننقل مستأجراً؟ > ذو صلة: [14](14-server-conventions.md) · [15](15-deploy-flow.md) · [20](20-tls.md) · [24](24-tenant-wallet-revenue.md). --- ## 1. الحاويات: أربع، والباك إند **واحد** لا مفكَّك | الحاوية | الصورة | الدور | |---|---|---| | `tripz-api` | مبنيّة محلياً | كل الـHTTP + WebSocket | | `tripz-worker` | **نفس الصورة**، أمر مختلف | المهام المجدولة والطوابير (BullMQ) | | `tripz-postgres` | postgis/postgis:16 | القاعدة | | `tripz-redis` | redis:7-alpine | الكاش · المطابقة · المواقع · الطوابير | **الدفع ليس حاوية منفصلة، وهذا مقصود.** المدفوعات وحدة (module) داخل نفس التطبيق تشارك القاعدة والمعاملة (transaction). فصلها إلى خدمة مستقلة يعني أن تسوية دفعةٍ تكتب في محفظتين عبر الشبكة بلا معاملة واحدة — أي انهيارٌ في المنتصف يترك مالاً نصف مسوّى. الوحدة الذرّية للمال هي معاملة قاعدة واحدة، ولا تُقطَع بحدود شبكة إلا لضرورة قاهرة. حين يكبر الحمل نُشغّل **نسخاً أكثر من نفس الـAPI**، لا خدمات مجزَّأة. **`tripz-martin` أُزيلت** (قرار المالك): انطلق منصّة قائمة بذاتها لها خوادمها وبلاطاتها. دورنا طلبٌ وردّ — لا استضافة خرائط ولا قاعدة جغرافية ضخمة نصونها بلا مقابل. ## 2. أين يقع كل شيء ``` ┌──────────── سيرفر المنصّة (Control Plane) ────────────┐ │ tripz-api (سوبر-أدمن فقط) · postgres · redis │ تطبيقات ولوحات ──────┤ admin.tripz.com ← لوحة السوبر-أدمن │ └───────────────────────┬──────────────────────────────┘ │ HTTPS + سرّ المنصّة ┌────────────────────────────────────┼────────────────────────────────┐ │ │ │ ┌───────▼────────┐ ┌────────▼───────┐ ┌─────────▼──────┐ │ سيرفر مشترك │ │ سيرفر سيادي │ │ سيرفر مستأجر │ │ عدة مستأجرين │ │ مستأجر واحد │ │ آخر… │ │ api·worker·db │ │ api·worker·db │ │ │ └────────────────┘ └─────────────────┘ └────────────────┘ ``` كل صندوق يشغّل **نفس الأربع حاويات**. الفرق في `.env` وحده: أي قاعدة، أي بادئة، أي مستأجرين. ## 3. سيرفر السوبر-أدمن المنفصل — كما طلبت **لماذا منفصل**: سرّ المنصّة (`PLATFORM_SECRET`) يفتح كل المستأجرين. بقاؤه على صندوق يشاركه مستأجرٌ يعني أن اختراق مستأجر واحد = اختراق المنصّة كلها. الفصل يجعل نطاق أي اختراق مستأجراً واحداً. **الإعداد**: 1. صندوق جديد (4 vCPU · 8GB يكفي — لا يحمل رحلات). 2. نفس `docker compose` بـ`.env` خاص: قاعدته الخاصة، و`PLATFORM_SECRET` **موجود هنا فقط**. 3. نطاق فرعي على دومينك: `admin.tripz.com` → Nginx يُنهي TLS ويوجّه إلى `127.0.0.1:4010`. 4. **يُحذف `PLATFORM_SECRET` من سيرفرات المستأجرين** — بلا هذا يبقى الفصل شكلياً. **نقطة تحتاج قراراً**: السوبر-أدمن يقرأ اليوم من **قاعدته المحلية**. مع سيرفرات متعدّدة لن يرى أرقام المستأجرين البعيدين تلقائياً. خياران: - **(أ) دفع دوري**: كل سيرفر مستأجر يرسل ملخّصه (رحلات · GMV · إيراد) إلى المنصّة كل ساعة. بسيط، والأرقام متأخّرة ساعة — وهذا مقبول لأنها أرقام إدارية لا تشغيلية. **توصيتي.** - **(ب) سحب حيّ**: المنصّة تنادي كل سيرفر عند فتح اللوحة. أرقام لحظية، لكن سيرفراً متوقّفاً يُعطّل اللوحة كلها. ## 4. WebSocket بدل الاستطلاع — وأثره العددي في سيرو كان السائق **يستطلع** (polling) بحثاً عن طلبات، فكل سائق يولّد طلباً كل بضع ثوانٍ سواء وُجد عمل أم لا. مع 1000 سائق واستطلاع كل 3 ثوانٍ = **333 طلب/ثانية دائمة بلا أي رحلة**. عندنا: اتصال WebSocket واحد يبقى مفتوحاً، ولا تمرّ بيانات إلا عند **حدث فعلي**. نفس الألف سائق = صفر طلب في الهدوء. هذا الفرق وحده هو سبب اتّساع الطاقة. **الغرف المبنيّة** (`realtime.gateway.ts`): - `tenant:{id}:user:{userId}` — إشعارات المستخدم. - `tenant:{id}:drivers` — بثّ لكل السائقين المتصلين. - `tenant:{id}:trip:{tripId}` — التتبّع والدردشة والمكالمة للطرفين. **«الرحلات المتاحة» (طلبك)**: تُبنى فوق `tenant:{id}:drivers` القائمة، مع غرفة لكل منطقة (`tenant:{id}:zone:{zoneId}`) حتى لا يُبثّ عرضٌ في عمّان إلى سائق في حلب. **بند مفتوح — يُنفَّذ عند المجموعة التالية.** **التوسّع الأفقي جاهز**: `redis-io.adapter` مبنيٌّ أصلاً، فنسخ الـAPI المتعدّدة تتشارك الغرف عبر Redis. سائقٌ متصل بالنسخة أ يستقبل حدثاً بثّته النسخة ب. ## 5. الطاقة والحجم — بالأرقام اختبار الحمل (docs/22 §1.2) على **صندوق مشترك مزدحم أصلاً**: ≈ **1.15 مليون رحلة/يوم**، أي **115 ضعف** هدف الـ10 آلاف/يوم. | الهدف اليومي | الحجم الكافي | ملاحظة | |---|---|---| | حتى 10 آلاف رحلة | 4 vCPU · 8GB | الوضع الحالي — فائض كبير | | حتى 100 ألف | 8 vCPU · 16GB | صندوق مخصّص لا مشترك | | حتى مليون | 8–16 vCPU · 32GB · NVMe | حدّ الصندوق الواحد | | فوق مليون | نسختا API + فصل القاعدة | التقسيم يبدأ هنا لا قبله | **وقت الذروة هو المقياس لا المعدّل اليومي**: مليون رحلة/يوم ليست 11.5 رحلة/ثانية موزّعة بالتساوي؛ الذروة (7–9 صباحاً · 4–7 مساءً) تحمل ~٢٥٪ من اليوم في ساعتين، أي ≈ 35 رحلة/ثانية. الحجم يُقاس على هذا. **العنق دائماً Postgres (بركة الاتصالات) لا الـCPU.** أول ما يبطؤ النظام، ارفع البركة وافحص الاستعلامات قبل أن تشتري معالجات. Redis 4–8GB يكفي لأن ما فيه نصوص صغيرة. ## 6. نقل مستأجر إلى سيرفر خاص لأن كل شيء في حاويات وكل مستأجر معزول ببادئة، النقل ميكانيكي: 1. صندوق جديد + `git clone` + `.env` خاص به. 2. `docker compose up -d --build` ثم `npm run migration:run`. 3. تصدير بيانات المستأجر من القاعدة القديمة واستيرادها. 4. `POST /admin/tenants/provision` أو نقل صفّه. 5. توجيه نطاقه الفرعي إلى الصندوق الجديد. 6. **تعليق المستأجر على الصندوق القديم** (`PATCH /admin/tenants/:id/status`) قبل التبديل — يمنع كتابةً جديدة على القاعدة القديمة أثناء النقل، وهي أخطر لحظة في العملية كلها. ## 7. التوسّع بالترتيب — لا تقفز خطوة 1. **رأسياً**: كبّر الصندوق. أرخص وأبسط ويكفي حتى ~مليون/يوم. 2. **نسخ API**: `docker compose up -d --scale api=3` خلف Nginx. الكود بلا حالة والـRedis adapter جاهز. 3. **فصل القاعدة**: Postgres على صندوق خاص + نسخة قراءة للتقارير. 4. **فصل Redis**: نادراً ما يلزم. 5. **التقسيم (sharding)**: لا داعي له في الأفق المنظور — بحث الحمل حسمها. ## 8. الاحتياطي نسخة Postgres متدفّقة (streaming replica) + Redis AOF على صندوق ثانٍ. `RPO ≈ ثوانٍ`، `RTO ≈ دقائق` (وقت إقلاع compose). ⚠️ نسخة احتياطية لم تُختبَر استعادتها ليست نسخة احتياطية. **بند مفتوح: تمرين استعادة دوري.** --- ## 9. بنود مفتوحة من هذا المستند - غرف «الرحلات المتاحة» حسب المنطقة (§4). - آلية تجميع أرقام المستأجرين للسوبر-أدمن — الخيار (أ) الموصى به (§3). - تمرين استعادة النسخة الاحتياطية (§8). - إزالة `PLATFORM_SECRET` من سيرفرات المستأجرين عند فصل المنصّة (§3).