كثيرون يشكون في احتمال التزوير وكتابة شيء بمفردهم. في كثير من الأحيان السعر مرتفع جدا. من الغريب أن نسمع عن JDKs الخاصة بك ، والتي من المفترض أن تكون في كل شركة كبيرة إلى حد ما. ماذا بحق الجحيم تحتدم مع الدهون؟ ستكون هذه المقالة قصة مفصلة عن الشركة ، والتي تحقق كل ذلك فوائد تجارية حقيقية ، والتي أدت عملاً فظيعًا ، لأنها:
- طورت آلة جافا افتراضية متعددة المستأجرين ؛
- توصلوا إلى آلية لتشغيل الأشياء التي لا تجلب النفقات العامة إلى جمع القمامة ؛
- لقد فعلوا شيئًا مثل نظير ReadyNow من Azul Zing ؛
- لقد قاموا بتجريف آكلاتهم الخاصة ذات العائد والاستمرارية (وحتى أنهم على استعداد لمشاركة تجربتهم مع Loom ، التي كتبت عنها في الخريف ) ؛
- ثملوا على كل هذه المعجزات نظامهم الفرعي الخاص للتشخيص.
كما هو الحال دائمًا ، ينتظرك الفيديو وفك تشفير النص الكامل والشرائح تحت الضغط. مرحبا بكم في جحيم واحدة من أصعب مجالات التكيف للمشاريع مفتوحة المصدر!

دكتور ، من اين لك مثل هذه الصور؟ أورايلي Covers Corner: خلفية KDPV مقدمة من قبل Joshua Newton وتصور رقصة Sangyang Jaran المقدسة في Ubud ، إندونيسيا. هذا أداء باليني كلاسيكي يتكون من رقصة النار والنشوة. رجل ذو كعب عارف يتحرك حول نار ، ولدت على قشور جوز الهند ، وشق الأشياء مع قدميه والرقص في حالة نشوة تحت تأثير روح الحصان. التوضيح المثالي ل JDK الخاصة بك ، أليس كذلك؟
شرائح ووصف للتقرير (لا تحتاج إليها ، يحتوي habratopike على كل ما تحتاجه).
مرحباً ، اسمي Sanhong Lee ، أعمل في Alibaba ، وأود أن أتحدث عن التغييرات التي أجريناها على OpenJDK لتلبية احتياجات أعمالنا. المنصب يتكون من ثلاثة أجزاء. في البداية سأتحدث عن كيفية استخدام Java في Alibaba. الجزء الثاني ، في رأيي ، هو الأهم - حيث سنناقش كيفية تكوين OpenJDK لاحتياجات أعمالنا. الجزء الثالث سيكون حول الأدوات التي أنشأناها للتشخيص.
ولكن قبل الانتقال إلى الجزء الأول ، أود أن أخبركم بإيجاز عن شركتنا.

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

في الصورة أعلاه ، ترى مخططًا يوضح الحمل على نظام الدعم الخاص بنا. يوضح الخط الأحمر عمل خدمة الطلب الخاص بنا ويظهر أعلى عدد من المعاملات في الثانية ، حيث بلغ العام الماضي 325 ألف. يشير الخط الأزرق إلى خدمة الدفع ، ولديها هذا الرقم البالغ 256 ألف. أود التحدث عن كيفية تحسين المكدس الذي يخدم العديد من المعاملات.
دعنا نناقش التقنيات الرئيسية التي تعمل في علي بابا مع جافا. بادئ ذي بدء ، يجب أن أقول أن لدينا عددًا من التطبيقات المفتوحة المصدر كأساس. لمعالجة البيانات الكبيرة نستخدم HBase Hadoop. كحاوية نستخدم Tomcat و OSGi. يتم استخدام Java على نطاق هائل - يتم نشر ملايين مثيلات JVM في مركز البيانات الخاص بنا. يجب أن أقول أيضًا أن هيكلنا موجه نحو الخدمة ، أي أننا ننشئ العديد من الخدمات التي تتواصل مع بعضها البعض باستخدام مكالمات RPC. أخيرًا ، الهندسة المعمارية لدينا غير متجانسة. لتحسين الأداء ، تتم كتابة العديد من الخوارزميات باستخدام مكتبات C و C ++ ، لذلك يتواصلون مع Java باستخدام مكالمات JNI.

