مدونة المُظلي

كلام عن تطوير البرمجيات والمبرمجين وما حولهما

Sunday, November 27, 2016

رهوان: كيف طورنا مواقع أسرع استجابة

مقدمة:
في هذه التدوينة، أكتب عن المراحل التي مررت بها شخصياً في تطوير مواقع الويب الإخبارية اعتماداً على dot net stack خلال السنوات الماضية، وأكتب عن "رهوان" وهو ال Data Access Layer الذي نعتمد عليه حالياً، والذي  ساعدنا في تحسين وقت استجابة أحد المواقع من 250 ملي ثانية وصولاً إلى 10 ملي ثانية وفي نفس الوقت خفض الحمل على قاعدة البيانات من 800 patch request/second وصولاً إلى 150 patch request/second

الزيتونة:
في أبسط صوره، "رهوان" ليس أكثر من data caching layer تم إضافته كطبقة وسيطة بين قاعدة البيانات وبين صفحات الموقع، هدف هذه الطبقة الأساسي هو توفير ال  Data Model المطلوبة لأي صفحة ويب، الوضع المثالي -والذي يتم الوصول له في معظم الأحيان- أن تتم العملية ك direct key/value retrieve operation  بحيث تطلب أي صفحة ال Data Model الخاص بها فيأتي محمل "بكل" البيانات المطلوبة "فوراً" بدون النزول إلي قاعدة البيانات، وأي تغير يحدث في قاعدة البيانات يتم إعادة بناء ال Data Model التي تعتمد على هذه البيانات بالكامل، ويتم ذلك في background tasks  .. وهو ما سنحاول شرحه في هذه التدوينة
ملاحظة: المرحلة الأولى والثانية هي استعراض لكيفية تطور الحل، يمكنك تخطيهما إن أردت

المرحلة الأولى: كود ملخبط بطئ :)
مررت  كمعظم المطورين  على مرحلة كل الإشارات بها "خضراء" فأي فرد من الفريق يريد كتابة أي كود في أي مكان بأي طريقه، فهو موضع ترحاب طالما ستكون النتيجة كود "شغال" حيث العميل عادة في غاية التعجل لإطلاق موقعه على الانترنت، وهو ما يضغط بشدة على فريق التطوير وتتحول الأولوية إلي بناء موقع "شغال" ويتم تأجيل أي حديث عن "جودة" الكود أو "تنسيق" أفضل بين أفراد الفريق، وتكون في العادة صفحات الموقع هي المسؤولة عن جلب كل ما تحتاجه من بيانات، كمثال:


كما هو واضح، يبدو الكود في هذه المرحلة كأنه بداية جيدة لوجبة "مكرونة اسباجتي" تقنية، فليس هناك نسق واحد، بل أن مهمة جلب البيانات تقع على ال Controller نفسه، ثم يتم نقل البيانات من ال Controller إلي ال View بطريقتين مختلفتين (ViewModel and ViewBag) واثناء عرض هذه البيانات نفسها، تم استعمال Html.RenderAction  مرة وتم استعمال Html.Partial مرة

صعب أن يصمد هذا الكود امام طلبات التعديل المستمرة من العميل، أو من طلبات حل ال bugs التي يعمل عليها أكثر من مطور، في العادة يتم حل bug في منطقة، فتتوقف منطقة أخرى كانت تعمل بدون مشاكل، وإن تم حل ال bugs كلها تظهر مشكلة بطء الموقع الملاحظ وهو ما يستهلك وقت ومجهود كبير للوصول إلي استقرار نسبي للموقع، بعد فترة يقرر الفريق أنهم عليهم أخذ فرصة لالتقاط الأنفاس ووضع حل نهائي لهذه المشاكل .. وهو ما سيصل بنا إلي

المرحلة الثانية: كود منظم (بعض الشئ)
بعد مواجهة مشاكل عديدة في المرحلة السابقة يقر الفريق -وأنا أيضاً- أنه ليس كل الإشارات الخضراء خير، وأن علينا وضع بعض الإشارات "الحمراء" على الطرق المؤدية للمشاكل، ويبدأ الاتفاق على خطوط عريضة موحدة Design Guidelines في تصميم وكتابة الكود
ما يلي هي  أمثلة حقيقية لبعض الإشارات الحمراء التي اتفقنا عليها في الفريق الذي أعمل معه
  • لا تستعمل ViewBag or ViewData إلا للضرورة 
  • لا تستعمل Html.RenderAction إلا للضرورة
وهذا هو مثالنا بعد اجتيازه المرحلة الثانية

