حول 30x التزامن دفعة في Node.js

ما هي أفضل طريقة لزيادة التزامن بسلاسة في خدمة Node.js المستخدمة في الإنتاج؟ هذا سؤال يحتاجه فريقي للإجابة قبل شهرين.

لقد أطلقنا 4000 حاوية Node (أو "عمال") ، والتي تضمن تشغيل خدمة التكامل الخاصة بنا مع البنوك. تم تصميم الخدمة في الأصل بحيث تم تصميم كل عامل لمعالجة طلب واحد فقط في كل مرة. قلل هذا من التأثير على نظام تلك العمليات التي يمكن أن تمنع دورة الأحداث بشكل غير متوقع وتسمح لنا بتجاهل الاختلافات في استخدام الموارد من خلال عمليات مماثلة مختلفة. ولكن نظرًا لأن قدراتنا كانت مقصورة على التنفيذ المتزامن لـ 4000 طلب فقط ، فلا يمكن تحجيم النظام بشكل مناسب. لم تعتمد سرعة الاستجابة لمعظم الطلبات على سعة الجهاز ، ولكن على قدرات الشبكة. لذلك ، يمكننا تحسين النظام وتقليل تكلفة دعمه إذا تمكنا من إيجاد طريقة لمعالجة الطلبات بشكل موثوق في نفس الوقت.



بعد دراسة هذه المشكلة ، لم نتمكن من العثور على دليل جيد يناقش الانتقال من "عدم وجود تواز" في Node.js إلى "مستوى عالٍ من التوازي". نتيجة لذلك ، قمنا بتطوير إستراتيجية الترحيل الخاصة بنا ، والتي كانت تستند إلى التخطيط الدقيق والأدوات الجيدة وأدوات المراقبة والجرعة الصحية من تصحيح الأخطاء. نتيجة لذلك ، تمكنا من زيادة مستوى التوازي في نظامنا بنسبة 30 مرة. وهذا يعادل خفض تكلفة صيانة النظام بنحو 300 ألف دولار في السنة.

هذه المادة مخصصة لقصة كيف قمنا بزيادة إنتاجية وفعالية عمال Node.js ، وما تعلمناه من خلال المضي بهذه الطريقة.

لماذا قررنا الاستثمار في التوازي؟


قد يبدو من المفاجئ أننا نمت إلى هذه الأبعاد دون استخدام التوازي. كيف حدث ذلك؟ يبدأ فقط 10٪ من عمليات معالجة البيانات التي تقوم بها أدوات Plaid من قبل المستخدمين الذين يجلسون على أجهزة الكمبيوتر وربطوا حساباتهم بالتطبيق. كل ما تبقى هو معاملات لتحديث المعاملات التي يتم تنفيذها بشكل دوري دون وجود المستخدم. تمت إضافة المنطق إلى نظام موازنة التحميل الذي نستخدمه للتأكد من أن الطلبات المقدمة من المستخدمين لها الأسبقية على طلبات تحديث المعاملة. سمح لنا ذلك بمعالجة رشقات نشاط عمليات الوصول إلى واجهة برمجة التطبيقات في 1000٪ أو أكثر. وقد تم ذلك من خلال المعاملات التي تهدف إلى تحديث البيانات.

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

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

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

كيف قدمنا ​​التحديثات ، مع الحرص على الموثوقية


