P2 (كليك/شام كاش بلا API): - الرسالة تُحفظ خاماً **قبل** أي تحليل: التحليل قد يفشل فنحتاج الأصل لإعادة المعالجة، وعند النزاع يكون النصّ الأصلي هو الحجّة لا تفسيرُنا له. - النقطة تصنع المال، فرسالة مزوّرة = رصيد من عدم. الحماية: سرّ لكل مستأجر بمقارنة ثابتة الزمن (المقارنة النصّية تسرّب السرّ حرفاً حرفاً زمنياً)، وبصمة محتوى فريدة تمنع احتساب إعادة الإرسال مرتين. - Gemini بحرارة صفر ومطالَب بإرجاع null عند عدم اليقين: نموذج يخمّن مبلغاً يسوّي فاتورة بمال لم يصل. بلا مبلغ صريح → مراجعة بشرية لا تسوية. - المطابقة بالمرجع أولاً، ثم بالمبلغ خلال 24 ساعة وبشرط فاتورة وحيدة — فاتورتان بنفس المبلغ التباسٌ يُراجَع، لا تسويةٌ عشوائية لإحداهما. - التسوية تمرّ بـmarkSuccess نفسه فلا يتفرّع مسار مالي ثانٍ. إزالة حاوية martin: انطلق منصّة قائمة بذاتها لها خوادمها؛ دورنا طلب وردّ لا استضافة خرائط (قرار المالك). docs/25: جرد الحاويات · لماذا الدفع ليس خدمة منفصلة · سيرفر السوبر-أدمن المنفصل ونطاقه الفرعي · WebSocket مقابل الاستطلاع بالأرقام · أحجام السيرفرات على أساس الذروة لا المعدّل · نقل مستأجر · ترتيب التوسّع. 13 اختباراً جديداً (201 إجمالاً، كلها خضراء). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
10 KiB
Markdown
115 lines
10 KiB
Markdown
# 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).
|