بدأ تاريخ عملنا مع OpenJDK في عام 2011 ، خلال OpenJDK 6. هناك ثلاثة أسباب مهمة لاختيارنا OpenJDK. أولاً ، يمكننا تغيير الرمز مباشرةً وفقًا لاحتياجات العمل. ثانياً ، عندما تنشأ مشاكل ملحة ، يمكننا حلها بمفردنا بشكل أسرع من انتظار الإصدار الرسمي. هذا أمر حيوي لأعمالنا. ثالثًا ، يستخدم مطورو جافا أدواتنا الخاصة لتصحيح الأخطاء والتشخيص بسرعة وعالية الجودة.
قبل الانتقال إلى المشكلات الفنية ، أريد سرد الصعوبات الرئيسية التي يتعين علينا التغلب عليها. أولاً ، لقد أطلقنا عددًا كبيرًا من مثيلات JVM - في هذه الحالة ، فإن مسألة تخفيض تكاليف الأجهزة هي مشكلة حادة. ثانياً ، لقد قلت بالفعل أننا نخدم عددًا كبيرًا من المعاملات. بفضل جامع القمامة ، تعدنا Java بـ "ذاكرة لا نهائية". بالإضافة إلى ذلك ، فازت في الأداء بمستوى منخفض بفضل برنامج التحويل البرمجي JIT. ولكن هذا له جانب آخر: وقت أطول في العالم لجمع القمامة. بالإضافة إلى ذلك ، يحتاج Java إلى دورات CPU إضافية لتجميع أساليب Java. هذا يعني أن المترجمين يتنافسون على دورات وحدة المعالجة المركزية. تفاقمت كلتا المشكلتين عندما يصبح التطبيق أكثر تعقيدًا.
الصعوبة الثالثة هي أن لدينا الكثير من التطبيقات قيد التشغيل. أعتقد أن الجميع هنا على دراية بالأدوات التي تأتي مع OpenJDK ، مثل JConsole أو VisualVM. المشكلة هي أنهم لا يقدمون لنا المعلومات الدقيقة التي نحتاج إلى تكوينها. بالإضافة إلى ذلك ، عندما نستخدم هذه الأدوات (على سبيل المثال ، JConsole أو VisualVM) في الإنتاج ، فإن الحمل المنخفض ليس مجرد رغبة ، ولكنه مطلب ضروري. اضطررت إلى كتابة أدوات التشخيص الخاصة بي.

توضح الصورة التغييرات التي أجريناها على OpenJDK. دعونا نلقي نظرة على كيفية التغلب على الصعوبات التي تحدثت عنها أعلاه.
متعدد المستأجر JVM
حل واحد نسميه JVM متعدد المستأجرين. يتيح لك تشغيل تطبيقات الويب المتعددة بأمان في حاوية واحدة. حل آخر يسمى GCIH (GC غير مرئية كومة). هذه هي الآلية التي توفر لك كائنات Java كاملة ، والتي في الوقت نفسه لا تتطلب تكلفة جمع القمامة. علاوة على ذلك ، لتقليل تكاليف سياقات سلاسل الرسائل ، قمنا بتطبيق coroutines على منصة Java الخاصة بنا. بالإضافة إلى ذلك ، لقد كتبنا آلية تسمى JWarmup - وظيفتها تشبه إلى حد بعيد ReadyNow. يبدو أن دوغلاس هوكينز ذكره في تقريره . أخيرًا ، قمنا بتطوير أداة التنميط الخاصة بنا ، ZProfiler.
دعونا نلقي نظرة فاحصة على كيفية تطبيقنا للعقود المتعددة المستندة إلى OpenJDK.

ألقِ نظرة على الصورة أعلاه - أعتقد أن معظمكم على دراية بهذا النمط. مقارنة النهج التقليدي مع المستأجر متعددة. إذا كان التطبيق الخاص بك يعمل باستخدام Apache Tomcat ، فيمكنك أيضًا تشغيل مثيلات متعددة في نفس الحاوية. لكن Tomcat لا يوفر استهلاكًا مستقرًا للموارد لكل منها. إذا كان أحد التطبيقات قيد التشغيل يحتاج إلى مزيد من وقت وحدة المعالجة المركزية أكثر من الآخر ، كيف يمكنك التحكم في تخصيص وقت وحدة المعالجة المركزية؟ كيفية التأكد من أن هذا التطبيق لا يؤثر على عمل الآخرين؟ كان هذا السؤال هو الأساس الذي جعلنا ننتقل إلى التكنولوجيا متعددة الجنسيات.

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