ool الأدوات والرصد


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

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

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

  • زودنا مكدس ELK المستخدم بالفعل من قبلنا بكمية كافية من المعلومات المسجلة ، مما قد يكون مفيدًا في اكتشاف ما يحدث في النظام بسرعة.
  • لقد أضفنا العديد من مقاييس بروميثيوس إلى النظام. بما في ذلك ما يلي:

    • الحصول على حجم كومة الذاكرة المؤقتة V8 باستخدام process.memoryUsage() .
    • معلومات حول عمليات جمع القمامة باستخدام حزمة gc-stats .
    • البيانات في الوقت المستغرق لإكمال المهام ، مجمعة حسب نوع العمليات المتعلقة بالتكامل مع البنوك ، ومستوى التزامن. كنا بحاجة إلى هذا لقياس مدى تأثير التزامن على إنتاجية النظام بشكل موثوق.
  • أنشأنا لوحة التحكم Grafana ، المصممة لدراسة درجة تأثير التوازي على النظام.
  • بالنسبة لنا ، كانت القدرة على تغيير سلوك التطبيق دون الحاجة إلى إعادة نشر الخدمة مهمة للغاية. لذلك ، أنشأنا مجموعة من إشارات LaunchDarkly المصممة للتحكم في المعلمات المختلفة. مع هذا النهج ، فإن اختيار معايير العمال ، المحسوبة بحيث تصل إلى الحد الأقصى من التوازي ، سمح لنا بإجراء تجارب سريعة والعثور على أفضل المعايير ، وقضاء بضع دقائق في هذا.
  • من أجل معرفة كيفية تحميل أجزاء مختلفة من التطبيق للمعالج ، قمنا ببناء أدوات جمع بيانات خدمة الإنتاج ، والتي بنيت على أساسها مخططات اللهب.

    • استخدمنا حزمة 0x لأن أدوات Node.js كانت سهلة الاندماج في خدمتنا ، ولأن التصور النهائي لبيانات HTML دعم البحث وقدم لنا مستوى جيدًا من التفاصيل.
    • أضفنا وضع التوصيف إلى النظام عندما بدأ العامل بحزمة 0x قيد التشغيل ، وعند الخروج ، كتب البيانات النهائية في S3. بعد ذلك ، يمكننا تنزيل السجلات التي نحتاجها من S3 وعرضها محليًا باستخدام أمر من النموذج 0x --visualize-only ./flamegraph . / 0x --visualize-only ./flamegraph .
    • في فترة زمنية معينة ، بدأنا التنميط لعامل واحد فقط. يعمل التنميط على زيادة استهلاك الموارد وتقليل الإنتاجية ، لذلك نود قصر هذه الآثار السلبية على عامل واحد.

▍ ابدأ النشر


بعد الانتهاء من الإعداد الأولي ، أنشأنا مجموعة ECS جديدة لـ "العمال الموازيين". هؤلاء هم العمال الذين استخدموا أعلام LaunchDarkly لضبط الحد الأقصى لمستوى التوازي بشكل ديناميكي.

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

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

توسيع ، واستكشاف ، كرر


زيادة الحد الأقصى لحجم كومة الذاكرة المؤقتة لـ Node.js


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

 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - Javascript heap out of memory 1: node::Abort() 2: node::FatalException(v8::Isolate*, v8::Local, v8::Local) 3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) 4: v8::internal::Factory::NewFixedArray(int, v8::internal::PretenureFlag) 

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

اقترحنا أن زيادة الحد الأقصى لحجم كومة الذاكرة المؤقتة Node.js ، والذي تم تعيينه على 1.7 غيغابايت بشكل افتراضي ، يمكن أن يساعد في حل هذه المشكلة. ثم بدأنا في تشغيل Node.js ، بتعيين الحد الأقصى لحجم الكومة إلى 6 جيجابايت (باستخدام علامة سطر الأوامر --max-old-space-size=6144 ). كانت هذه هي القيمة الأكبر التي كانت مناسبة لحالات EC2 الخاصة بنا. من دواعي سرورنا ، مثل هذه الخطوة سمحت لنا للتعامل مع الخطأ أعلاه الذي يحدث في الإنتاج.

▍ تحديد عنق الزجاجة الذاكرة


بعد أن حللنا مشكلة تخصيص الذاكرة ، بدأنا نواجه ضعف إنتاجية المهام على العمال الموازيين. في الوقت نفسه ، لفت أحد الرسوم البيانية على لوحة التحكم انتباهنا على الفور. كان هذا تقريرا عن كيفية استخدام عمليات العامل المتوازي حفنة.


استخدام كومة

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

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

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

