مدونة الهندسة - Prexy: قواعد بحث واستبدال فائقة السرعة

مرحبًا بكم في أول مدونة هندسية لنا. قد يكون المحتوى أكثر تقنية مما اعتدتم عليه في المدونة الأخرى، لكننا بذلنا قصارى جهدنا لجعلها مفهومة للجميع. في هذه المقالة، سنتحدث عن Prexy، وهو برنامج جديد ضمن Clonable يُستخدم لتطبيق قواعد الاستبدال.

الخلفية

إذا كنت تستخدم Clonable ، فربما تكون على دراية بوظيفة «قواعد الاستبدال». يمكنك استخدام هذه القواعد لاستبدال أجزاء من النص أو الكود ببدائل تختارها. على سبيل المثال، يمكنك استبدال مفتاح واجهة برمجة التطبيقات (API) أو معرّف التحليلات (Analytics ID) لتتمكن من إجراء تحليلات منفصلة لموقعك الأصلي ونسخك اللغوية. داخليًا، تُستخدم هذه القواعد نفسها لاستبدال اسم النطاق الأصلي باسم نطاق النسخة، مع تنفيذ عدد من التعديلات الأخرى.

منذ الإصدار الأول لـ « Clonable »، لم تُعدّل هذه الوظيفة، إذ كانت تؤدي مهمتها بكفاءة. ومع ذلك، كانت هناك فرص لتحسين الأداء وسهولة الاستخدام. بين الحين والآخر، نختار جزءًا من منتجنا « Clonable » لم نكرّس له وقتًا منذ فترة طويلة، ونعمل على تحسينه. فعلى سبيل المثال، قمنا العام الماضي بمراجعة البنية التحتية للبيانات بالكامل لجعلها أسرع وأكثر قابلية للتوسع وأكثر تحملاً للأخطاء، وقمنا في هذا الإطار بإعادة بناء وظيفة ترجمة عناوين URL من الصفر. وفي هذا الربع، جاء دور قواعد الاستبدال.

الوضع السابق والقيود

لا تُطبق قواعد الاستبدال في Clonable إلا في المرحلة الأخيرة تمامًا، قبل إرسال الاستجابة إلى العميل مباشرةً. ولذلك، في الإصدار الأول من Clonable ، تم اختيار تنفيذ ذلك داخل خادم الويب NGINX باستخدام replace-filter-nginx-module، وهي وحدة نمطية أنشأتها OpenResty. تستخدم الوحدة محرك تعبيرات منتظمة للتدفق (sregex)، وهو أيضًا من تطوير OpenResty. ويُعد جانب التدفق مهمًا، لأننا لا نرغب في تحميل كل استجابة بالكامل في الذاكرة. فإذا قمنا بذلك، فقد تتسبب الاستجابات الكبيرة في تعطل NGINX أو توقفه عن العمل بسبب عدم توفر ذاكرة كافية. ومن السمات المهمة الثانية لـ sregex قدرته على معالجة عدة قواعد بشكل متوازٍ. فكل نسخة لغوية من الموقع تحتوي بالفعل على عدد من القواعد القياسية، بالإضافة إلى عدد من القواعد التي تمت إضافتها خصيصًا لتلك النسخة. وإذا تعذر معالجتها في وقت واحد، فسيتعين علينا تخزين الاستجابة بالكامل مؤقتًا، لأننا سنحتاج بعد معالجة القاعدة الأولى إلى العودة مرة أخرى لمعالجة القاعدة الثانية.

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

نموذج التزامن في NGINX

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

يتميز NGINX بهيكلية «العامل» (worker) التي تتألف من عملية رئيسية واحدة (master process) والعديد من العمليات العاملة (worker processes). يتم توزيع الاتصالات على العمال (workers) حتى يتسنى معالجة عدة طلبات في وقت واحد. لكن هذا الإعداد له عيب أيضًا: عندما يكون أحد العمال مشغولًا بمعالجة طلب ما، فإن الطلبات الأخرى المخصصة لنفس العامل تضطر إلى الانتظار. وقد يؤدي ذلك إلى أن يتسبب طلب واحد ثقيل في تأخير عدة طلبات أخرى، حتى لو لم يكن لدى العمال الآخرين أي عمل. يظهر هذا التأثير بوضوح في الصورة أدناه. أثناء التقاط الصورة، كان جارٍ اختبار تحميل على 10 اتصالات مختلفة. تُظهر الصورة أن 3 عمليات عاملة مشغولة، بينما لا تقوم العملية الرابعة بأي عمل تقريبًا.