في هذه المرحلة تم تحسين وضع الكود عن طريق:
  1. استعمال ال Unit Of Work pattern كوسيط بين قاعدة البيانات وبين ال Controller وهو ما يفتح الباب لعمل optimization للطريقة التي يتم جلب البيانات بها من قاعدة البيانات بدون لمس أي Controller بعد ذلك
  2. الاعتماد على ال Dependency Injection لتقديم ال Unit Of Work المطلوبة لل Controller
  3. الاعتماد على ال ViewModel كطريقة وحيدة لنقل البيانات من ال Controller ل ال View والذي من المفترض أن يساعد أي مطور في الفريق على معرفة كل البيانات المطلوبة لرسم أي View بالقاء نظرة واحدة على ال ViewModel الخاص به، ما يعني أخطاء أقل
  4. تم الاستغناء عن RenderAction فهي تتطلب إنشاء نسخة جديدة من ال Controller المستهدف وتمشي في مسار أطول وأبطئ من RenderPartial فإن لم يكن هناك سبب وجيه فال RenderPartial هي الإختيار الأول لنا
  5. استعمال OutputCache والذي سيجعل ال asp.net process تحتفظ بالنتيجة التي خرجت من ال Index Action لمدة 60 ثانية، قبل إعادة طلب النتيجة من جديد من ال Controller وهو ما يساعدنا في تحسين وقت استجابة الموقع هنا

المرحلة الثالثة: رهوان
لفهم رهوان، دعونا نجري مقابلة معه

أنا: السلام عليكم
رهوان: وعليكم السلام

أنا: ممكن تعرف الناس بك
رهوان: أنا Data Access Layer مُحَسنّ، مسؤول عن جلب البيانات لل Presentation Layer

أنا: لماذا تقول أنك "مُحَسنّ" ماذا يميزك عن أي Data Access Layer آخر
رهوان: أنا مصمم بحيث يكون انسياب البيانات غالباً في اتجاه واحد

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

أنا: هذا عن المشكلة، فماذا عن الحل الذي جئت انت به؟
رهوان: الحل هو نقل كل البيانات الهامة أولاً بأول من قاعدة البيانات إلي ال Data Caching Engine حتى لو لم يتم طلبها بعد في موقع الويب، بذلك نضمن جاهزية دائمة للبيانات وانسياب في اتجاه واحد، من قاعدة البيانات إلي الذاكرة إلي صفحة الويب

أنا: وكيف تم تحقيق ذلك؟
رهوان: عن طريق هذا التصميم
أنا: ماذا يعني هذا المخطط/التصميم
رهوان: يوضح المكونات التي اعتمد عليها لإنجاز مهمتي وهي:
  • Change Detector: مهمته التقاط أي تغير حصل في قاعدة البيانات وإعلام ال Cached Repository بذلك
  • Cached Repository: مهمتها توفير البيانات لأي طرف يطلبها، وتحاول توفيرها من ال Cache Engine مباشرة إن استطاعت أو جلبها من ال Repository أولاً ثم وضعها في ال Cache Engine ثم توفيرها لمن طلبها
  • Cache Engine: مهمته الاحتفاظ بالبيانات في الذاكرة in-memory
  • Repository: مهمتها النزول الفعلي لقاعدة البيانات لجلب البيانات المطلوبة
أنا: ولكن أي ORM مثل Entity Framework يمكنه أن يفعل كل ما ذكرته انت، فلم كل هذا؟
رهوان: شتان بين الطريقتين، ف Entity Framework مثلاً غير مصمم ك Thread Safe أي لا يمكن لأكثر من صفحة التعامل مع نفس ال Entity Framework Context بينما رهوان مصمم ليكون نقطة تواصل واحدة مع جميع صفحات الموقع

أنا: ممكن نتصرف في موضوع ال Thread Safe بطريقة أو بأخرى، ونعتمد على ال ORM بدلاً من كل هذا التعقيد
رهوان: بهذا الشكل تكون حللت نصف الأحجية فقط، فكيف ستحل النصف الآخر؟

أنا: وما هو النصف الآخر؟
رهوان: الاستعلامات البطئية على قاعدة البيانات، فتكفي خمس دقائق متابعة ب sp who is active لتكتشف أن ال ORMs عموماً و Entity Framework على سبيل المثال يتعامل مع الاستعلامات المعقدة complex queries بشكل غير كفء، ما ينتج عنه -عند الضغط على الموقع- time out errors كثيرة، بينما فصل ال Data Cache عن طريقة جلب البيانات سيعطي لنا حرية كبيرة في جلبها من أي طريق (مثل dapper) وإعادة كتابة استعلامات محددة بشكل يدوي لزيادة كفائتها بدون التأثير على طريقة ال Caching الخاصة بها

