الوظيفة:
مطور باك اند
المستوى:
خبير
عدد الأسئلة:
١٢ سؤالاً
الزمن:
٦ دقائق

أسئلة انترفيو Senior Back End مع الإجابة

قرارات ومواقف صعبة يُسأل عنها الخبراء في مقابلات وظيفة مطور باك اند: الأداء والتخزين المؤقت، والاختبارات والنشر، والبرمجة غير المتزامنة والطوابير، والمصادقة وأمن التطبيقات، وتصميم واجهات REST البرمجية، وقواعد البيانات والفهارس والمعاملات. في آخرها تقرير بنقاط ضعفك.

المهارات: البرمجة غير المتزامنة والطوابير، قواعد البيانات والفهارس والمعاملات، الاختبارات والنشر، تصميم واجهات REST البرمجية، المصادقة وأمن التطبيقات، الأداء والتخزين المؤقت

تعليمات: لكل سؤال إجابة صحيحة واحدة. إن لم تعرف الإجابة فاختر «لا أعرف»، وسيدلّك التقرير على ما تراجعه.

السؤال الأول: تحفظ خدمة الطلبات الطلب الجديد في PostgreSQL، ثم تنشر الحدث OrderCreated إلى Kafka مباشرة بعد commit. لاحظ الفريق أن بعض الطلبات لا يصل حدثها أبداً إلى الخدمات الأخرى عندما تتعطل النسخة أو يُعاد تشغيلها بين الخطوتين. ما الحل الأمتن؟

اختر إجابتك لتبدأ الاختبار

كل الأسئلة مع الإجابات (١٢ سؤالاً)

