← All posts

نُشغّل نظام مصادقة بأكمله على مخازن لا تعرف سوى get و put

Authagonal·July 10, 2026
storagearchitectureazure

يبدو نظام المصادقة وكأنه يريد قاعدة بيانات علائقية. مستخدمون، أدوار، جلسات، منح OAuth، رموز تحديث، كلها متقاطعة المراجع. أما نظامنا فلا يستخدم واحدة. إنه يعمل على Azure Table Storage، وهو مخزن يمنحك مفتاح تقسيم (partition key) ومفتاح صف (row key) ولا شيء آخر تقريباً. لا عمليات ربط (joins)، ولا فهارس ثانوية ذات معنى، ولا عامل زيادة، ولا معاملات متعددة الصفوف بالشكل الذي اعتدت عليه. كان ذلك اختياراً. وإليك ما يشتريه هذا الاختيار، والأنماط التي تجعله يعمل، والجزء الذي كانت SQL فيه أسهل بحق.

السبب في فعل ذلك هو التكلفة والبساطة التشغيلية. لا تجمّع اتصالات يمكن أن ينفد، ولا نسخة قاعدة بيانات تحتاج إلى تحديد حجمها أو ترقيعها أو تحقيق تجاوز الفشل لها، وتوسّع لكل قسم تحصل عليه مجاناً. لا يكلّف الاحتفاظ بجدول شيئاً يُذكر، ولا يوجد شيء قد ينفد. وثمن ذلك هو مخزن بلا مخطِّط استعلامات (query planner)، وهو ما لا يؤتي ثماره إلا حين تكون أنماط وصولك متوقَّعة بقدر ما المخزن غبي. وأنماط المصادقة كذلك، ولهذا يتّكئ التصميم بأكمله على هذه الحقيقة.

المفتاح يؤدي العمل الذي كان تخطيط الجدول سيؤديه. مع غياب عمليات الربط، تصمّم المفتاح بحيث يصير الاستعلام هو المفتاح نفسه. تُلغي التطبيع عمداً، وتختار الأقسام بحيث تكون قراءاتك الساخنة عمليات جلب نقطية، وتُرمّز الهويات المركّبة مباشرةً في مفتاح الصف، مثل "{pk}|{rk}" للمدخلات التي تحتاج إلى هوية مركّبة في مخزن لا يوفّر سوى خانة واحدة لمفتاح الصف. لا بد من الإجابة عن سؤال "كيف أبحث عن هذا" حين تصمّم المفتاح، لا وقت الاستعلام. وهذا لا بأس به في المصادقة، لأن أنماط الوصول معروفة ولا تنحرف: اجلب مستخدماً بمعرّفه، اجلب مِنح مستخدم، استهلك رمزاً. أنت لا تستكشف البيانات. أنت تعرف كل سؤال مسبقاً.

تحتفظ بسجل تغييرات (change log) خاص بك. كل صف يحمل حقل Timestamp تديره الخوادم، ومن المغري أن تبني عليه نسخاً احتياطياً تراكمياً: اسحب كل شيء حيث Timestamp gt watermark. هذا يعمل في العرض التوضيحي وينهار في بيئة الإنتاج، لأن المخزن يفهرس مفتاح التقسيم ومفتاح الصف ولا شيء غيرهما. حقل Timestamp ليس في أي فهرس، فالتصفية عليه تفحص كل صف في الجدول. إنه مسح كامل يرتدي زيّ الاستعلام، ويزداد بطئاً مع كل يوم يكبر فيه الجدول. لذا يكتب المخزن سجل تغييراته الخاص بدلاً من ذلك. كل تعديل، كل عملية إدراج أو تحديث (upsert) وكل حذف، يُلحق صفاً بجدول سجل التغييرات مفتاحه اسم الجدول المنطقي، مع المركّب "{pk}|{rk}" بوصفه مفتاح الصف، وراية تبيّن ما إذا كان الصف قد كُتب أم حُذف. الآن يصبح سؤال "ما الذي تغيّر منذ العلامة المائية (watermark)" قراءةً لقسم صغير مفهرس واحد بدلاً من مسح الجدول بأكمله، ويقرأ النسخ الاحتياطي نقطياً الصفوف التي تحرّكت فعلاً فقط. أنت تعيد بناء التقاط تغيّر البيانات (change-data-capture) من عمليات كتابة عادية، وهو يتوسّع مع معدّل التغيّر بدلاً من حجم الجدول.