حان وقت Prexy

بعد تحديد نقطة الاختناق، وضعنا خطة تحسين. أطلقنا على المشروع اسم Prexy، وهو مزيج من RegEx وProxy. تضمنت النتيجة النهائية 3 متطلبات رئيسية:

  1. يجب أن يكون بديلاً مباشراً للإعداد الحالي. يجب تطبيق قواعد الاستبدال بنفس الطريقة لتجنب حدوث أعطال في الإعدادات الحالية.

  2. يجب تطبيق قواعد الاستبدال بشكل متواصل لتجنب استهلاك الحل لكمية كبيرة من الذاكرة.

  3. يجب أن يكون الحل الجديد أسرع من الحل الحالي.

أولًا، بحثنا عن بيئة تشغيل متعددة الخيوط قادرة على معالجة الطلبات بكفاءة. بعد دراسة عدة خيارات، وقع الاختيار على بيئة تشغيل Tokio للغة البرمجة Rust. تشتهر Rust بكتابة برامج سريعة دون المخاطر الأمنية التي تنطوي عليها لغات البرمجة منخفضة المستوى الأخرى مثل C++. بيئة التشغيل «طوكيو» هي بيئة تشغيل مرنة وغير متزامنة مصممة لتطبيقات الشبكات. ومن أهم ميزاتها بالنسبة لنا أنها تدعم ميزة «سرقة العمل» (work-stealing). وهذا يعني أنه، على عكس NGINX، لا يرتبط الطلب بعامل واحد فقط، بل يمكن للعامل الذي ليس لديه ما يفعله أن «يسرق» العمل من عامل آخر. وبهذه الطريقة، يمكن الاستفادة من موارد خوادمنا بشكل أفضل.

النموذج الأولي الأول

بعد اختيار التقنيات الأساسية، قررنا إنشاء نموذج أولي لتقدير الزيادة المتوقعة في الأداء وما إذا كان المشروع يستحق العناء. في التطبيق الأول لمحرك التعبيرات النمطية (الجزء الذي يعالج القواعد ويطبقها)، استخدمنا المكتبة القياسية للتعبيرات النمطية (أو crate كما تُسمى في لغة Rust). يعمل هذا المحرك، تمامًا مثل sregex، وفق آلية non-backtracking، مما يقلل احتمال التعرض لمشكلة ReDoS. في حالة ReDoS، يتعامل التعبير النمطي مع مدخلات من النوع الذي يؤدي إلى زيادة الوقت المستغرق لتقييم المدخلات بشكل أسي. وقد يكون هذا كارثياً على الأداء، وقد تسبب في حدوث عطل كبير في Cloudflare. لذا، من المهم بالنسبة لنا أن يكون المحرك الذي نستخدمه غير رجعي.

باستخدام محرك التعبيرات النمطية هذا، قمنا ببناء نموذج أولي. استخدم هذا النموذج قواعد مبرمجة بشكل ثابت، وكان محرك التعبيرات النمطية يقوم بتخزين الاستجابة بالكامل مؤقتًا بدلاً من بثها، لكن هذا كان كافيًا لإجراء اختبار الأداء الأولي. 

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

أظهرت النتائج الأولية أن Prexy أسرع بحوالي 22 ضعفًا في معالجة طلب واحد (انظر الصورة أدناه). منح هذا المشروع الضوء الأخضر النهائي، إذ يوفر هامش أداء كافٍ لتغطية أي تراجع قد يحدث أثناء إضافة الميزات.

بعد الاختبار الأول، نفّذنا تحسينات بديهية مثل تخزين التعبيرات النمطية المُجمَّعة في ذاكرة التخزين المؤقت. كما طبّقنا طريقة أكثر كفاءة لإعادة استخدام الاتصالات مع الخادم الرئيسي. وبذلك حققنا زيادة في السرعة بنسبة 20٪ تقريبًا. ثم قمنا باختبار الحلين تحت أحمال الذروة، حيث أرسلنا أكبر عدد ممكن من الطلبات إلى الخادم. وهنا أيضًا كان الفرق واضحًا للغاية، حيث حقق Prexy معدل نقل بيانات أعلى بحوالي 25 ضعفًا مقارنةً بالحل القديم.