في مقتطف الشفرة المقدم ، ننشئ مستأجرًا واحدًا ، ثم نبيّن كم من الوقت يتم توفير وحدة المعالجة المركزية والذاكرة له. المؤشر الأول هو عدد صحيح ، مما يعني أن حصة وقت وحدة المعالجة المركزية المتاحة للمستأجر ، في هذه الحالة أشرنا إلى 512. نستخدم نهجًا مشابهًا جدًا في حالة مجموعات cgroups ، وسأتناول هذا بمزيد من التفاصيل. المقياس الثاني هو الحد الأقصى لحجم الكومة الذي يمكن للمستأجرين استخدامه.
النظر في كيفية تفاعل المستأجر مع موضوع. توفر الفئة .run()
طريقة .run()
، وعندما يدخلها مؤشر ترابط ، يتم .run()
تلقائيًا إلى المستأجر ، وعندما يتركها ، يحدث الإجراء العكسي. لذلك يتم تنفيذ كافة التعليمات البرمجية داخل الأسلوب .run()
. بالإضافة إلى ذلك ، يتم إرفاق أي مؤشر ترابط تم إنشاؤه داخل الأسلوب .run()
من مؤشر الترابط الأصل.
لقد توصلنا إلى سؤال مهم للغاية - كيف تتم إدارة وحدة المعالجة المركزية في JVM متعدد المستأجرين؟ لقد تم تطبيق حلنا للتو على نظام التشغيل Linux x64. هناك آلية السيطرة المجموعة ، cgroups. يسمح لك بتحديد عملية في مجموعة منفصلة ، ثم الإشارة إلى وضع استهلاك الموارد لكل مجموعة. دعنا نحاول نقل هذا النهج إلى سياق Hotspot JVM. في Hotstpot ، يتم تنظيم مؤشرات ترابط Java على أنها مؤشرات ترابط أصلية.