جرّب المقابلة أولاً، ثم افتح الإجابات لتراجعها مع الشرح.
  1. ١. تحفظ خدمة الطلبات الطلب الجديد في PostgreSQL، ثم تنشر الحدث OrderCreated إلى Kafka مباشرة بعد commit. لاحظ الفريق أن بعض الطلبات لا يصل حدثها أبداً إلى الخدمات الأخرى عندما تتعطل النسخة أو يُعاد تشغيلها بين الخطوتين. ما الحل الأمتن؟

    • كتابة الحدث في جدول outbox ضمن معاملة حفظ الطلب نفسها، ثم نشره إلى Kafka بعملية مستقلة تقرأ هذا الجدول
    • ضبط acks=all في المنتج (producer) كي لا يضيع أي حدث بعد إرساله، حتى لو تعطل أحد وسطاء Kafka
    • إحاطة استدعاء النشر بإعادة محاولة تلقائية مع exponential backoff حتى ينجح بعد commit مهما طال انقطاع Kafka
    • نشر الحدث إلى Kafka قبل commit داخل المعاملة نفسها، فإذا فشل النشر يُلغى حفظ الطلب تلقائياً ولا يبقى طلب بلا حدث

    الشرح: نمط transactional outbox يجعل حفظ الطلب وتسجيل الحدث عملية ذرية واحدة، فلا يضيع الحدث مهما تعطلت الخدمة، ثم يُنشر مرة واحدة على الأقل لذا يجب أن يكون المستهلكون idempotent. النشر قبل commit يعكس المشكلة فقط إذ قد يُنشر حدث لطلب فشل حفظه، وإعادة المحاولة لا تنقذ حدثاً كان في ذاكرة عملية توقفت.

  2. ٢. في نظام مناوبات أحد المستشفيات يجب أن يبقى طبيب واحد على الأقل مناوباً في كل وردية. عندما يطلب طبيب إجازة، تنفّذ الخدمة هذين الاستعلامين داخل معاملة واحدة في PostgreSQL: SELECT count(*) FROM shifts WHERE shift_id = 7 AND on_call = true; -- only if count >= 2: UPDATE shifts SET on_call = false WHERE shift_id = 7 AND doctor_id = $1; طلب الطبيبان المناوبان الوحيدان إجازة في اللحظة نفسها، فنجحت المعاملتان وبقيت الوردية بلا مناوب، مع أن الفريق رفع مستوى العزل إلى REPEATABLE READ. ما السبب وما الحل الصحيح؟

    • هذا non-repeatable read، والحل تكرار استعلام العدّ مباشرة قبل UPDATE للتأكد من أن القيمة لم تتغير بين الاستعلامين
    • هذا write skew لأن كل معاملة تعدّل صفاً مختلفاً، والحل مستوى SERIALIZABLE مع إعادة المعاملة عند فشل التسلسل
    • استعلام العدّ بطيء فتتسع نافذة التزامن، والحل فهرس على (shift_id, on_call) يجعل الفحص والتعديل شبه لحظيين
    • هذا dirty read، والحل خفض مستوى العزل إلى READ UNCOMMITTED كي ترى كل معاملة تعديل الأخرى قبل commit

    الشرح: تقرأ المعاملتان اللقطة نفسها وتعدّل كل منهما صفاً مختلفاً، فلا يرى REPEATABLE READ القائم على عزل اللقطة أي تعارض كتابة ويقبل الاثنتين، وهذا هو write skew. أما SERIALIZABLE في PostgreSQL فيكتشف هذا النمط ويُفشل إحدى المعاملتين بخطأ serialization_failure، لذا يجب أن يعيد التطبيق تنفيذها.

  3. ٣. نفّذت أداة الترحيل هذا الأمر على جدول orders في قاعدة PostgreSQL الإنتاجية، وفيه 200 مليون صف: CREATE INDEX idx_orders_customer ON orders (customer_id); طوال 20 دقيقة استمرت القراءات طبيعياً، لكن كل عمليات INSERT وUPDATE على الجدول علقت حتى انتهى بناء الفهرس. ما الطريقة الصحيحة لإضافة فهرس كهذا في المرة القادمة؟

    • استخدام CREATE INDEX CONCURRENTLY خارج أي معاملة، مع التحقق بعده من أن الفهرس لم يبقَ INVALID
    • رفع maintenance_work_mem قبل الأمر ليكتمل البناء أسرع، فتقصر مدة حجب الكتابات من 20 دقيقة إلى دقائق قليلة
    • تشغيل VACUUM FULL على الجدول قبل إنشاء الفهرس لتقليص حجمه، فيُبنى الفهرس أسرع ويقل زمن الحجب
    • تنفيذ الأمر داخل معاملة صريحة مع بقية الترحيلات، كي يُتراجع عنه تلقائياً إن طال حجب الكتابات أكثر من اللازم

    الشرح: الأمر العادي CREATE INDEX يقفل الجدول أمام الكتابة حتى ينتهي البناء، بينما يبني CONCURRENTLY الفهرس دون حجب الكتابات مقابل وقت أطول، ولا يمكن تشغيله داخل كتلة معاملة، وإن فشل يترك فهرساً INVALID يجب حذفه وإعادة بنائه. تسريع البناء يقصّر الحجب ولا يلغيه، وVACUUM FULL أسوأ لأنه يقفل الجدول حتى أمام القراءة.

  4. ٤. يرسل تطبيق الجوال الطلب POST /payments لخصم مبلغ من بطاقة العميل. على الشبكات الضعيفة تنتهي مهلة الطلب لدى التطبيق مع أن الخادم أتم الخصم فعلاً، فيعيد التطبيق المحاولة ويُخصم المبلغ مرتين. ما التصميم الصحيح لواجهة الدفع؟

    • إيقاف إعادة المحاولة التلقائية في التطبيق، وإظهار رسالة تطلب من العميل مراجعة كشف حسابه قبل الدفع مجدداً
    • استبدال POST بـ PUT على المسار نفسه /payments، لأن المواصفة تجعل PUT آمنة التكرار (idempotent) فلا تُنشأ الدفعة مرتين
    • رفض أي دفعة تطابق دفعة سابقة في المبلغ ورقم البطاقة خلال آخر 60 ثانية، واعتبارها إعادة للطلب نفسه دون تغيير في التطبيق
    • يرسل التطبيق مفتاح Idempotency-Key فريداً لكل دفعة ويكرره عند الإعادة، فيعيد الخادم النتيجة المحفوظة دون خصم جديد

    الشرح: مفتاح Idempotency-Key يربط كل المحاولات بعملية دفع واحدة، فيميّز الخادم الإعادة من دفعة جديدة مقصودة ويعيد الاستجابة الأصلية دون تنفيذ ثانٍ. تغيير الطريقة إلى PUT لا يجعل الخادم آمن التكرار من تلقاء نفسه، ومطابقة المبلغ والبطاقة ترفض دفعات مشروعة متطابقة ولا تلتقط إعادة تصل بعد 60 ثانية.

  5. ٥. نقطة النهاية POST /reports تولّد تقريراً سنوياً يستغرق بين 3 و10 دقائق، بينما مهلة موازن الأحمال (load balancer) 60 ثانية، فيتلقى العملاء 504 Gateway Timeout ويعيدون الطلب، فتتراكم عمليات توليد مكررة على الخادم. ما التصميم الأنسب؟

    • رفع مهلة موازن الأحمال والعميل إلى 15 دقيقة، مع إبقاء التوليد متزامناً داخل الطلب نفسه كي تبقى البنية بسيطة ولا تحتاج إلى طابور
    • إبقاء الاتصال مفتوحاً وإرسال مسافات فارغة إلى العميل كل 30 ثانية، حتى لا يعدّ موازن الأحمال الاتصال خاملاً فيقطعه
    • إرجاع 202 Accepted فوراً مع معرّف مهمة يتابع العميل حالتها عبر GET /reports/jobs/{id}، وتوليد التقرير في worker بالخلفية
    • إرجاع 408 Request Timeout فوراً ليعيد العميل المحاولة بعد دقائق، حين يكون التقرير قد جهز في الذاكرة المؤقتة

    الشرح: العمليات التي تستغرق دقائق لا تناسب طلباً متزامناً، والنمط المعياري أن يقبل الخادم الطلب بـ 202 Accepted ويعطي العميل مورداً يتابع منه الحالة حتى يجهز الناتج. رفع المهلات يحجز الاتصالات والعمال دقائق طويلة ويبقى هشاً أمام أي انقطاع، فتعود الطلبات المكررة نفسها.

  6. ٦. تنشر خدمة الطلبات الأحداث OrderCreated وOrderPaid وOrderShipped إلى موضوع (topic) في Kafka فيه 12 قسماً (partition) دون تحديد مفتاح للرسالة. أحياناً يعالج المستهلكون OrderShipped قبل OrderPaid للطلب نفسه. ما الحل الذي يحفظ الترتيب ويُبقي المعالجة المتوازية؟

    • ضبط acks=all في المنتج كي لا يُؤكَّد أي حدث قبل أن تكتبه كل النسخ المتماثلة بترتيب وصوله
    • تقليص الموضوع إلى قسم واحد، لأن Kafka يضمن الترتيب داخل القسم الواحد فقط
    • إضافة طابع زمني إلى كل حدث، وجعل كل مستهلك يرتب ما يستلمه من أحداث حسب هذا الطابع قبل معالجتها
    • استخدام order_id مفتاحاً للرسالة، فتذهب كل أحداث الطلب الواحد إلى القسم نفسه

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

  7. ٧. في Kubernetes يستدعي livenessProbe لخدمتك المسار /health، وهو يتحقق من الاتصال بقاعدة البيانات. أثناء failover لقاعدة البيانات دام دقيقتين، أعاد Kubernetes تشغيل كل الـ pods في وقت واحد، فطال تعافي الخدمة بسبب الإقلاع البارد وسيل الاتصالات الجديدة. ما الإصلاح الصحيح؟

    • حذف livenessProbe وreadinessProbe كليهما، والاكتفاء بمراقبة معدل الأخطاء في لوحات المتابعة والتنبيهات
    • رفع initialDelaySeconds في livenessProbe إلى دقيقتين، كي تُمنح كل نسخة وقتاً كافياً للاتصال بقاعدة البيانات قبل الفحص
    • توسيع /health ليفحص Redis وبقية التبعيات أيضاً، كي يكتشف Kubernetes العطل في أي جزء منها ويعالجه فوراً
    • جعل livenessProbe يتحقق من استجابة العملية نفسها فقط، دون أي اعتماد على قاعدة البيانات أو خدمات خارجية

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

  8. ٨. أنت القائد التقني للفريق. بعد عشر دقائق من نشر الإصدار 2.4، وهو لا يتضمن أي تغيير في مخطط قاعدة البيانات، ارتفعت نسبة أخطاء 5xx من 0.1% إلى 8% وبدأ العملاء يشتكون. ما أفضل تصرف الآن؟

    • إبقاء الإصدار الحالي والتحقيق مباشرة على الإنتاج حتى يُعرف السبب الجذري، كي لا تضيع الأدلة بالتراجع
    • التراجع فوراً إلى الإصدار 2.3 لاستعادة الخدمة، ثم تحليل السبب الجذري من السجلات بعد الاستقرار
    • كتابة إصلاح سريع ونشره مباشرة على الإنتاج متجاوزين خط CI لتوفير الوقت، لأن التراجع قد يسبب مشكلات أخرى
    • الانتظار نصف ساعة لمعرفة ما إذا كانت الأخطاء ستنخفض من تلقاء نفسها بعد امتلاء الذاكرة المؤقتة

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

  9. ٩. تبني تطبيق جوال لنظامي iOS وAndroid يسجّل دخول المستخدمين عبر خادم التفويض (authorization server) الخاص بشركتك وفق OAuth 2.0. أي تدفق (flow) هو الصحيح لهذا التطبيق؟

    • Authorization Code مع PKCE عبر متصفح النظام، دون أي client_secret داخل التطبيق
    • Resource Owner Password Credentials، إذ يجمع التطبيق اسم المستخدم وكلمة المرور ويرسلهما إلى نقطة token
    • Implicit الذي يعيد access token مباشرة في جزء الرابط (fragment) دون خطوة تبادل إضافية
    • Authorization Code مع client_secret مخزّن داخل التطبيق بعد تشفيره وإخفائه بأدوات obfuscation

    الشرح: تطبيق الجوال عميل عام لا يستطيع حماية أي سر، فـ client_secret يُستخرج من الملف التنفيذي مهما أُخفي، بينما يربط PKCE رمز التفويض بالنسخة التي بدأت الطلب عبر code_verifier فلا ينفع اعتراضه. أما Implicit وResource Owner Password Credentials فتوصي أفضل ممارسات أمان OAuth 2.0 الحالية بعدم استخدامهما.

  10. ١٠. ميزة جديدة تتيح للمستخدم إدخال رابط صورة ليحمّلها الخادم ويصنع منها صورة مصغرة. أدخل مختبِر الاختراق عنوان خدمة البيانات الوصفية في السحابة 169.254.169.254 فحصل على بيانات اعتماد الخادم. أضاف الفريق فحصاً يرفض الروابط التي يكون مضيفها localhost أو عنوان IP داخلياً مكتوباً نصاً، فتجاوزه المختبِر بنطاق يملكه يُحَلّ (resolve) إلى 127.0.0.1. ما الإصلاح الصحيح؟

    • التحقق من أن ترويسة Content-Type في الاستجابة تبدأ بـ image/ قبل حفظها، ورفض أي استجابة أخرى دون عرض محتواها للمستخدم
    • فحص العنوان الناتج بعد حلّ الاسم ورفض النطاقات الداخلية، ثم الاتصال بهذا العنوان نفسه مع إعادة الفحص عند كل redirect
    • توسيع القائمة السوداء بتعبيرات نمطية (regex) تلتقط الصيغ العشرية والست عشرية والمختصرة لعناوين IP الداخلية وكل أشكال localhost
    • قبول الروابط التي تبدأ بـ https فقط، لأن خدمات البيانات الوصفية والخدمات الداخلية تعمل عادة على http دون تشفير

    الشرح: أي فحص لنص الرابط يُتجاوز بالأسماء وصيغ العناوين البديلة وإعادة التوجيه، لذا يجب فحص العنوان الفعلي بعد DNS والاتصال به هو نفسه، كي لا يتغير الحل بين الفحص والاتصال كما في DNS rebinding. فحص Content-Type لا يمنع الطلب الداخلي أصلاً، فالخادم يكون قد وصل إلى الخدمة الداخلية قبل قراءة الترويسة.

  11. ١١. وضع الفريق شبكة CDN أمام الواجهة البرمجية لتخفيف الحمل. نقطة النهاية GET /me/account تعيد بيانات الحساب حسب cookie الجلسة مع الترويسة Cache-Control: max-age=300. بعد النشر أبلغ بعض المستخدمين أنهم رأوا بيانات حسابات أشخاص آخرين. ما الإصلاح الصحيح؟

    • إرسال Cache-Control: private أو no-store مع هذه الاستجابة كي لا تخزنها أي ذاكرة مؤقتة مشتركة
    • إضافة s-maxage=60 مع إبقاء max-age=300، كي تحتفظ CDN بالنسخة مدة أقصر بكثير من متصفح المستخدم
    • خفض max-age إلى 10 ثوانٍ، كي تنتهي صلاحية النسخة المخزنة قبل أن يطلبها مستخدم آخر في الغالب
    • إضافة Vary: Accept-Encoding كي تحتفظ CDN بنسخة منفصلة لكل عميل بدل نسخة واحدة مشتركة بين الجميع

    الشرح: الترويسة max-age وحدها تسمح لأي ذاكرة مؤقتة مشتركة مثل CDN بتخزين الاستجابة وتقديمها لكل من يطلب المسار نفسه، بينما يقصر private التخزين على متصفح المستخدم ويمنعه no-store كلياً. تقصير مدة التخزين أو ضبط s-maxage يقلل التسريب ولا يلغيه.

  12. ١٢. تستخدم خدمتك Redis Cluster من ستة أقسام (shards) لتخزين بيانات المنتجات مؤقتاً لمدة خمس دقائق. خلال عرض كبير صار المفتاح product:9001 الخاص بالمنتج المميز يتلقى 200 ألف قراءة في الثانية، فبلغ استهلاك المعالج في قسم واحد 100% بينما الأقسام الأخرى شبه خاملة، وارتفع زمن الاستجابة. ما الحل الأنسب؟

    • تخزين هذا المفتاح في ذاكرة محلية داخل كل نسخة من التطبيق لبضع ثوانٍ فقط
    • زيادة عدد الأقسام في Redis Cluster من ستة إلى اثني عشر قسماً، ليتوزع الحمل على عدد أكبر من الخوادم
    • رفع مدة صلاحية المفتاح من خمس دقائق إلى ساعة كاملة، كي لا يُعاد تحميله من قاعدة البيانات أثناء العرض
    • تغيير maxmemory-policy إلى allkeys-lfu كي يعطي Redis أولوية للمفاتيح الأكثر طلباً ويخدمها أسرع

    الشرح: كل مفتاح في Redis Cluster ينتمي إلى hash slot واحد يقع على قسم واحد، لذا لا توزع إضافة الأقسام قراءات مفتاح ساخن (hot key) واحد. الذاكرة المحلية القصيرة تخدم أغلب القراءات من ذاكرة كل نسخة من التطبيق، مقابل تأخر مقبول لثوانٍ في ظهور التحديثات.

المزيد