المادة ، التي ننشر ترجمتها اليوم ، مكرسة لقصة كيفية تحسين Airbnb لأجزاء الخادم من تطبيقات الويب مع التركيز على الاستخدام المتزايد باستمرار لتقنيات عرض الخادم. على مدار عدة سنوات ، حولت الشركة تدريجياً الواجهة الأمامية بالكامل إلى بنية
موحدة ، وفقًا لصفحات الويب عبارة عن هياكل هرمية لمكونات React مليئة بالبيانات من API الخاصة بهم. على وجه الخصوص ، خلال هذه العملية كان هناك هجر منهجي لروبي على القضبان. في الواقع ، تخطط Airbnb للتبديل إلى خدمة جديدة تعتمد فقط على Node.js ، وبفضلها سيتم تسليم الصفحات المعدة بالكامل على الخادم إلى متصفحات المستخدمين. ستنشئ هذه الخدمة معظم كود HTML لجميع منتجات Airbnb. يختلف محرك العرض المعني عن معظم خدمات الواجهة الخلفية التي تستخدمها الشركة نظرًا لعدم كتابتها في Ruby أو Java. ومع ذلك ، فإنه يختلف عن خدمات Node.js التقليدية التي يتم تحميلها بشكل كبير ، والتي يتم من خلالها بناء النماذج العقلية والأدوات المساعدة المستخدمة في Airbnb.