أنا: رجوعاً إلي المخطط الذي وضحته انت منذ قليل، ممكن مثال عن كيف تعمل الأربع مكونات التي تحدثت انت عنها مع بعضها البعض
رهوان: خذ هذا المثال -الحقيقي-
  1. مشرف في موقع إخباري نشر خبر جديد
  2. تم حفظ الخبر الجديد في قاعدة البيانات
  3. التقط ال Change Detector أن هناك خبر جديد في قاعدة البيانات تم نشره ، فنادى على Cached Repository لإحضاره قبل حتى أن يطلبه أي أحد
  4. ال Cached Repository تجلب الخبر من قاعدة البيانات وتحتفظ به في ال Cache Engine
  5. أحد ال Controllers يطلب الخبر، فيتم مناولته مباشرة من ال Cache Engine
لم يتحمل ال Controller في هذه العملية أي تكلفة من تكاليف جلب الخبر من قاعدة البيانات، مما حسن جداً من وقت استجابته على متصفحي الموقع

أنا: معنى ذلك انك ستنسخ قاعدة البيانات بالكامل في ال Cache Engine الخاص بك؟
رهوان: بالطبع لا، فلدي طريقة أحدد بها البيانات التي تستحق الاحتفاظ بها في ال Cache Engine والبيانات التي ستخرج منه

أنا: إذاً هل ممكن بعض التفاصيل عن طريقتك في تحديد البيانات التي ستظل في ال Cache Engine؟
رهوان: في المواقع الإخبارية مثلاً تكون الأهمية القصوى لأخبار آخر شهر، ثم تأتي الأولوية للأخبار ذات معدل التصفح العالي، أما غير ذلك فيمكن أن يظل خارج ال Cache Engine ويتم جلبه من قاعدة البيانات عند الطلب

أنا: إذاً لن يكون انسياب البيانات في اتجاه واحد دائماً
رهوان: لذلك قلت ذكرت كلمة "غالباً" وأنا أتحدث عن هذه الميزة، فنسبة كبيرة من طلبات البيانات ستكون في اتجاه واحد، ونسبة أقل سيتم جلبها من قاعدة البيانات أولاً قبل تقديمها لمن طلبها

أنا: رجوعاً إلي المكونات الأربعة التي تحقق ما تحدثت انت عنه، هل ممكن تفاصيل عن كيف تم بناء كل مكون من هذه المكونات
رهوان: بالتأكيد، مع ملاحظة ان طريقة البناء الداخلية لكل مكون يمكن أن تختلف مع الوقت، إلا أن هذه هي التفاصيل الحالية
  • Change Detector: البناء الحالي تم عن طريق تنفيذ استعلامات دورية sql queries على قاعدة البيانات لمعرفة أي بيانات تم تحديثها وذلك باستخدام تاريخ تعديل البيان، وتم الإعتمادعلى Hangfire لجدولة وتنفيذ هذه الاستعلامات الدورية، لإنه ينفذها في Background Threads داخل موقع الويب نفسه
  • Cached Repository: وهي طبقة رقيقة مهمتها تنسيق العمل بين ال Repository وال Cache Engine لكن أهميتها أنها نقطة التواصل الوحيدة مع ال Presentation Layer 
  • Repository: وهي المكان الذي ينفذ عمليات جلب البيانات من قاعدة البيانات فعلياً، ونعتمد فيه على Dapper بصفة أساسية لسرعته الكبيرة ، ونعتمد أيضاً على Entity Framework في الاستعلامات غيرالمعقدة، وهناك ملاحظة هامة هنا:
    • أحد القوانين الحاكمة هنا هو التقليل من ال sql joins إلي أقل قدر ممكن فمثلاً عملية جلب خبر من قاعدة البيانات لن تجلب معه بيانات كاتب الخبر، بل ستكتفي بجلب رقم كاتب الخبر فقط author id ثم يتم طلب بيانات الكاتب من ال Cached Repository الخاص بالكتاب، فالاحتمال الأكبر أن بياناته موجودة بالفعل في ال Caching Engine فنكسب مرتين، مرة بتقليل ال sql joins ومرة بعدم جلب بيانات الكاتب من قاعدة البيانات بدون داعي
  • Cache Engine: تم بنائه باستخدام static ConcurrentDictionary لبساطته وقدر التحكم الذي نحصل عليه في البيانات الموجودة داخله
أنا: ماذا عن النتائج
رهوان: على مستوى ال Server Side فقد تم قياس النتائج باستخدام Mini Profiler وهذا مثال على الفرق


رهوان: وعلى مستوى قاعدة البيانات فهذا هو الفرق قبل وبعد استعمال رهوان