محرك تعبيرات عادية (regex) متدفق

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

تحسينات إضافية

كما كان متوقعًا، أثر تعديل محرك التعبيرات النمطية (regex) سلبًا على أداء Prexy. ومع ذلك، كان هامش الأداء لا يزال كافيًا مقارنةً بالحل القديم، وبفضل إضافة المزيد من التحسينات، تمكنا في النهاية من تحقيق أداء أعلى مما كان عليه قبل تعديل محرك التعبيرات النمطية. كان أحد التحسينات التي قمنا بتطبيقها هو تذكر ما إذا كان يجب تطبيق قاعدة معينة على ملف معين أم لا. الأصول الثابتة مثل ملفات CSS وJS لا تتغير تقريبًا، لذلك لا فائدة من تطبيق القاعدة في كل مرة على هذه الملفات. كان البديل هو تخزين نتائج الاستبدال مؤقتاً، إلا أن هذه الاستراتيجية تستهلك مساحة كبيرة من الذاكرة. باستخدام هذه الطريقة، احتجنا فقط إلى بضعة بايتات لكل ملف عن طريق تخزين المعلومات كصورة نقطية.

بالإضافة إلى ذلك، نستفيد بشكل أمثل من عمليات التحسين داخل محرك التعبيرات النمطية (regex)؛ على سبيل المثال، من خلال عدم الاحتفاظ بمجموعات التقاط (capturing groups) عندما لا يتم استخدامها في عملية الاستبدال. وبذلك، لا يحتاج محرك التعبيرات النمطية سوى إلى تذكر بداية ونهاية المطابقة بأكملها، مما يقلل من عبء العمل. كما قمنا بتعطيل مجموعات التقاط المسماة (named capturing groups)، لأن ذلك كان معقدًا إلى حد ما في النسخة المتدفقة (streaming)، ولأن الحل القديم لم يكن يدعم ذلك أيضًا، فلم يكن ذلك ضروريًا بالضرورة. وقد أسفرت جميع هذه التحسينات في النهاية عن الأداء التالي:

النتيجة النهائية

بعد اختبارات مكثفة للتأكد من مطابقة Prexy للأداء القديم، بدأنا النشر التدريجي. وعلى مدار أسبوع تقريبًا، قمنا بتفعيل Prexy لكل نسخة لغوية. كان من السهل ملاحظة لحظة التنشيط في نظام المراقبة لدينا. ولم يكن من النادر ملاحظة انخفاض بنسبة 50٪ تقريبًا في أوقات الاستجابة. كما لوحظت فروق أقل حجمًا بنسبة 10٪ تقريبًا لدى عملاء آخرين لديهم عدد قليل من قواعد الاستبدال. وأصبح موقعنا الإلكتروني أسرع بنسبة 30٪ تقريبًا (~50 مللي ثانية).

بالنسبة لبنيتنا التحتية العامة، نلاحظ الاختلافات التالية بعد طرح Prexy.

  • انخفاض استهلاك ذاكرة الوصول العشوائي (RAM) بنسبة 33%

  • انخفاض استهلاك وحدة المعالجة المركزية (CPU) بنسبة 20%

  • استجابات أسرع بنسبة 10-50٪

بشكل عام، يمكننا القول إن Prexy كان مشروعًا ناجحًا. من خلال استبدال وحدة NGINX قديمة، يمكننا الآن استخدام تقنيات أحدث وأكثر كفاءة تُحدث فرقًا واضحًا وقابلًا للقياس لجميع عملائنا. وسنواصل في المستقبل العمل على تحسين Clonable، سواء من حيث إضافة ميزات جديدة أو تحسين الوظائف الحالية.


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

ابدأ اليوم مجانًا مع Clonable

اكتشف كيفية إدارة المواقع الإلكترونية الدولية مركزياً عبر منصة واحدة.

نستخدم ملفات تعريف الارتباط لتحسين تجربتك، وتحليل حركة المرور على الموقع الإلكتروني، وتعزيز خدماتنا.

اقرأ سياسة ملفات تعريف الارتباط الخاصة بنا