التزامن من دون معاملات. لا وجود لـ SELECT ... FOR UPDATE هنا. ما تحصل عليه بدلاً منه بدائيّة ذرّية واحدة: الكتابة المشروطة، تُسلَّم إليك على هيئة ETag. لا تكتب صفاً إلا إذا كانت النسخة التي قرأتها لم تتغيّر من تحتك. كل ما يحتاج إلى أمان في ظل الوصول المتزامن يُعبَّر عنه بالتزامن التفاؤلي (optimistic concurrency) مع إعادة المحاولة عند التعارض. وأوضح مثال هو عدّاد قفل الحساب. يحتاج AccessFailedCount إلى الزيادة بشكل ذرّي، لكن المخزن لا يملك عامل زيادة. لذا تقرأ الصف، وترفع العدّاد، وتعيد كتابته مشروطاً بالـ ETag الذي قرأته، وتعيد المحاولة إن سبقك أحد إليه. تلك الحلقة هي كيف تجعل العدّاد ذرّياً على مخزن لا يملك عدّاداً. أطلِق خمسين كلمة مرور خاطئة دفعةً واحدة وستُسجَّل كل واحدة منها، لأن التعارض يُكتشف ويُعاد فيه بدلاً من أن يُفقد.

الحذف يمكن أن يكون قفلاً. الاستهلاك ذو الاستخدام الواحد، رمز تفويض أو رمز لمرة واحدة يجب أن يُستبدل مرةً واحدةً بالضبط، هو حذف مشروط. إنه حذف مشروط بالـ ETag: إن نجح الحذف فقد فزت بالسباق ويمكنك المتابعة؛ وإن فشل لأن الصف قد اختفى أصلاً، فقد استهلكه غيرك أولاً وعليك التوقف. الحذف هو القفل. الفكرة نفسها تؤدي انتخاب القائد (leader election)، مبنيةً على إيجار كائن ثنائي (blob lease)، قفل مصنوع من كتابة ذرّية بدلاً من خدمة تنسيق منفصلة. نادراً ما تحتاج إلى خدمة أقفال حين تكون الكتابة نفسها ذرّية.

يجب أن يكون الحذف من الدرجة الأولى، وهذا نصف سبب وجود سجل التغييرات. مخزن المفتاح-القيمة لا يملك علامة حذف: الصف المحذوف يتلاشى ببساطة، فأي شيء يمسح بحثاً عن التغييرات لن يستطيع حتى أن يرى أنه رحل. النسخ الاحتياطي المبني على طوابع زمنية للصفوف كان سيحمل المستخدم المحذوف إلى الأمام إلى الأبد بهدوء. سجل التغييرات يسجّل الحذف بوصفه حذفاً، فتعيد عملية الاستعادة تشغيله بدلاً من أن تبعثه من جديد. نمط الفشل ذاك، نسخ احتياطي يعيد بأمانة كل من أزلته، قصة كاملة بحد ذاتها وتستحق منشوراً خاصاً بها، لكن النمط ينتمي إلى هذه القائمة: في مخزن بلا سجل حذف، تحتفظ بسجل الحذف بنفسك.

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

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

هذه الأنماط هي محرك التخزين الكامن خلف Authagonal: تصميم مفتاح-قيمة لا يكلّف تشغيله شيئاً يُذكر، وهو جزء كبير من كيف نضع كل ميزة في كل خطة.