في الختام:
رهوان ليس الرصاصة الفضية للقضاء على مشاكل البطء لأي موقع ويب، بل أنه ولد من رحم مشكلات المواقع الإخبارية، أما المواقع/التطبيقات التي تعمل بشكل مختلف، ستحتاج حل مناسب أكثر لاحتياجاتها، قد يكون رهوان أو تعديل له أو حتى غيره

تحدثت في هذا المقال عن "فكرة" رهوان، ولم أتطرق لرحلة تنفيذ رهوان نفسها، فقد كانت مليئة بالتفاصيل، أكثر من أن يتحملها مقال واحد، سواء في تفاصيل ال Implementation ، أو في تفاصيل المشاكل التي واجهتنا أثناء التطوير مثل Memory Leaks و Sudden Process Termination  لنكتشف بعد رحلة تشخيص عميقة أنه Unhandled Stackoverflow Exception، إن يسر الله لي، يمكن أن أكتب جزءً آخر من المقال يتحدث بشئ من التفصيل والأمثلة عن كيف نفذنا الفكرة نفسها وكيف صنعت التفاصيل الصغيرة الفارق

وهناك أيضاً تفاصيل في كيفية تطويره مستقبلاً فعلى سبيل المثال نخطط لإعادة بناء ال Change Detector ليعتمد على  Publish/Subscribe pattern بدل الاستعلامات الدورية على قاعدة البيانات مباشرة، ونحتاج تجربة ال Caching Engine إذا تم تشغيله ب Redis فماذا سيكون الفارق

إلا أني حاولت قدر الإمكان الإلمام بالصورة الكاملة مع أهم تفاصيل الحل
أسأل الله أن يكون مقالاً نافعاً

في انتظار تعليقاتكم على الحل واقتراحتكم
والحمد لله رب العالمين

21 comments:

  1. great Article ya Mozaly :)

    ReplyDelete
  2. هو في سؤال، لو قاعدة البيانات كبيرة جدا بيحث أن شرط استحضار البيانات وتجهيزها في الذاكرة يطابق كمية بيانات كبيرة أيضا، ألا يؤثر ذلك على موارد نظام الخادم ككل بحيث قد يحدث خروج عن حدود الذاكرة ومن ثم توقف كامل للخادم او التطبيق، وعندها نصبح أمام مقارنة بين التوقف الكامل او البطئ النسبي، وليست البطئ والسرعة!

    ReplyDelete
    Replies
    1. I'm quoting from Ahmed's conclusion:
      رهوان ليس الرصاصة الفضية للقضاء على مشاكل البطء لأي موقع ويب، بل أنه ولد من رحم مشكلات المواقع الإخبارية، أما المواقع/التطبيقات التي تعمل بشكل مختلف، ستحتاج حل مناسب أكثر لاحتياجاتها، قد يكون رهوان أو تعديل له أو حتى غيره

      Delete
  3. Awesome, perfect as usual Mozaly :D

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. More than excellent article ya Mozaly. Keep up the great work :)

    ReplyDelete
  6. كيف يتم استخدام طريقة رهوان ؟

    ReplyDelete
    Replies
    1. اعمل تحسينات على الكود بتاعك الأول
      بص على الأمثلة المكتوبة في المرحلة الأولى والثانية واتأكد ان الكود بتاعك اتعمل فيه تحسينات المرحلة الثانية
      وبعدين انقل للرحلة الثالثة
      وابني الأربع مكونات بتوع رهوان وخلي ال
      UnitOfWork
      تستعملهم

      Delete
  7. Excellent
    How much is the time of hangfire jobs?

    ReplyDelete
    Replies
    1. Hangfire has two types of jobs
      * recurring (can not be scheduled less than 1 minute)
      * normal (can be scheduled anytime, but manually not by hangfire itself)
      --
      we usually use recurring jobs (1 minute) and waiting Hangfire to support cron jobs by seconds

      Delete
  8. ارجو رفع مثال عملى على كيفية تطبيق ال architecture

    ReplyDelete
    Replies
    1. لو قدرت عمل كده بإذن الله

      Delete
  9. رائعع ، شكرا

    ReplyDelete
  10. Why not to use redis https://redis.io/ ? as in memory store;

    ReplyDelete
  11. Great article and great effort Mozaly

    ReplyDelete
  12. الله عليك يا مظلى على المقال الرائع
    جزاك الله كل خير فعلا تحسين الكود عليه عامل قوى جدا فى زيادة قرائة البيانات والتعامل معاها

    ReplyDelete
  13. جزاكم الله خيرا مقال رائع

    ReplyDelete