منصة Node.js
بالتفكير في منصة Node.js ، يمكنك أن تتخيل كيف أن تطبيقًا معينًا ، تم إنشاؤه مع مراعاة إمكانات هذا النظام الأساسي لمعالجة البيانات غير المتزامنة ، يخدم بسرعة مئات الآلاف من الاتصالات المتوازية بسرعة وكفاءة. تقوم الخدمة بسحب البيانات التي تحتاجها من كل مكان وتعالجها قليلاً حتى تلبي احتياجات عدد كبير من العملاء. ليس لدى صاحب مثل هذا التطبيق سبب للشكوى ، فهو واثق من النموذج الخفيف لمعالجة البيانات المتزامنة التي يستخدمها (في هذه المادة نستخدم كلمة "متزامن" لنقل مصطلح "متزامن" ، لمصطلح "موازٍ" - "موازٍ"). إنها تحل تمامًا المهمة المحددة لها.
التقديم من جانب الخادم (SSR) يغير الأفكار الأساسية التي تؤدي إلى رؤية مماثلة للمشكلة. لذا ، يتطلب تقديم الخادم الكثير من موارد الحوسبة. يتم تنفيذ التعليمات البرمجية في بيئة Node.js في مؤشر ترابط واحد ، ونتيجة لذلك ، لحل المشاكل الحسابية (على عكس مهام الإدخال / الإخراج) ، يمكن تنفيذ التعليمات البرمجية في وقت واحد ، ولكن ليس بالتوازي. منصة Node.js قادرة على معالجة عدد كبير من عمليات الإدخال / الإخراج المتوازية ، ومع ذلك ، عندما يتعلق الأمر بالحوسبة ، يتغير الوضع.
منذ تطبيق العرض من جانب الخادم ، يزداد الجزء الحسابي لمهمة معالجة الطلب مقارنةً بالجزء المتعلق بالإدخال / الإخراج ، ستؤثر الطلبات الواردة في الوقت نفسه على سرعة استجابة الخادم نظرًا لأنها تتنافس على موارد المعالج. وتجدر الإشارة إلى أنه عند استخدام العرض غير المتزامن ، لا يزال التنافس على الموارد موجودًا. يعمل العرض غير المتزامن على حل استجابة العملية أو المتصفح ، ولكنه لا يحسن الموقف مع التأخير أو التزامن. في هذه المقالة ، سنركز على نموذج بسيط يتضمن أحمال حسابية حصرية. إذا تحدثنا عن حمل مختلط ، والذي يتضمن كلاً من عمليات الإدخال / الإخراج والحساب ، فإن الطلبات الواردة في الوقت نفسه ستزيد من التأخير ، ولكن مع الأخذ في الاعتبار ميزة إنتاجية أعلى للنظام.
خذ بعين الاعتبار أمر النموذج
Promise.all([fn1, fn2])
. إذا كان
fn1
أو
fn2
عبارة عن وعود تم حلها بواسطة النظام الفرعي I / O ، فمن الممكن أثناء تنفيذ هذا الأمر تحقيق تنفيذ متوازي للعمليات. يبدو هذا:
التنفيذ المتوازي للعمليات عن طريق النظام الفرعي للمدخلات / المخرجاتإذا كانت
fn1
و
fn2
مهام حسابية ، فسيتم تنفيذها على النحو التالي:
مهام الحوسبةيجب أن تنتظر إحدى العمليات اكتمال العملية الثانية ، نظرًا لوجود مؤشر ترابط واحد فقط في Node.js.
في حالة عرض الخادم ، تحدث هذه المشكلة عندما يتعين على عملية الخادم معالجة العديد من الطلبات المتزامنة. سيتم تأجيل معالجة هذه الطلبات حتى تتم معالجة الطلبات التي تم تلقيها سابقًا. إليك كيف تبدو.
معالجة الطلبات المتزامنةمن الناحية العملية ، غالبًا ما تتكون معالجة الطلب من العديد من المراحل غير المتزامنة ، حتى إذا كانت تنطوي على حمل حسابي خطير على النظام. هذا يمكن أن يؤدي إلى موقف أكثر صعوبة مع تبديل المهام لمعالجة مثل هذه الطلبات.
لنفترض أن استعلاماتنا تتكون من سلسلة
renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body))
تشبه هذه:
renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body))
. عندما يصل زوج من هذه الطلبات إلى النظام ، مع فاصل زمني صغير بينهما ، يمكننا ملاحظة الصورة التالية.
معالجة الطلبات التي تصل إلى فاصل زمني صغير ، مشكلة النضال من أجل موارد المعالجفي هذه الحالة ، تستغرق معالجة كل طلب ضعف الوقت الذي تستغرقه معالجة طلب فردي. مع زيادة عدد الطلبات التي تمت معالجتها في وقت واحد ، يصبح الوضع أسوأ.
بالإضافة إلى ذلك ، أحد الأهداف النموذجية لتطبيق SSR هو القدرة على استخدام نفس الرمز أو رمز مشابه جدًا على كل من العميل والخادم. الاختلاف الخطير بين هذه البيئات هو أن بيئة العميل هي في الأساس بيئة يعمل فيها عميل واحد ، وبيئات الخادم ، بحكم طبيعتها ، هي بيئات متعددة العملاء. ما يعمل بشكل جيد على العميل ، مثل الأحجار الفردية أو طرق أخرى لتخزين الحالة العالمية للتطبيق ، يؤدي إلى أخطاء وتسريبات في البيانات ، وبشكل عام ، إلى الارتباك ، أثناء معالجة العديد من الطلبات التي تصل إلى الخادم.
تصبح هذه الميزات مشاكل في موقف تحتاج فيه إلى معالجة طلبات متعددة في نفس الوقت. عادة ما يعمل كل شيء بشكل طبيعي في ظل أحمال منخفضة في بيئة مريحة لبيئة التطوير ، والتي يستخدمها عميل واحد في شخص مبرمج.
هذا يؤدي إلى موقف مختلف تمامًا عن أمثلة تطبيق Node.js الكلاسيكية. وتجدر الإشارة إلى أننا نستخدم وقت تشغيل JavaScript لمجموعة غنية من المكتبات المتاحة فيه ، ولأنه مدعوم من المتصفحات ، وليس من أجل نموذجه لمعالجة البيانات المتزامنة. في هذا التطبيق ، يوضح النموذج غير المتزامن للمعالجة المتزامنة للبيانات جميع عيوبه ، والتي لا يتم تعويضها بالمزايا ، والتي تكون إما قليلة جدًا أو لا تعمل على الإطلاق.
دروس مشروع Hypernova
ستكون خدمة العرض الجديدة ، Hyperloop ، هي الخدمة الأساسية التي سيتفاعل معها مستخدمو Airbnb. ونتيجة لذلك ، تلعب موثوقيتها وأدائها دورًا حاسمًا في ضمان الراحة للعمل مع مورد. عند إدخال Hyperloop في الإنتاج ، نأخذ في الاعتبار التجربة التي اكتسبناها أثناء العمل مع نظام عرض الخادم السابق -
Hypernova .
لا تعمل Hypernova مثل خدمتنا الجديدة. هذا نظام عرض خالص. يتم استدعاؤها من خدمة السكك الحديدية المتجانسة لدينا ، والتي تسمى Monorail ، وتعيد مقتطفات HTML فقط لمكونات محددة معروضة. في كثير من الحالات ، يمثل هذا "المقتطف" حصة الأسد من الصفحة ، ولا يوفر Rails تخطيط الصفحة إلا. باستخدام التكنولوجيا القديمة ، يمكن ربط أجزاء من الصفحة معًا باستخدام ERB. على أي حال ، لا تقوم Hypernova بتحميل أي بيانات ضرورية لتكوين الصفحة. هذه هي مهمة ريلز.
وبالتالي ، تتمتع Hyperloop و Hypernova بأداء حاسوبي مشابه. في الوقت نفسه ، توفر Hypernova ، كخدمة إنتاج ومعالجة كميات كبيرة من حركة المرور ، مجالًا جيدًا للاختبار ، مما يؤدي إلى فهم كيفية تصرف بديل Hypernova في ظروف القتال.
سير عمل Hypernovaإليك كيفية عمل Hypernova. تأتي طلبات المستخدم إلى تطبيق Rails الرئيسي ، Monorail ، الذي يجمع خصائص مكونات React التي يجب عرضها على الصفحة ويقدم طلبًا إلى Hypernova ، ويمرر هذه الخصائص وأسماء المكونات. تعرض Hypernova مكونات بخصائص من أجل إنشاء كود HTML الذي يجب إعادته إلى تطبيق Monorail ، والذي يقوم بعد ذلك بتضمين هذا الرمز في قالب الصفحة وإرساله مرة أخرى إلى العميل.
إرسال صفحة الانتهاء إلى العميلفي حالة الطوارئ (قد يكون هذا خطأ أو مهلة الاستجابة) في Hypernova ، هناك خيار احتياطي ، عند الاستخدام يتم تضمين المكونات وخصائصها في الصفحة دون إنشاء HTML على الخادم ، وبعد ذلك يتم إرسال كل هذا إلى العميل ويتم تقديمه هناك نأمل أن تكون ناجحة. هذا قادنا إلى حقيقة أننا لم نعتبر خدمة Hypernova جزءًا مهمًا من النظام. نتيجة لذلك ، يمكننا السماح بحدوث عدد معين من حالات الفشل والمواقف التي يتم فيها تشغيل المهلة. من خلال ضبط مهلات الطلب ، نقوم ، بناءً على الملاحظات ، بضبطها على المستوى P95 تقريبًا. ونتيجة لذلك ، ليس من المستغرب أن يعمل النظام بمعدل استجابة مهلة أقل من 5٪.
في المواقف التي وصلت فيها حركة المرور إلى قيم الذروة ، يمكننا أن نرى أن ما يصل إلى 40٪ من الطلبات إلى Hypernova تم إغلاقها بسبب انتهاء المهلة في خط واحد. على جانب Hypernova ، رأينا قمم
BadRequestError: Request aborted
ذات ارتفاع أقل. بالإضافة إلى ذلك ، كانت هذه الأخطاء موجودة في الظروف العادية ، بينما في التشغيل العادي ، بسبب بنية الحل ، لم تكن الأخطاء المتبقية ملحوظة بشكل خاص.
قيم مهلة الذروة (خطوط حمراء)نظرًا لأن نظامنا يمكن أن يعمل بدون Hypernova ، فإننا لم ننتبه كثيرًا لهذه الميزات ، فقد كان يُنظر إليها على أنها تفاهات مزعجة ، وليس مشاكل خطيرة. أوضحنا هذه المشاكل من خلال ميزات النظام الأساسي ، لأن إطلاق التطبيق بطيء بسبب صعوبة عملية جمع البيانات المهملة الأولية ، نظرًا لخصائص تجميع التعليمات البرمجية وتخزين البيانات مؤقتًا ، ولأسباب أخرى. كنا نأمل أن تتضمن إصدارات React أو Node الجديدة تحسينات في الأداء من شأنها أن تخفف من عيوب الإطلاق البطيء للخدمة.
كنت أظن أن ما يحدث كان على الأرجح نتيجة لضعف موازنة الحمل أو نتيجة لمشاكل في نشر الحل ، عندما ظهرت زيادة التأخير بسبب الحمل الحسابي المفرط على العمليات. أضفت طبقة مساعدة إلى النظام لتسجيل معلومات حول عدد الطلبات التي تتم معالجتها في وقت واحد بواسطة عمليات فردية ، وكذلك لتسجيل الحالات التي تلقت فيها العملية أكثر من طلب للمعالجة.
نتائج البحثلقد اعتبرنا أن البداية البطيئة للخدمة هي السبب في التأخير ، ولكن في الواقع كانت المشكلة ناتجة عن الطلبات المتوازية التي تقاتل من أجل وقت وحدة المعالجة المركزية. ووفقًا لنتائج القياس ، اتضح أن الوقت الذي يقضيه الطلب تحسبًا لاستكمال معالجة الطلبات الأخرى يتوافق مع الوقت المستغرق في معالجة الطلب. بالإضافة إلى ذلك ، هذا يعني أن الزيادة في التأخير بسبب المعالجة المتزامنة للطلبات تبدو هي نفسها الزيادة في التأخير بسبب زيادة التعقيد الحسابي للشفرة ، مما يؤدي إلى زيادة الحمل على النظام عند معالجة كل طلب.
هذا ، بالإضافة إلى ذلك ، جعل من الواضح أن
BadRequestError: Request aborted
لا يمكن تفسيره بثقة من خلال بدء تشغيل بطيء للنظام. حدث الخطأ من رمز التحليل الخاص بنص الطلب ، وحدث عندما قام العميل بإلغاء الطلب قبل أن يتمكن الخادم من قراءة نص الطلب بالكامل. توقف العميل عن العمل وأغلق الاتصال وحرمنا من البيانات المطلوبة لمتابعة معالجة الطلب. من المرجح أن هذا قد حدث لأننا بدأنا في معالجة الطلب ، وبعد ذلك تحولت حلقة الحدث إلى عرض محظور لطلب آخر ، ثم عدنا إلى المهمة التي تمت مقاطعتها لإكمالها ، ولكن نتيجة لذلك اتضح أن العميل الذي أرسل إلينا هذا الطلب ، تم قطع اتصاله بالفعل ، مما أدى إلى إحباط الطلب. بالإضافة إلى ذلك ، كانت البيانات المرسلة في الطلبات إلى Hypernova ضخمة جدًا ، في المتوسط ، في منطقة عدة مئات من الكيلوبايت ، وهذا بالطبع لم يساهم في تحسين الوضع.
خطأ ناتج عن قطع اتصال عميل لم ينتظر استجابةقررنا التعامل مع هذه المشكلة باستخدام بعض الأدوات القياسية التي كانت لدينا خبرة كبيرة معها. نحن نتحدث عن خادم وكيل عكسي (
nginx )
وموازن تحميل (
HAProxy ).
التوكيل العكسي وموازنة الحمل
من أجل الاستفادة من بنية المعالج متعدد النواة ، نقوم بتشغيل العديد من عمليات Hypernova باستخدام وحدة Node.js المدمجة. نظرًا لأن هذه العمليات مستقلة ، يمكننا معالجة الطلبات الواردة في نفس الوقت.
معالجة موازية للطلبات التي تصل في نفس الوقتتكمن المشكلة هنا في أن كل عملية عقدة مشغولة تمامًا طوال الوقت الذي تستغرقه معالجة طلب واحد ، بما في ذلك قراءة نص الطلب المرسل من العميل (يلعب Monorail دوره في هذه الحالة). على الرغم من أنه يمكننا قراءة العديد من الاستعلامات في عملية واحدة في نفس الوقت ، إلا أنه عندما يتعلق الأمر بالتقديم ، فإنه يؤدي إلى تبديل العمليات الحسابية.
يرتبط استخدام موارد عملية العقدة بسرعة العميل والشبكة.
كحل لهذه المشكلة ، يمكننا التفكير في خادم وكيل عكسي مؤقت ، والذي سيسمح لنا بالحفاظ على جلسات التواصل مع العملاء. كان مصدر إلهام هذه الفكرة هو خادم الويب يونيكورن ، والذي نستخدمه لتطبيقات ريلز لدينا.
توضح المبادئ التي أعلنها يونيكورن تمامًا سبب ذلك. لهذا الغرض استخدمنا nginx. يقرأ Nginx الطلب من العميل إلى المخزن المؤقت ، ويمرر الطلب إلى خادم Node فقط بعد قراءته بالكامل. يتم تنفيذ جلسة نقل البيانات هذه على الجهاز المحلي ، من خلال واجهة الاسترجاع أو باستخدام مآخذ نطاق يونيكس ، وهذا أسرع بكثير وأكثر موثوقية من نقل البيانات بين أجهزة كمبيوتر منفصلة.
يقوم Nginx بتخزين الطلبات مؤقتًا ثم يرسلها إلى خادم العقدةنظرًا لحقيقة أن nginx تعمل الآن في طلبات القراءة ، فقد تمكنا من تحقيق تحميل أكثر اتساقًا لعمليات العقدة.
تحميل عملية موحدة باستخدام nginxبالإضافة إلى ذلك ، استخدمنا nginx لمعالجة بعض الطلبات التي لا تتطلب الوصول إلى عمليات العقدة. تستخدم طبقة الكشف والتوجيه في خدمتنا
/ping
طلبات
/ping
التي لا تنشئ حمولة كبيرة على النظام للتحقق من الاتصال بين المضيفين. معالجة كل هذا في nginx يلغي مصدرًا مهمًا لعبء العمل الإضافي (وإن كان صغيرًا) لـ Node.js.
يتعلق التحسين التالي بموازنة الحمل. نحن بحاجة إلى اتخاذ قرارات مستنيرة بشأن توزيع الطلبات بين عمليات العقدة. توزع وحدة
cluster
الطلبات وفقًا لخوارزمية Round-robin ، في معظم الحالات مع محاولات لتجاوز العمليات التي لا تستجيب للطلبات. مع هذا النهج ، تتلقى كل عملية طلبًا حسب الأولوية.
تقوم وحدة
cluster
بتوزيع الاتصالات ، وليس الطلبات ، لذلك لا يعمل كل هذا كما نحتاج. يزداد الوضع سوءًا عند استخدام الاتصالات المستمرة. يرتبط أي اتصال دائم من العميل بسير عمل واحد محدد ، مما يعقد التوزيع الفعال للمهام.
تعتبر خوارزمية Round-robin جيدة عندما يكون هناك تغير منخفض في تأخيرات الطلب. على سبيل المثال ، في الحالة الموضحة أدناه.
خوارزمية مستديرة وروابط يتم من خلالها استقبال الطلبات بثباتهذه الخوارزمية ليست جيدة بالفعل عندما يتعين عليك معالجة الطلبات من أنواع مختلفة ، والتي قد تتطلب معالجة تكاليف زمنية مختلفة تمامًا. يضطر آخر طلب تم إرساله إلى عملية معينة إلى الانتظار حتى اكتمال معالجة جميع الطلبات التي تم إرسالها سابقًا ، حتى إذا كانت هناك عملية أخرى لديها القدرة على معالجة مثل هذا الطلب.
تحميل عملية غير متساويةإذا قمت بتوزيع الاستعلامات الموضحة أعلاه بشكل أكثر عقلانية ، فستحصل على شيء مثل ذلك الموضح في الشكل أدناه.
التوزيع العقلاني للطلبات حسب المواضيعمع هذا النهج ، يتم تقليل الانتظار ويصبح من الممكن إرسال الردود على الطلبات بشكل أسرع.
يمكن تحقيق ذلك عن طريق وضع الطلبات في قائمة الانتظار ، وتعيينها لعملية فقط عندما لا تكون مشغولة في معالجة طلب آخر. لهذا الغرض نستخدم HAProxy.
HAProxy وموازنة تحميل العمليةعندما استخدمنا HAProxy لموازنة الحمل على Hypernova ، أزلنا تمامًا ذروة المهلة ، بالإضافة إلى أخطاء
BadRequestErrors
.
كانت الطلبات المتزامنة أيضًا السبب الرئيسي للتأخير أثناء التشغيل العادي ؛ قلل هذا النهج من مثل هذه التأخيرات. كانت إحدى نتائج ذلك أنه تم إغلاق 2٪ فقط من الطلبات في الوقت المحدد ، وليس 5٪ ، مع نفس إعدادات المهلة. أظهرت حقيقة أننا تمكنا من الانتقال من موقف يحتوي على أخطاء بنسبة 40٪ إلى موقف مع انتهاء مهلة في 2٪ من الحالات أننا نتحرك في الاتجاه الصحيح. ونتيجة لذلك ، يرى مستخدمونا اليوم شاشة تحميل موقع الويب بشكل أقل تكرارًا. وتجدر الإشارة إلى أن استقرار النظام سيكون ذا أهمية خاصة بالنسبة لنا مع الانتقال المتوقع إلى نظام جديد ليس لديه نفس آلية النسخ الاحتياطي التي تمتلكها Hypernova.
تفاصيل حول النظام وإعداداته
لكي يعمل كل هذا ، تحتاج إلى تكوين تطبيق nginx و HAProxy و Node. في ما يلي
مثال لتطبيق مشابه يستخدم nginx و HAProxy ، حيث يمكنك تحليل الجهاز الذي يمكنك فهمه. يعتمد هذا المثال على النظام الذي نستخدمه في الإنتاج ، ولكن يتم تبسيطه وتعديله بحيث يمكن تنفيذه في المقدمة نيابة عن مستخدم غير مميز. في الإنتاج ، يجب تكوين كل شيء باستخدام نوع من المشرف (نستخدم runit ، أو في كثير من الأحيان ، kubernetes).
تكوين nginx قياسي جدًا ، فهو يستخدم خادمًا يستمع على المنفذ 9000 ، تم تكوينه لطلبات الوكيل إلى خادم HAProxy ، الذي يستمع على المنفذ 9001 (في تكويننا ، نستخدم مآخذ نطاق Unix).
بالإضافة إلى ذلك ، يعترض هذا الخادم الطلبات على نقطة نهاية
/ping
لطلبات الخدمة المباشرة التي تهدف إلى التحقق من اتصال الشبكة. nginx ,
worker_processes
1, nginx — HAProxy Node-. , , , Hypernova, ( ). .
Node.js
cluster
. HAProxy,
cluster
, .
pool-hall . — , , ,
cluster
, .
pool-hall
, .
HAProxy , 9001 , 9002 9005. —
maxconn 1
, . . HAProxy ( 8999).
HAProxyHAProxy . ,
maxconn
.
static-rr
(static round-robin), , , . , round-robin, , , , , . , , . .
, , . ( ). , , , , . , , .
HAProxy
HAProxy. , , , . , , ( ) . , ,
cluster
. , .
ab
(Apache Benchmark) 10000 . - . :
ab -l -c <CONCURRENCY> -n 10000 http://<HOSTNAME>:9000/render
15 4- -,
ab
, . (
concurrency=5
), (
concurrency=13
), , (
concurrency=20
). , .
, -, . , . , , , , . , , , .
, — .
maxconn 1
, , .
HTTP TCP , , , . ,
maxconn
, . , , (, , ).
, , , , , , .
— , .
option redispatch
retries 3
, , , , , , . .
, - , . , . , , . 100 , 10 , , . , . ,
accept
.
, (
backlog ) , . SYN-ACK (
, , , ACK ). , , , , .
, , , , . , , 1.
maxconn
. 0 , , , , , . , . - , , .
abortonclose
, . ,
abortonclose
. nginx.
, , . ( ) , , , , , . HAProxy , , ( ). , , , HTML. , , . , , ( , , ). , , . , , , . HAProxy, MAINT HAProxy.
, , ,
server.close
Node.js , HAProxy , , , . , , , , , .
, ,
balance first
, (
worker1
) 15% , , ,
balance static-rr
. , «» . . (12 ), , , - . , , , «» «». .
, , Node
server.maxconnections
, ( , ), , , , . ,
maxconnection
, , , . JavaScript, ( ). , , , . , , , HAProxy Node , . , , .
, , , ,
.
Node.js . , , , -. Node.js . , , , , , , , nginx HAProxy.
, Airbnb , Node.js .
! ?