لقد بدأنا في البحث في التعليمات البرمجية الخاصة بنا عن العمليات التي يتم تنفيذها على أساس مبدأ "تم التنفيذ والنسيان". وتسمى أيضا "الوعود العائمة" ("الوعد العائم"). كان الأمر بسيطًا - كان كافياً للعثور على الخطوط التي تم فيها تعطيل قاعدة linter - promo- وعد . جذبت طريقة واحدة انتباهنا. قام بإجراء مكالمة إلى compressAndUploadDebuggingPayload دون انتظار النتائج. يبدو أن مثل هذه الدعوة يمكن أن تستمر بسهولة لفترة طويلة حتى بعد اكتمال معالجة المهمة.

 const postTaskDebugging = async (data: TypedData) => {    const payload = await generateDebuggingPayload(data);       //       ,    //        .    // tslint:disable-next-line:no-floating-promises    compressAndUploadDebuggingPayload(payload)        .catch((err) => logger.error('failed to upload data', err)); } 

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


باستخدام كومة الذاكرة المؤقتة بعد تعطيل postTaskDebugging

اتضح! الآن مستوى استخدام كومة الذاكرة المؤقتة في العمال موازية يبقى ثابتاً على مدى فترة طويلة من الوقت.

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

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

بعد أن قررنا أننا لن نستخدم هذا الحل للمشكلة إلا كحل أخير ، بدأنا في التفكير في تحسين الكود. كيفية تسريع هذه العملية؟

ixFix عنق الزجاجة S3


من السهل معرفة منطق compressAndUploadDebuggingPayload . نحن هنا نضغط بيانات تصحيح الأخطاء ، ويمكن أن يكون حجمها كبيرًا ، بما في ذلك حركة مرور الشبكة. ثم نقوم بتحميل البيانات المضغوطة إلى S3.

 export const compressAndUploadDebuggingPayload = async (    logger: Logger,    data: any, ) => {    const compressionStart = Date.now();    const base64CompressedData = await streamToString(        bfj.streamify(data)            .pipe(zlib.createDeflate())            .pipe(new b64.Encoder()),    );    logger.trace('finished compressing data', {        compression_time_ms: Date.now() - compressionStart,    );           const uploadStart = Date.now();    s3Client.upload({        Body: base64CompressedData,        Bucket: bucket,        Key: key,    });    logger.trace('finished uploading data', {        upload_time_ms: Date.now() - uploadStart,    ); } 

من سجلات Kibana ، كان من الواضح أن تنزيل البيانات إلى S3 ، حتى لو كان حجمها صغيرًا ، يستغرق الكثير من الوقت. لم نعتقد في البداية أن مآخذ التوصيل يمكن أن تصبح عنق الزجاجة في النظام ، حيث يقوم وكيل Node.js HTTPS القياسي بتعيين المعلمة maxSockets على Infinity . ومع ذلك ، في النهاية ، قرأنا وثائق AWS على Node.js ووجدنا شيئًا يثير الدهشة بالنسبة لنا: العميل S3 يقلل من قيمة المعلمة maxSockets إلى 50 . وغني عن القول ، هذا السلوك لا يمكن أن يسمى بديهية.

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

 const s3Client = new AWS.S3({    httpOptions: {        agent: new https.Agent({            //                 //          S3.            maxSockets: 1024 * 20,        }),    },    region, }); 

▍ تسريع التسلسل JSON


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

يتكون ضغط البيانات من ثلاث مراحل (هنا ، للحد من استخدام الذاكرة ، يتم استخدام تدفقات Node.js). وهي ، في المرحلة الأولى ، يتم إنشاء سلسلة بيانات JSON ، في الثانية ، يتم ضغط البيانات باستخدام zlib ، في المرحلة الثالثة ، يتم تحويلها إلى تشفير base64. نشك في أن مصدر المشكلات يمكن أن يكون مكتبة الطرف الثالث التي نستخدمها لإنشاء سلاسل JSON - bfj . لقد كتبنا نصًا يفحص أداء المكتبات المختلفة لإنشاء بيانات سلسلة JSON باستخدام التدفقات (يمكن العثور على الرمز المقابل هنا ). اتضح أن حزمة Big Friendly JSON التي كنا نستخدمها لم تكن ودية على الإطلاق. انظر فقط إلى نتائج بضع قياسات تم الحصول عليها أثناء التجربة:

 benchBFJ*100:    67652.616ms benchJSONStream*100: 14094.825ms 

نتائج مذهلة. حتى في اختبار بسيط ، تبين أن حزمة bfj أبطأ بخمس مرات من الحزمة الأخرى ، JSONStream. لمعرفة ذلك ، سرعان ما قمنا بتغيير bfj إلى JSONStream ورأينا على الفور زيادة كبيرة في الأداء.

▍ تقليل الوقت اللازم لجمع القمامة


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


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

يرجى ملاحظة أنه في بعض الأحيان يكون المؤشر في حدود 8: 1 ، وهذا على الرغم من أن متوسط ​​مستوى موازاة المهام منخفض نسبيًا وأنه يقع في نطاق 30. لقد أدركنا أن المهام التي نحلها فيما يتعلق بالتفاعل مع البنوك لا تنشئ عبء ثقيل على المعالجات. عرفنا أيضًا أن حاوياتنا "الموازية" لم تكن محدودة بأي شكل من الأشكال. لا نعرف أين نبحث عن سبب المشكلة ، وذهبنا لقراءة المواد على تحسين مشاريع Node.js. على الرغم من قلة عدد هذه المقالات ، فقد صادفنا هذه المادة ، والتي تتناول تحقيق 600 ألف اتصال عبر شبكة الإنترنت في Node.js.

على وجه الخصوص ، تم لفت انتباهنا إلى استخدام --nouse-idle-notification . هل يمكن لعمليات Node.js الخاصة بنا قضاء الكثير من الوقت في جمع القمامة؟ هنا ، بالمناسبة ، أعطتنا حزمة gc-stats الفرصة للنظر في متوسط ​​الوقت الذي يقضيه في جمع القمامة.


تحليل الوقت الذي يقضيه في جمع القمامة

كان هناك شعور بأن عملياتنا قضت حوالي 30 ٪ من الوقت في جمع القمامة باستخدام خوارزمية Scavenge. هنا لن نصف التفاصيل الفنية المتعلقة بأنواع مختلفة من جمع القمامة في Node.js. إذا كنت مهتمًا بهذا الموضوع - ألق نظرة على هذه المواد. جوهر خوارزمية Scavenge هو أن مجموعة البيانات المهملة تبدأ غالبًا بمسح الذاكرة التي تشغلها كائنات صغيرة في كومة Node.js التي تسمى "مساحة جديدة".

لذلك ، اتضح أنه في Node.js عملياتنا يبدأ جمع القمامة في كثير من الأحيان. هل يمكنني تعطيل مجموعة البيانات المهملة V8 وتشغيلها بنفسي؟ هل هناك طريقة لتقليل تكرار مكالمة جمع القمامة؟ اتضح أن أول ما سبق لا يمكن القيام به ، ولكن الأخير - من الممكن! يمكننا ببساطة زيادة حجم مساحة "المساحة الجديدة" عن طريق زيادة الحد من مساحة "المساحة شبه" في Node.js باستخدام --max-semi-space-size=1024 سطر الأوامر --max-semi-space-size=1024 . يسمح لك ذلك بإجراء المزيد من عمليات تخصيص الذاكرة للكائنات قصيرة العمر حتى يبدأ V8 في تجميع البيانات المهملة. نتيجة لذلك ، يتم تقليل وتيرة إطلاق مثل هذه العمليات.


نتائج تحسين جمع القمامة

انتصار آخر! أدت الزيادة في مساحة "المساحة الجديدة" إلى انخفاض كبير في مقدار الوقت الذي يقضيه في جمع القمامة باستخدام خوارزمية Scavenge - من 30 ٪ إلى 2 ٪.

pt تحسين الاستفادة من المعالج


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

بناءً على البيانات التي تم جمعها على أحد العمال المتوازيين لدينا ، تم إنشاء جدول ناري. كان لدينا تصور أنيق تحت تصرفنا ، والتي يمكننا أن نعمل على الجهاز المحلي. نعم ، إليك تفاصيل مثيرة للاهتمام: كان حجم هذه البيانات 60 ميغابايت. هذا ما رأيناه من خلال البحث عن كلمة logger في المخطط الناري 0x.


تحليل البيانات مع أدوات 0x

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

بعد هذا التحسين ، تمكنا من معالجة ما يصل إلى 30 مهمة في وقت واحد في عامل واحد دون الإضرار بأداء النظام.

النتائج


لقد أدى التحول إلى العمال الموازيين إلى خفض التكاليف السنوية لـ EC2 بحوالي 300 ألف دولار وبسّط بنية النظام إلى حد كبير. الآن نستخدم في الإنتاج حوالي 30 مرة أقل من الحاويات من قبل. نظامنا أكثر مقاومة للتأخير في معالجة الطلبات الصادرة وطلبات واجهة برمجة التطبيقات الواردة من المستخدمين.

أثناء موازنة خدمة التكامل مع البنوك ، تعلمنا الكثير من الأشياء الجديدة:

  • لا تقلل أبداً من أهمية وجود مقاييس نظام منخفضة المستوى. لقد وفرت لنا القدرة على مراقبة البيانات المتعلقة بجمع القمامة واستخدام الذاكرة مساعدة هائلة في نشر النظام ووضع اللمسات الأخيرة عليه.
  • الرسومات المشتعلة هي أداة رائعة. الآن بعد أن تعلمنا كيفية استخدامها ، يمكننا بسهولة تحديد الاختناقات الجديدة في النظام بمساعدتهم.
  • سمح لنا فهم آليات وقت تشغيل Node.js بكتابة كود أفضل. على سبيل المثال ، عند معرفة كيفية تخصيص V8 للذاكرة للكائنات ، وكيف تعمل مجموعة البيانات المهملة ، رأينا نقطة في استخدام أسلوب إعادة استخدام الكائنات على أوسع نطاق ممكن. في بعض الأحيان ، من أجل فهم كل هذا بشكل أفضل ، تحتاج إلى العمل مباشرةً مع V8 أو تجربة علامات سطر الأوامر Node.js.
  • , . maxSocket , Node.js, , , , AWS Node.js . , , , .

أعزائي القراء! Node.js-?

Source: https://habr.com/ru/post/ar483688/


All Articles