الأساسيات
محوران Router على منفذين (8080 مستوى البيانات، 8090 مستوى الإدارة)، تم إنشاؤهما من نفس AppStateالمشترك. يتطلب مستوى البيانات مفتاح API على أي مسار؛ تتطلب إدارة المستوى وحدة تحكم جمهور JWT مخصصة - فالآليتان لا تتداخلان أبدًا. يتم تطبيق تحديد المعدل مرتين: بواسطة IP قبل المصادقة، ثم بواسطة الممثل المصادق عليه بعد ذلك. قاطع الدائرة ليس برنامجًا وسيطًا عالميًا: إنه كائن لكل خدمة (ولكل هدف مخصص لـ PostgREST)، يتم استدعاؤه مباشرة في رمز الوكيل. ويقوم الوكيل النهائي بتغيير النقل اعتمادًا على المسار - أحيانًا وفقًا لطريقة HTTP أو البحث في قاعدة البيانات: طلب/رد NATS لمعظم حركة المرور، وتدفق HTTP المباشر للتخزين، والمتغيرات الثلاثة في الوقت الفعلي وPostgres CRUD المخصص.
بوابة واحدة، وجمهورين مختلفين للغاية
تأتي حركة مستوى البيانات من SDK أو تطبيق العميل: حجم الطلبات المجهولة أو الطلبات التي تمت مصادقتها بواسطة مفتاح واجهة برمجة التطبيقات، مع ملف تعريف إساءة الاستخدام بالقرب من أي واجهة برمجة تطبيقات عامة. يأتي مستوى إدارة حركة المرور من الاستوديو - واجهة إدارة المشروع - ويحمل عمليات حساسة: إنشاء المشروع، وتدوير المفاتيح، وقراءة سجلات المستأجر. يشترك الاثنان في هدف مشترك (الوكالة لنفس الخدمات الداخلية: aura-auth، aura-db، aura-storage، وما إلى ذلك) ولكن ليس نفس سطح الخطر.
يتطلب المرور عبر نفس جهاز التوجيه الاختيار بين خيارين سيئين: إما أن يرث Studio CORS حرف البدل الضروري لمجموعة SDK العامة (Access-Control-Allow-Origin: *)، أو يرث SDK قائمة أصول مقيدة مصممة للوحة معلومات داخلية. يقوم كود بوابة الهالة بفرز هذا التوتر من main.rs: اثنان منفصلان Router، كل منهما له CorsLayer الخاص به - حرف البدل المعتمد على جانب مستوى البيانات، تم رفضه وتسجيله كخطأ على جانب مستوى الإدارة.
جهازي توجيه أكسوم، أحدهما AppState مشترك
لا يعد الفصل عملية نشر منفصلة: تعمل الخطتان في نفس العملية، على نفس AppState (تجمعات Postgres، عميل NATS، ذاكرات Moka المؤقتة، قواطع الدائرة). يختلف بناء Router فقط، عبر وظيفتين مخصصتين يتم استدعاء كل منهما مرة واحدة عند بدء التشغيل ويتم تقديمهما بواسطة وظيفتين منفصلتين TcpListener.
يبدأ الرسمان البيانيان للمسار من نفس القاعدة، service_routes(): يتم تثبيت نفس معالجات الوكيل (db_proxy, storage_proxy, functions_proxy…) على كلا الخطتين، مع مسارات إضافية خاصة بكل منهما. تؤدي إعادة استخدام نفس المعالجات إلى تجنب التنفيذ المزدوج للوكيل؛ يؤدي التباعد فقط في البرامج الوسيطة إلى تجنب تكرار منطق الأعمال للحصول على حدود أمنية. إذا كانت الواجهة الخلفية الخاصة بك مُصممة كمساحة عمل Cargo متعددة الخدمات، فاطلع على دليل بنية مساحة عمل Cargo — البوابة هي مجرد صندوق واحد من بين صناديق أخرى في هذا القسم.
تختلف المصادقة عند الدخول
على مستوى البيانات، يكون مفتاح API إلزاميًا على أي مسار، باستثناء عدد قليل من المسارات العامة حقًا (/health, JWKS، نقاط نهاية التسجيل). يتم نقله كرأس apikey أو X-API-Key - أو، لمسارات تدفق WebSocket وSSE فقط، كمعلمة ?apikey=. يحظر الكود صراحةً هذا الوضع الأخير لمفتاح service_role: يتسرب مفتاح URL في سجلات الوصول، وتتبعات OTel، ورأس المُحيل. يظل JWT اختياريًا على جانب مستوى البيانات: بدونه، يظل المتصل anon؛ معه يصبح authenticated.
على مستوى الإدارة، مفتاح API غير موجود: يتم قبول وحدة تحكم JWT فقط، والتي يجب أن يكون جمهورها بالضبط aurabase-control. لا يتم تنفيذ الدور بواسطة الرمز المميز نفسه - يتم إعادة حسابه عند كل طلب من عضوية المستخدم في المؤسسة التي تمتلك المشروع، الموروثة عبر علاقة المشروع → المؤسسة.
| الرمز المميز مطلوب | مفتاح API (apikey / X-API-Key)، دائمًا | وحدة تحكم JWT (التفويض: الحامل)، دائمًا |
|---|---|---|
| ارتفاع الدور | JWT اختياري: حالًا → مصادق عليه | RBAC الموروثة من المؤسسة (المالك/المسؤول/المطور/المشاهد) |
| أدخل سلسلة الاستعلام | يُسمح به في WS/SSE فقط، ولا يُسمح به مطلقًا في Service_role | لا ينطبق |
| الجمهور المتوقع | المشروع المستهدف (UUID للمسار) | ثابت "التحكم aurabase" |
| كورس | حرف البدل * مسموح به | تم رفض Wildcard، أصول الاستوديو فقط |
الترتيب الحقيقي للبرمجيات الوسيطة (وسبب أهميتها)
يقوم axum بتكديس البرامج الوسيطة باستدعاءات .layer() المتعاقبة - والقاعدة التي تحكم ترتيب التنفيذ مثيرة للدهشة من الناحية العملية: آخر .layer() الذي تم وضعه يصبح الطبقة الخارجية، وبالتالي أول طبقة يتم اجتيازها بواسطة طلب وارد، وآخر من يرى الاستجابة تترك. وبالتالي فإن القراءة الخطية للملف تعطي الترتيب العكسي لترتيب التنفيذ الفعلي.
تقوم البرمجيات الوسيطة request_id بتعيين رأس X-Request-Id فقط في الاستجابة، وليس على الطلب الوارد أبدًا. نظرًا لأنه تم وضع AccessLogLayer بعده في الملف - وبالتالي فهو خارجي أكثر، وبالتالي تم اجتيازه من قبل - فإن التقاطه للحقل request_id يقرأ الرأس كما أرسله العميل، وليس المعرف الذي تم إنشاؤه لاحقًا في السلسلة. إذا لم يقدم المتصل أي X-Request-Id، فسيغادر سطر سجل الوصول بحقل فارغ، بينما تحمل الاستجابة التي تم إرجاعها معرف UUID تم إنشاؤه حديثًا. ليس عيبًا مخفيًا — تذكير بأن الترتيب الذي تمت به كتابة السلسلة .layer() لا يضمن أي شيء يتعلق بالترتيب المنطقي الذي ننسبه إليها.
تحديد المعدل: IP أولاً، ثم الممثل
يتم تطبيق تحديد المعدل في تمريرتين مختلفتين، في وقتين مختلفين في السلسلة. يعمل الأول قبل المصادقة والحدود بواسطة عنوان IP - وهو مرشح عام مضاد للفيضانات، نشط حتى على الطرق العامة: بدونه، يمكن للتدفق غير المصادق أن يطرق نقطة نهاية باهظة الثمن، مثل تجميع السجلات، دون تشغيل فحص JWT على الإطلاق. يتم تشغيل الخيار الثاني بعد المصادقة والقيود من قبل الممثل - مفتاح واجهة برمجة التطبيقات أو المستخدم - باستخدام المطالبات التي أدخلتها المصادقة للتو: هذه هي حصة المنتج الحقيقية، وهي الحصة التي يتم احتسابها للفواتير والخطط.
يعتمد التنفيذ على صندوق governor (مجموعة الرمز المميز) للحساب المحلي، مع نافذة Lua Redis النصية المنزلقة للتوزيع بين مثيلات البوابة، واحتياطي محلي (ذاكرة التخزين المؤقت Moka) إذا كان Redis غير متاح. الإعدادات الافتراضية للمستودع: 100 طلب في الثانية، دفعة من 1000.
قاطع الدائرة ليس طبقة، بل هو كائن لكل هدف
على عكس بقية السلسلة، لا يظهر قاطع الدائرة في أي .layer(). يحمل AppState مثيل CircuitBreaker واحد لكل خدمة (المصادقة، db، الوقت الفعلي، التخزين، الوظائف، الإشعارات، ai، الموفر، التحكم)، ورمز الوكيل نفسه - وليس جهاز التوجيه - هو الذي يستدعي try_acquire_probe() قبل محاولة الطلب، ثم record_success() أو record_failure() اعتمادًا على النتيجة.
حالة PostgREST مميزة: المشاريع في الهيكل المخصص (Postgres وPostgREST الخاصان بالمشروع) لا تحتوي على مجال خطأ مشترك - كل عملية PostgREST هي هدفها الخاص. وبالتالي، تحتفظ البوابة بجدول قواطع الدائرة المفهرسة حسب الهدف الذي تم حله، ويتم ملؤه سريعًا وتطهيره كل 60 ثانية عن طريق عملية مسح تزيل الإدخالات غير النشطة: بدون هذا التطهير، سيضيف كل مشروع مخصص جديد إدخالاً لا يختفي أبدًا.
يتم إرجاع رمز التحقيق كـ Drop إذا لم يتم استهلاكه بشكل صريح مطلقًا - وهو مفيد عندما تنتهي مهلة جميع محاولات الطلب دون الوصول إلى الفرع الذي كان سيحرره. ولا يتم تشغيل إعادة التشغيل التلقائي إلا في حالة وجود دليل صارم على عدم التسليم من جانب NATS (NoResponders): لا تثبت مهلة البوابة البسيطة أي شيء عن التسليم الحقيقي للطلب، ويمكن أن تؤدي إعادة تشغيله إلى تنفيذه مرتين.
الرابط الأخير: NATS أو HTTP المباشر، وليس عشوائيًا أبدًا
الوكيل النهائي لا يتحدث بروتوكولًا واحدًا بشكل عكسي، ولا يتم تحديد الاختيار عن طريق المسار: يمكن أن يعتمد على طريقة HTTP، أو حتى على البحث في قاعدة البيانات. بالنسبة لمعظم حركة المرور (المصادقة، والوظائف، والإشعارات، والتحكم، وغالبية قاعدة البيانات)، تقوم البوابة بإجراء تسلسل لطلب HTTP في مظروف NATS وإرساله كطلب/رد إلى موضوع مخصص للخدمة - رحلة ذهابًا وإيابًا بدون مصافحة TCP، موثقة في التعليمات البرمجية باعتبارها أسرع بكثير من وكيل HTTP الكلاسيكي لحركة المرور من نوع RPC.
التخزين، والمتغيرات الثلاثة للوقت الفعلي (WebSocket، وSSE، وREST للبث/القنوات/التواجد) و- بشكل مشروط - تخرج طلبات Postgres CRUD من هذا المسار وتنتقل عبر عميل HTTP مباشر ومجمع. لقد اتخذ التخزين هذا الاختيار بشكل صريح: تشفير نص ثنائي في مظروف NATS يتطلب إجراء تسلسل له، وتحميله بالكامل في الذاكرة من كلا الطرفين، والبقاء تحت الحد الأقصى لحجم رسالة NATS - وهي تكلفة حقيقية للكائنات الكبيرة. WebSocket وSSE ببساطة لا يتسامحان مع دلالات الطلب/الرد: ترقية البروتوكول والتدفق الذي يظل مفتوحًا ليس لهما مكافئ NATS.
الحالة الأكثر إثارة للاهتمام هي /v1/db/*، التي يقرر معالجها بنفسه عند كل طلب: تنتقل مسارات الإدارة (المخطط والسياسات وSQL الأولية) دائمًا في NATS إلى aura-db، وينتقل PUT دائمًا إلى NATS (يرجع PostgREST 405 عند الاستبدال الكامل)، وينتقل مشروع MongoDB دائمًا إلى NATS - ولا يذهب إلا CRUD في مشروع Postgres مع مثيل PostgREST المخصص الذي تم حله إلى Direct HTTP. إذا لم يتم حل هذا المثيل المخصص، تستجيب البوابة 503 بدلاً من الرجوع إلى PostgREST مشترك: يُفترض أنه تم إغلاق الفشل، وليس احتياطيًا صامتًا متدهورًا. تنطبق رؤوس الأمان (CSP الصارم، لا توجد بيانات اعتماد CORS) بشكل موحد على جميع هذه المسارات، الموضوعة في نهاية السلسلة، قبل أن تغادر الاستجابة البوابة.
ميزانية المهلة لكل مسار، وليست المهلة الشاملة
تطبق البوابة TimeoutLayer لكل مجموعة من المسارات بدلاً من المهلة العامة - وهو خيار مرتبط بنفس آليات التراص مثل ترتيب البرامج الوسيطة. يحتاج مسار وظائف Edge إلى ميزانية أطول بكثير من الباقي (يمكن تشغيل الوظيفة بشكل قانوني لعدة دقائق): الافتراضي للمستودع هو 30 ثانية لغالبية المسارات، مقارنة بـ 380 ثانية لـ /v1/functions/*.
إن تكديس TimeoutLayer عالمي واحد فوق كل شيء كان من شأنه أن يؤدي إلى قطع المجموعتين بنفس الحد: دائمًا ما تكون أقصر مهلة موضوعة في الموضع الخارجي هي التي تفوز، بغض النظر عن المهلة الأطول الموضوعة في الداخل. الطريقة الوحيدة لمنح الوظائف ميزانية منفصلة هي عدم الدخول مطلقًا في غلاف مشترك: كل فرع من المسارات يحمل TimeoutLayerالخاص به، والذي يتم وضعه قبل دمج جهازي التوجيه - ولا يتم تطبيق مهلة عامة بعد ذلك.
أعد إنتاج هذا النمط في مكان آخر: القائمة المرجعية
- منفصل عن طريق PLAN (سطح التعرض)، وليس عن طريق الخدمة: يجب ألا تصل حزمة SDK العامة المخترقة أبدًا إلى قائمة أصل CORS بلوحة تحكم المشرف الخاصة بك.
- احتفظ بحالة مشتركة واحدة بدلاً من عمليتي نشر منفصلتين - تكرار منطق الأعمال يكلف أكثر من تكرار جهاز التوجيه.
- تحقق من ترتيب البرامج الوسيطة الفعلي عن طريق تتبعه من آخر
.layer()، وليس من القراءة الخطية للملف أبدًا. - حد منفصل للمعدل لكل IP (قبل المصادقة) عن الحصة لكل ممثل (بعد) - وإلا فإن التدفق غير المصادق يفرض تحققًا مكلفًا بدون حدود.
- ضع القاطع في أقرب مكان ممكن من مكالمة الشبكة الفعلية، في الوكيل - وحجمه حسب الهدف عندما لا تتم مشاركة المجال الخطأ.
- قم بإعادة تشغيل الطلب فقط عند إثبات عدم التسليم، ولا يتم ذلك مطلقًا عند انتهاء مهلة بسيطة.
- امنح كل مجموعة مسارات ميزانية المهلة الخاصة بها التي تم تعيينها قبل دمج أجهزة التوجيه - وليس
TimeoutLayerعموميًا من شأنه أن يحل محل الميزانية الأطول.