Files
tripz-llc/docs/25-infrastructure-topology.md
T
Hamza-AyedandClaude Fable 5 da035e46a4 feat: P2 — التسوية بالرسائل + إزالة مارتن + مستند البنية (docs/25)
P2 (كليك/شام كاش بلا API):
- الرسالة تُحفظ خاماً **قبل** أي تحليل: التحليل قد يفشل فنحتاج الأصل لإعادة
  المعالجة، وعند النزاع يكون النصّ الأصلي هو الحجّة لا تفسيرُنا له.
- النقطة تصنع المال، فرسالة مزوّرة = رصيد من عدم. الحماية: سرّ لكل مستأجر
  بمقارنة ثابتة الزمن (المقارنة النصّية تسرّب السرّ حرفاً حرفاً زمنياً)،
  وبصمة محتوى فريدة تمنع احتساب إعادة الإرسال مرتين.
- Gemini بحرارة صفر ومطالَب بإرجاع null عند عدم اليقين: نموذج يخمّن مبلغاً
  يسوّي فاتورة بمال لم يصل. بلا مبلغ صريح → مراجعة بشرية لا تسوية.
- المطابقة بالمرجع أولاً، ثم بالمبلغ خلال 24 ساعة وبشرط فاتورة وحيدة —
  فاتورتان بنفس المبلغ التباسٌ يُراجَع، لا تسويةٌ عشوائية لإحداهما.
- التسوية تمرّ بـmarkSuccess نفسه فلا يتفرّع مسار مالي ثانٍ.

إزالة حاوية martin: انطلق منصّة قائمة بذاتها لها خوادمها؛ دورنا طلب وردّ لا
استضافة خرائط (قرار المالك).

docs/25: جرد الحاويات · لماذا الدفع ليس خدمة منفصلة · سيرفر السوبر-أدمن
المنفصل ونطاقه الفرعي · WebSocket مقابل الاستطلاع بالأرقام · أحجام السيرفرات
على أساس الذروة لا المعدّل · نقل مستأجر · ترتيب التوسّع.

13 اختباراً جديداً (201 إجمالاً، كلها خضراء).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:19:17 +03:00

115 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).