يظهر هذا في الرسم البياني أعلاه: كل خيط Java موجود في مراسلات فردية مع الخيط الأصلي. في مثالنا ، لدينا حاوية TenantA
، والتي يوجد بها TenantA
. من أجل التمكن من التحكم في توزيع وقت وحدة المعالجة المركزية ، فإننا نضع كلا الخيوط الأصلية في مجموعة تحكم واحدة. نتيجة لهذا ، يمكننا تنظيم استهلاك الموارد ، بالاعتماد فقط على وظائف [مجموعات التحكم] ( https://en.wikipedia.org/wiki/Cgroups ).
دعنا نلقي نظرة على مثال أكثر تفصيلا.

يتم تعيين مجموعات التحكم على Linux إلى دليل. في مثالنا ، أنشأنا دليل /t0
للمستأجر 0. يحتوي هذا الدليل على دليل /t0/tasks
، وستكون كل مؤشرات t0
موجودة هنا. ملف مهم آخر هو /t0/cpu.shares
. فهو يشير إلى مقدار الوقت الذي سيتم إعطاء وحدة المعالجة المركزية لهذا المستأجر. هذه البنية بأكملها موروثة من مجموعات التحكم - لقد كفلنا مجرد مراسلات مباشرة بين سلسلة رسائل Java ، سلسلة الرسائل الأصلية ومجموعة التحكم.
مسألة أخرى مهمة تتعلق بإدارة مجموعة من كل مستأجر.

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

بشكل تخطيطي ، يتم توضيح هذه العملية في الرسم البياني أعلاه. كما قلت ، يعتمد تطبيقنا على G1GC. G1GC هو أداة تجميع مجمعي البيانات المهملة ، لذلك أثناء تجميع البيانات المهملة نحتاج إلى التأكد من نسخ الكائن إلى القسم الصحيح من الكومة. على الشريحة ، يجب نسخ جميع الكائنات التي تم إنشاؤها بواسطة Tenant-1
إلى جزءها من الكومة ، على غرار Tenant-2
.
هناك اعتبارات أخرى تنشأ عندما يتم عزل المستأجرين عن بعضهم البعض. هنا يجب أن أقول عن TLAB (مؤشر ترابط تخصيص الخيط المحلي) - آلية للتخصيص السريع للذاكرة. تعتمد مساحة TLAB على مقطع الكومة. كما قلت ، فإن المستأجرين المختلفين لديهم مجموعات مختلفة من أقسام الكومة.

يتم عرض تفاصيل العمل مع TLAB على الشريحة - عندما ينتقل مؤشر الترابط من Tenant 1
إلى Tenant 2
، نحتاج إلى التأكد من استخدام قسم الكومة الصحيح لمساحة TLAB. ويمكن تحقيق ذلك بطريقتين. الطريقة الأولى هي عندما Tenant 1
مؤشر Thread A
من Tenant 1
إلى Tenant 2
، ونتخلص من القديم Tenant 2
واحدًا جديدًا في Tenant 2
. هذه الطريقة سهلة التنفيذ نسبيًا ، لكنها تهدر المساحة في TLAB ، وهو أمر غير مرغوب فيه. الطريقة الثانية أكثر تعقيدًا - لجعل TLAB على دراية بالمستأجرين. هذا يعني أنه سيكون لدينا العديد من المخازن المؤقتة TLAB لمؤشر ترابط واحد. عندما Tenant 1
مؤشر Thread A
من Tenant 1
إلى Tenant 2
، نحتاج إلى تغيير المخزن المؤقت واستخدام واحد تم إنشاؤه في Tenant 2
.
هناك آلية أخرى يجب أن تقال فيما يتعلق بتعيين المستأجرين وهي IHOP (بدء نسبة شغل الخيط). في البداية ، تم حساب IHOP على أساس الكومة بأكملها ، ولكن في حالة وجود آلية متعددة العناصر ، يجب حسابها على أساس قسم واحد فقط من الكومة.
دعونا نلقي نظرة فاحصة على ما هو GCIH (GC Invisible Heap). تقوم هذه الآلية بإنشاء قسم على الكومة ، مخفي عن أداة تجميع مجمعي البيانات المهملة ، وبالتالي ، لا يتأثر بمجموعة البيانات المهملة. يدار هذا الموقع من قبل المستأجر GCIH.

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

تُظهر الصورة مخطط GCIH عالي المستوى. على اليمين يوجد كومة Java عادية ، على اليسار توجد المساحة المخصصة لـ GCIH. الارتباطات من كومة الذاكرة المؤقتة العادية إلى الكائنات الموجودة في GCIH صالحة ، لكن الارتباطات من GCIH إلى كومة الذاكرة المؤقتة غير صالحة. لفهم سبب ذلك ، فكر في مثال. لدينا كائن "A" في GCIH ، والذي يحتوي على إشارة إلى الكائن "B" في كومة الذاكرة المؤقتة العادية. المشكلة هي أنه يمكن نقل الكائن B بواسطة أداة تجميع مجمعي البيانات المهملة. كما قلت من قبل ، نحن لا نقوم بتحديثات في GCIH ، لذلك بعد عمل أداة تجميع مجمعي البيانات المهملة ، قد يحتوي الكائن "A" على مرجع غير صالح للكائن "B". يمكن حل هذه المشكلة باستخدام حاجز ما قبل الكتابة - تمت مناقشتها في تقرير سابق. على سبيل المثال ، افترض أن شخصًا ما يحتاج إلى حفظ ارتباط من كومة Java عادية إلى GCIH قبل الحفظ الذي افترضنا أنه سيؤدي إلى استثناء من التنبؤ بعلامة مؤشر على انتهاك القاعدة.
لتطبيق معين ، يتم استخدام JVM متعدد المستأجرين في منصة التخصيص Taobao الخاصة بنا ، TPP المختصرة. هذا هو نظام التوصية لتطبيق التسوق الإلكتروني لدينا. يمكن لـ TPP نشر العديد من الخدمات المصغرة في حاوية واحدة ، وبمساعدة JVM متعدد المستأجرين ، فإننا نتحكم في الذاكرة ووقت وحدة المعالجة المركزية (CPU) المتاح لكل خدمة من خدمات micros.
بالنسبة إلى GCIH ، يتم استخدامه في نظامنا الآخر ، UM Platform. هذا هو تطبيق خصم على الانترنت. يستخدم مالك هذا التطبيق GCIH لتخزين بيانات GCIH مسبقًا على الجهاز المحلي ، حتى لا يتم الوصول إلى الكائنات الموجودة على خادم التخزين المؤقت البعيد أو قاعدة البيانات عن بُعد. نتيجة لذلك ، نقوم بتخفيف الحمل على الشبكة ونقوم بإجراء تسلسل أقل وإزالة التسلسل.

تعرض الصورة رسمًا بيانيًا يوضح فيه اللون الأزرق الحمل عند استخدام JDK تقليدي ، والأحمر - GCIH. كما ترون ، نحن نخفض استخدام وحدة المعالجة المركزية بنسبة تزيد عن 18 ٪.
على حد علمي ، تم حل مشكلة مماثلة بواسطة BellSoft ، وكان حلها مشابهًا لـ GCIH ، لكنهم استخدموا طريقة مختلفة لتقليل تكاليف التسلسل وإلغاء التسلسل.
Coroutines في جاوة
دعنا نعود إلى Alibaba ونرى كيف يمكن تنفيذ coroutines في Java. لكن أولاً ، دعنا نتحدث عن الأصول ، لماذا نحتاج إلى القيام بذلك. في Java ، كان من السهل جدًا دائمًا كتابة تطبيقات ذات مؤشرات ترابط متعددة. ولكن المشكلة في إنشاء مثل هذه التطبيقات هي ، كما قلت ، في Hotspot Java تم تنفيذ سلاسل الرسائل بالفعل كخيط أصلي. لذلك ، عندما يكون هناك الكثير من مؤشرات الترابط في التطبيق الخاص بك ، تصبح تكاليف تغيير سياق سلسلة الرسائل عالية جدًا.

فكر في مثال سيكون لدينا فيه 4 مؤشرات ترابط إدخال / إخراج و 200 مؤشر ترابط مع منطق التطبيق الخاص بك. يعرض الجدول على الشاشة نتائج بدء هذا العرض البسيط - يمكنك معرفة مقدار الوقت الذي تستغرقه وحدة المعالجة المركزية لتغيير السياقات. قد يكون الحل لهذه المشكلة هو تنفيذ corutin في جافا.
لتوفير ذلك ، كنا بحاجة إلى شيئين. أولا ، علي بابا JDK اللازمة لإضافة دعم الاستمرارية. استند هذا العمل على تصحيح JKU ، وسنتناوله بمزيد من التفاصيل. ثانياً ، أضفنا sheduler وضع المستخدم الذي سيكون مسؤولاً عن استمرار في موضوع. ثالثا ، هناك الكثير من التطبيقات في علي بابا. لذلك ، يعد حلنا مهمًا جدًا لمطوري برامج Java ، وكان من الضروري جعله شفافًا تمامًا بالنسبة لهم. وهذا يعني أنه في تطبيق أعمالنا يجب ألا يكون هناك أي تغييرات في الكود عملياً. نحن ندعو الحل Wisp. يستخدم تطبيقنا على coroutines في Java على نطاق واسع في Alibaba ، لذلك يمكن اعتباره أثبت أنه يعمل في Java. تعرف عليه بمزيد من التفاصيل.

لنبدأ بالمثال ، الذي تم عرض الكود الخاص به أعلاه - هذا تطبيق Java عادي تمامًا. أولاً ، يتم إنشاء تجمع مؤشر ترابط. ثم يتم إنشاء مهمة Runnable أخرى تقبل المقبس. بعد ذلك ، يتم تنفيذ القراءة من الدفق. بعد ذلك ، نقوم بإنشاء مهمة Runnable أخرى ، نتصل بها بالخادم ، وأخيراً ، نكتب البيانات إلى الدفق. كما ترون ، يبدو كل شيء قياسيًا تمامًا. إذا قمت بتشغيل الكود على JDK عادي ، فسيتم تنفيذ كل مهمة من مهام Runnable في سلسلة رسائل منفصلة. ولكن في قرارنا ، ستكون الميكانيكا مختلفة تمامًا.

كما ترون من تفريغ الخيط الموضح على الشريحة ، نقوم بإنشاء اثنين من coroutines في خيط واحد ، وليس خيطين. الآن تحتاج إلى جعل هذا الحل يعمل. الشيء الرئيسي هنا هو جعل أحداث توليد الربح في جميع نقاط الحجب الممكنة. في مثالنا ، ستكون هذه النقاط serverSocket.accept()
و is.read(buf)
ووصلة مقبس و os.write(buf)
. بفضل نتائج الأحداث في هذه النقاط ، سنكون قادرين على نقل التحكم من coroutine إلى آخر داخل نفس الموضوع. لتلخيص ، نهجنا هو أن نحقق أداء غير متزامن باستخدام coroutine ، ولكن يمكن للمبرمجين لدينا كتابة التعليمات البرمجية بأسلوب متزامن ، لأن هذا الرمز هو أبسط بكثير وأسهل في الصيانة والتصحيح.
دعونا نلقي نظرة على كيفية تقديم الدعم المستمر في Alibaba JDK. كما قلت ، يعتمد هذا العمل على مشروع الجهاز الظاهري متعدد اللغات الذي أنشأه المجتمع - إنه في المجال العام. استخدمنا هذا التصحيح في بابا JDK وإصلاح بعض الأخطاء التي حدثت في بيئة الإنتاج لدينا.

كما ترون في الرسم التخطيطي ، هنا في موضوع واحد يمكن أن يكون هناك العديد من coroutines ، ولكل منها يتم إنشاء كومة منفصلة. بالإضافة إلى ذلك ، يوفر لنا التصحيح الذي تحدثت عنه واجهة برمجة التطبيقات الأكثر أهمية هنا - yieldTo ، بمساعدة التحكم الذي يتم نقله من coroutine إلى آخر.
دعنا ننتقل إلى كيفية تطبيقنا sheduler وضع المستخدم ل coroutine. نحن نستخدم محدد ، ومعه نسجل عدة قنوات. عند حدوث أي حدث إدخال / إخراج (قراءة مأخذ التوصيل ، أو كتابة مأخذ توصيل ، أو توصيل مأخذ توصيل ، أو قبول مأخذ توصيل) ، يتم كتابته كمفتاح محدد. لذلك ، في نهاية هذا الحدث ، نتلقى تنبيهًا من المحدد. وبالتالي ، فإننا نستخدم محددًا لتخطيط coroutines في حالة قفل I / O. النظر في مثال على كيفية عمل هذا.

في الصورة نرى مأخذ التوصيل و client.read(buffer)
اتصال متزامن client.read(buffer)
. في الجزء السفلي من الشريحة ، يتم كتابة رمز سيتم تنفيذه داخل هذه المكالمة. أولاً ، يتحقق ما إذا كان من الممكن القراءة من القناة أم لا. إذا كان الأمر كذلك ، فإننا نعيد النتيجة. يحدث الشيء الأكثر إثارة للاهتمام إذا كانت القراءة لا يمكن القيام بها. ثم نسجل حدث القراءة في جدولة لدينا مع محدد. هذا يجعل من الممكن التخطيط لتنفيذ أي coroutine أخرى. نلقي نظرة على كيف يحدث هذا. لدينا موضوع يتم فيه إنشاء برنامج جدولة. الخيط و coroutine لدينا في المراسلات واحد إلى واحد مع بعضها البعض. Sheduler يسمح لنا لإدارة coroutines من هذا الموضوع. ماذا يحدث إذا تم حظر الإدخال / الإخراج؟ عند حدوث أحداث I / O ، يتلقى sheduler تنبيهًا ، وفي هذه الحالة يعتمد تمامًا على المحدد. بعد مثل هذا الحدث ، يحصل sheduler على فرصة للتخطيط ل coroutine المقبل المتاحة.
دعونا نلخص نظرة عامة على sheduler لدينا ، والتي أطلقنا عليها WispEngine. لكل موضوع من مواضيعنا ، نخصص WispEngine منفصل. عند حدوث قفل coroutine ، نقوم بتسجيل أحداث معينة (مأخذ القراءة / الكتابة وما إلى ذلك) باستخدام WispEngine. ترتبط بعض الأحداث بوقوف انتظار مؤشر الترابط ، على سبيل المثال ، إذا قمت بالاتصال بـ thread.sleep()
مع تأخير قدره 100 مللي ثانية. في هذه الحالة ، سيتم إنشاء حدث لوقوف السيارات في الخيط ، وسيتم تسجيله بعد ذلك في المُحدد. قضية أخرى مهمة هي عندما يعين sheduler coroutine التالي المتاح. هناك شرطان رئيسيان. الأول هو عندما يتم إنشاء أحداث معينة ، مثل أحداث I / O أو أحداث المهلة. كل شيء بسيط هنا: لنفترض أنك تجري مكالمة على thread.sleep()
مع تأخير قدره 200 مللي ثانية. عندما تنتهي صلاحيتها ، فإن sheduler لديه الفرصة لتنفيذ coroutine المقبل المتاحة. أو هنا يمكننا التحدث عن بعض أحداث التفريغ التي يتم إنشاؤها ، على سبيل المثال ، من خلال استدعاء object.notify()
أو object.notifyAll()
الشرط الثاني هو عندما يقدم المستخدم طلبات جديدة ، وننشئ coroutine لخدمة هذه الطلبات ، ثم يعين sheduler تنفيذه.
هنا تحتاج أيضًا إلى أن تقول عن الخدمة التي أنشأناها ، WispThreadExecutor.

يتم تقديم رمز مثال على الشاشة ، ونرى أن هذا هو ExecutorService منتظم ، تم إنشاؤه بنفس الطريقة. .execute()
و submit()
للمهام Runnable ، ولكن المشكلة هي أن جميع المهام Runnable التي تمر عبر أسلوب submit()
سيتم تنفيذها في corutin ، وليس في سلسلة الرسائل. هذا الحل شفاف تمامًا بالنسبة لأولئك الذين سينفذون تطبيقنا ، وسيكونون قادرين على استخدام واجهة برمجة التطبيقات الخاصة بنا من أجل coroutines.

لقد جئت إلى الجزء الصعب الأخير من وظيفة - كيفية حل مسألة التزامن في coroutines. هذا سؤال معقد ، لذلك دعونا ننظر إليه بمثال مبسط. لدينا هنا coroutine A ( test::foo
) و corutin
( test::bar
). أولاً ، نخصص تنفيذ test:foo
إلى coroutine
Corutin
المكالمات wait()
. إذا لم يتم تنفيذ أي شيء ، فسيتم حظر سلسلة الرسائل الحالية من خلال الدعوة إلى wait()
. كما يمكن أن يرى من تفريغ الخيط ، سيحدث طريق مسدود ، ولن نتمكن من تحديد موعد تنفيذ coroutine التالي.
كيفية حل هذه المشكلة؟ يوفر Hotspot ثلاثة أنواع من الأقفال. الأول هو قفل سريع. هنا ، يتم تحديد مالك القفل من خلال العنوان الموجود في المجموعة. كما قلت ، كل من coroutines لدينا لديه كومة منفصلة. لذلك ، في حالة القفل السريع ، لا نحتاج إلى القيام بأي عمل إضافي. لا يوجد دعم مماثل لقفل متحيز في نظامنا. لقد جربنا ذلك على إنتاجنا واتضح أنه في حالة عدم وجود قفل متحيز ، فإن الأداء لا يتناقص. بالنسبة لنا هو مناسب جدا.

دعنا نتحدث عن قفل أكثر تعقيدًا - قفل مضغوط. دعنا ننظر مرة أخرى إلى المثال الذي ذكرته أعلاه. لدينا Corutin
( .foo()
) و Corutin B
( .bar()
). أولاً ، نخصص تنفيذ coroutine
وبدءه. ثم يستدعي Object.wait
، وبعد ذلك يدخل في قائمة الانتظار. بعد ذلك ، نأخذ خطوة مهمة للغاية: نقوم بإنشاء الحدث yieldTo
، والذي ينقل التحكم إلى مؤشر الترابط الرئيسي. بعد ذلك ، نبدأ Corutin B
يستدعي Object.notify
، ويتم unpark
أحداث unpark
المقابلة. وسوف يستيقظون في نهاية المطاف coroutine
بعد اكتمال تنفيذ bar()
، سيكون من الممكن نقل التحكم إلى coroutine
وبالتالي ، فإن الطريق المسدود الذي ذكرته سابقًا تم التغلب عليه تمامًا.
لنناقش الأداء الآن. نحن نستخدم coroutines في أحد تطبيقاتنا على الإنترنت. بناءً عليه ، يمكننا مقارنة عمل corutin مع عمل JDK منتظم.

كما ترون ، فهي تسمح لنا بتقليل استهلاك وقت المعالج بنسبة 10٪ تقريبًا. أدرك أن معظمكم على الأرجح ليس لديهم القدرة على إجراء هذه التغييرات المعقدة مباشرةً على كود JDK. لكن الاستنتاج الرئيسي هنا ، في رأيي ، هو أنه إذا كانت خسائر الأداء تكلف مالًا وكان المبلغ الناتج كبيرًا بدرجة كافية ، فيمكنك محاولة تحسين الأداء باستخدام مكتبة coroutine.
جارمارم
دعنا ننتقل إلى أداتنا الأخرى - JWarmup. إنه مشابه جدًا لأداة أخرى ، ReadyNow. كما نعلم ، يوجد في Java مشكلة الاحماء - المترجم في هذه المرحلة يتطلب دورات CPU إضافية. تسبب هذا لنا مشاكل - على سبيل المثال ، حدث خطأ TimeOut. عند التوسع ، تزداد هذه المشكلات سوءًا ، وفي حالتنا نتحدث عن تطبيق معقد للغاية - أكثر من 20 ألف فصل وأكثر من 50 ألف طريقة.
قبل أن نبدأ في استخدام JWarmup ، استخدم أصحاب تطبيقنا بيانات محاكاة للتدفئة. على هذه البيانات ، برنامج التحويل البرمجي JIT pre-compiled قبل تلقي الطلبات. لكن البيانات المحاكاة تختلف عن تلك الحقيقية ؛ لذلك ، فهي ليست ممثلة للمترجم. في بعض الحالات ، حدث deoptimization غير متوقع ، عانى الأداء. كان الحل لهذه المشكلة هو JWarmup. لديه مرحلتان رئيسيتان للعمل - التسجيل والتجميع. بابا لديه نوعان من البيئات ، بيتا والإنتاج. كلاهما يتلقى طلبات حقيقية من المستخدمين ، وبعد ذلك يتم نشر نفس الإصدار من التطبيق في هاتين البيئتين. في بيئة الإصدار التجريبي ، يتم جمع بيانات ملفات التعريف فقط ، على أساسها يتم إجراء تجميع أولي في الإنتاج.

دعونا نرى بمزيد من التفصيل نوع المعلومات التي نجمعها. نحتاج إلى كتابة الفئات التي تمت تهيئتها تمامًا ، والأساليب التي يتم تجميعها ، ثم يتم مسح هذه البيانات في السجل على محرك الأقراص الثابت ، والذي يمكن للمترجم الوصول إليه. أصعب لحظة هي تهيئة الطبقات. . — Bar
Foo.test()
, foo.count
. , .

JWarmup (tiered compilation), . , — CPU. JWarmup , CPU, JDK. , , JDK. , , .
JWarmup. , , , groovy-, Java-, . . , , «null check elimination». . , JWarmup , JWarmup, .
, Alibaba.

. JVM — , , . Java-, metaspace, VM ( VM) JIT-. OpenJDK. -, , . -, . HotMethodProfiling, , CPU. , , Honest Profiler , , , HotMethodProfiling. MethodTracing. , , . , metaspace . Java-, . metaspace , . Java.
, , ZProfiler.

. JVMTi, JVM ( ). , ZProfiler Apache Tomcat. -. ZProfiler JVM. , ZProfiler -UI, . ZProfiler . -, UI JVM. -, ZProfiler post-mortem . , OutOfMemoryError, , JVM ZProfiler, . , , , Eclipse MAT.
. . JVM, GCIH, Alibaba JDK, JWarmup — , ReadyNow Zing JVM. , ZProfiler. , , OpenJDK. , , JWarmup OpenJDK. , OpenJDK Loom, Java. , .
. , , JPoint 2018 . 2019 , JPoint , 5-6 . , Rafael Winterhalter Sebastian Daschner. . , YouTube . JPoint!